Fee Specification
All revenue sources, fee distribution, referral splits, and what's configurable. Every claim is grounded in code with file:line references.
Fee Policy Package (@cabal/fees)
All fee constants, calculation logic, builder fee validation, trade value computation, incubator share, referral stats queries, and aggregation helpers live in packages/fees/. This is the single source of truth for fee policy across the monorepo.
src/protocols.ts— Fee schedule registry (FEE_SCHEDULES) + all named constantssrc/calculate.ts—calculateFees(db, agentId, protocol)+calculateFeesBps()(pure)src/builder.ts—isValidBuilderFill(),calculateBuilderRevenue()(pure math)src/trade-utils.ts—computeFeeAmount(),computeTradeValues()(pure)src/incubator.ts—computeIncubatorShare(feeValueUsd, protocol)src/stats.ts—getReferralStats(),getPublicReferralStats(),getReferralList()src/aggregation.ts—aggregateReferrerEarnings(),buildReferrerSummaries(),getCompletedPayoutTotals()src/lookup.ts—lookupByReferralCode()src/dbc.ts—recordDbcFeeClaim()
Dependencies: @cabal/client (Result type), @cabal/db (schema, CabalDb type), drizzle-orm.
Revenue Sources
Cabal earns fees from three trade types. Each has a different fee mechanism, rate, and collection method.
1. Jupiter / Solana Swaps
| Property | Value |
|---|---|
| Fee rate | 1% (100 BPS) of input amount, reduced to 0.9% (90 BPS) for referred agents |
| Collection | On-chain: Jupiter embeds fee transfer to treasury ATA in swap tx |
| Recipient | SQUADS_VAULT_ADDRESS (Squads v4 multisig vault PDA) |
| Constant | PLATFORM_FEE_BPS = 100 — packages/fees/src/protocols.ts:61 (derived from FEE_SCHEDULES.jupiter.baseBps) |
handleSolanaTrade()callscalculateFees(db, agent.id, 'jupiter')—apps/web/app/api/v1/trade/route.tsbuildTrade()passesfees.effectiveFeeBps(90 or 100) asplatformFeeBpsto Jupiter's/quoteendpoint —packages/solana/src/jupiter/fees.ts- Jupiter returns a quote with embedded
platformFee.amountin the response buildUnsignedSwapTx()sends the treasury's ATA asfeeAccountto Jupiter's/swapendpoint —packages/solana/src/jupiter/fees.ts- Jupiter builds a versioned tx with a CPI that sends the fee directly to the treasury ATA
Fee mint selection (packages/solana/src/jupiter/fees.ts):
- If output mint is SOL or stablecoin → fee collected in output token (standard
/swappath) - Else if input mint is SOL or stablecoin → fee collected in input token
- Else (exotic pair) → uses
/swap-instructionspath withcreateAssociatedTokenAccountIdempotentInstructionprepended to create the treasury ATA on-chain - Fallback: if
/swap-instructionsfails (route too complex for legacy tx), re-quotes withplatformFeeBps: 0— trade goes through but no fee is collected
Fee amount computation (packages/fees/src/trade-utils.ts):
computeFeeAmount(quote.platformFee?.amount, fromSmallestUnit, inputAmount, effectiveFeeBps)Uses Jupiter's reported platformFee.amount if available; otherwise estimates as inputAmount * effectiveFeeBps / 10000.
2. Hyperliquid Builder Fees
| Property | Value |
|---|---|
| Perps fee | 5 BPS (0.05%) of notional value |
| Spot sell fee | 50 BPS (0.5%) of sell value |
| Spot buy fee | 0 (no fee) |
| Collection | Off-chain: HL deducts from fill settlement |
| Recipient | CABAL_HL_BUILDER_ADDRESS env var |
| Constants | BUILDER_FEE_PERPS = 50, BUILDER_FEE_SPOT_SELL = 500 (tenths of BPS) — packages/fees/src/protocols.ts:74-76 |
- On first trade,
approveBuilderFee(account)callsclient.approveBuilderFee({ maxFeeRate: '0.05%' })—packages/hyperliquid/src/exchange.ts - Orders include
{ builder: { b: CABAL_BUILDER_ADDRESS, f: 50|500 } }viagetBuilderParams(isSpot)—packages/hyperliquid/src/builder-fee.ts:31-36 - HL deducts the fee during fill settlement
recordHLFill()persistsfeeAmount: fill.builderFee || 0—packages/hyperliquid/src/verification.ts- Fill validation with 10% tolerance:
isValidBuilderFill()—packages/fees/src/builder.ts:10-36
Builder wallet requirement: Must hold 100+ USDC on HL perps to collect fees.
3. DBC (Meteora Bonding Curve) Partner Fees
| Property | Value |
|---|---|
| Fee rate | Set on-chain per pool (creator/partner fee config in DBC program) |
| Collection | Manual batch claim via apps/web/scripts/claim-dbc-fees.ts |
| Recipient | Incubator Privy wallet, then split |
| Tracking | dbc_fee_claims table — packages/db/src/schema/referrals.ts:21-35 |
DBC pools accrue trading fees on-chain (base + quote). Partner fees are claimed periodically by calling claimPartnerTradingFee2() from the Meteora DBC SDK. Each claim is recorded in dbc_fee_claims with USD value and referrer attribution.
Fee Distribution
Every fee dollar flows through up to four destinations: treasury, referrer, incubator pool, and (for DBC) on-chain pool.
Jupiter / Solana
Gross input amount (e.g. $1,000)
│
├─ Non-referred agent: 1.0% fee ($10)
│ ├─ 100% → Treasury ($10) — on-chain via Jupiter CPI
│ └─ 10% of fee → Incubator pool ($1) — virtual USD ledger
│
└─ Referred agent: 0.9% fee ($9) — on-chain only 90 BPS collected
├─ Treasury keeps: effectiveFeeBps - referrerRewardBps = 80 BPS ($8) — on-chain
├─ Referrer: 10 BPS of gross volume ($1) — matview-computed, paid out monthly
└─ 10% of fee → Incubator pool ($0.90) — virtual USD ledgerKey detail: The referral split is not enforced on-chain. Jupiter collects 90 BPS total to the treasury. The referrer's 10 BPS share is computed in the materialized view and paid out separately.
Referrer earning formula (matview, packages/db/src/schema/referrals.ts):
SUM((t."valueUsd" + t."feeAmount") * t.referrer_fee_bps / 10000.0)Incubator deposit (packages/fees/src/incubator.ts + packages/incubator/src/lifecycle/pool.ts):
incubatorShare = computeIncubatorShare(feeValueUsd, protocol) = feeValueUsd * (schedule.incubatorPct / 100)Called fire-and-forget after trade confirmation — apps/web/app/api/v1/trade/route.ts.
Hyperliquid
Trade value (e.g. $10,000 perps)
│
├─ Builder fee: 5 BPS ($5.00)
│ └─ 100% → Builder wallet (all to Cabal, on HL settlement)
│
└─ 10% of estimated builder fee → Incubator pool ($0.50)Incubator deposit (apps/web/app/api/v1/trade/route.ts):
const feeRate = FEE_SCHEDULES[hlProtocol].baseBps / 10_000 // 0.0005 for perps, 0.005 for spot sells
const builderFee = result.filled ? valueUsd * feeRate : 0Uses the protocol-specific fee rate from the fee schedule registry. (Previously used hardcoded 0.0005 for all trade types — fixed in @cabal/fees extraction.)
Referral tracking (apps/web/app/api/v1/trade/route.ts): calculateFees(db, agent.id, hlProtocol) is called with the specific HL protocol, returning protocol-correct referral rates. The result is passed to recordHLFill() as referralInfo (verification.ts). HL perps get 1 BPS referrer reward; HL spot sells get 10 BPS. (Previously used Solana's 10 BPS for all HL trades — fixed in @cabal/fees extraction.)
DBC Partner Fees
Partner fee claim (e.g. 0.5 SOL = $75)
│
├─ Non-referred agent: 100% → incubator wallet
│
└─ Referred agent:
├─ 50% → Referrer ($37.50)
└─ 50% → Incubator wallet ($37.50)DBC_REFERRER_SHARE_RATE = 0.5 — packages/fees/src/protocols.ts:69. Recorded per-claim in dbc_fee_claims.referrer_share_rate (packages/db/src/schema/referrals.ts:32). Aggregated in matview branch 3.
Fee Calculation Logic
calculateFees(db, agentId, protocol) — packages/fees/src/calculate.ts
Fee calculation is protocol-aware. Each protocol has its own fee schedule in FEE_SCHEDULES (packages/fees/src/protocols.ts):
| Protocol | baseBps | referrerBps | discountBps | incubatorPct |
|---|---|---|---|---|
jupiter | 100 | 10 | 10 | 10 |
hl-perps | 5 | 1 | 0 | 10 |
hl-spot-sell | 50 | 10 | 0 | 10 |
hl-spot-buy | 0 | 0 | 0 | 0 |
dbc | 0 | 0 | 0 | 10 |
Input: agentId, protocol (defaults to 'jupiter')
Output: { protocol, schedule, effectiveFeeBps, referrerRewardBps, treasuryFeeBps, referrerId, feeDiscount }
Jupiter, non-referred:
effectiveFeeBps = 100 (schedule.baseBps)
referrerRewardBps = 0
treasuryFeeBps = 100
Jupiter, referred (user.feeDiscount defaults to 0.1):
discount = user.feeDiscount || (schedule.discountBps / schedule.baseBps) = 0.1
effectiveFeeBps = round(100 * (1 - 0.1)) = 90
referrerRewardBps = 10 (schedule.referrerBps)
treasuryFeeBps = 90 - 10 = 80
HL perps, referred:
discountBps = 0, so no discount applied
effectiveFeeBps = 5 (schedule.baseBps)
referrerRewardBps = 1 (schedule.referrerBps)
treasuryFeeBps = 4
HL spot sell, referred:
discountBps = 0, so no discount applied
effectiveFeeBps = 50 (schedule.baseBps)
referrerRewardBps = 10 (schedule.referrerBps)
treasuryFeeBps = 40Constants imported from @cabal/fees (packages/fees/src/protocols.ts):
PLATFORM_FEE_BPS = 100(=FEE_SCHEDULES.jupiter.baseBps)BASE_FEE_BPS = 100(alias)REFERRER_REWARD_BPS = 10(=FEE_SCHEDULES.jupiter.referrerBps)REFEREE_DISCOUNT_BPS = 10(=FEE_SCHEDULES.jupiter.discountBps)
Referral linking happens at registration:
apps/web/lib/services/registration.ts— setsfeeDiscount: 0.1apps/web/lib/services/device-auth.ts— samefeeDiscount: 0.1
Referral Economics Summary
| Source | Cabal's fee | Referrer's cut | Referrer as % of Cabal's fee |
|---|---|---|---|
| Jupiter (Solana) | 90 BPS (with referral) | 10 BPS of gross volume | 12.5% of treasury take (10/80) |
| HL perps | 5 BPS | 1 BPS of gross volume | 25% of treasury take (1/4) |
| HL spot sell | 50 BPS | 10 BPS of gross volume | 25% of treasury take (10/40) |
| HL spot buy | 0 BPS | N/A (no fee) | N/A |
| DBC pool | variable | 50% of partner share | 50% |
What's Configurable
Per-Agent (DB columns)
| Column | Table | Schema location | Effect | Current usage |
|---|---|---|---|---|
feeDiscount | user | packages/db/src/schema/core.ts:66 | Multiplier: effectiveFeeBps = BASE_FEE_BPS * (1 - feeDiscount) | Set to 0.1 on registration if referred. Could support VIP tiers. |
referredBy | user | packages/db/src/schema/core.ts:63 | FK to referrer. If set, calculateFees() returns referrer info. | Set at registration via --ref=code flag |
referralCode | user | packages/db/src/schema/core.ts:64 | Referral code = Twitter username (lowercase). Used for lookupByReferralCode(). | Set at agent creation |
referralPayoutWallet | user | packages/db/src/schema/core.ts:65 | Payout destination for referral earnings | Set by referrer via dashboard |
Environment Variables
| Variable | Default | Effect |
|---|---|---|
INCUBATOR_FEE_PERCENT | 10 | % of trade fees deposited to incubator pool — packages/fees/src/protocols.ts:84 |
INCUBATOR_BIRTH_THRESHOLD_USD | 15 | Pool balance threshold to trigger agent birth — packages/fees/src/protocols.ts:87 |
SQUADS_VAULT_ADDRESS | (required) | Squads vault PDA for Jupiter fee collection. Treasury ATA derived in fees.ts:109-127 |
CABAL_HL_BUILDER_ADDRESS | (required) | EVM address for HL builder fee collection — packages/hyperliquid/src/builder-fee.ts:7 |
JUPITER_API_KEY | (required) | API key for Jupiter fee-bearing swap API (api.jup.ag/swap/v1) |
METEORA_DBC_CONFIG_KEY | (required) | DBC config for partner fee queries |
Hardcoded Constants
All fee constants are defined in packages/fees/src/protocols.ts (single source of truth):
| Constant | Value | Notes |
|---|---|---|
PLATFORM_FEE_BPS | 100 | = FEE_SCHEDULES.jupiter.baseBps. Jupiter fee rate passed to quote API |
BASE_FEE_BPS | 100 | Alias for PLATFORM_FEE_BPS |
REFERRER_REWARD_BPS | 10 | = FEE_SCHEDULES.jupiter.referrerBps. Jupiter referrer's cut |
REFEREE_DISCOUNT_BPS | 10 | = FEE_SCHEDULES.jupiter.discountBps. Jupiter fee discount for referred agents |
DBC_REFERRER_SHARE_RATE | 0.5 | = FEE_SCHEDULES.dbc.referrerShareRate. DBC referrer split |
BUILDER_FEE_PERPS | 50 (tenths of BPS = 5 BPS) | HL perps builder fee (HL API format) |
BUILDER_FEE_SPOT_SELL | 500 (tenths of BPS = 50 BPS) | HL spot sell builder fee (HL API format) |
BUILDER_FEE_RATE_PERPS | 0.0005 | = BUILDER_FEE_PERPS / 100_000. Decimal fraction for perps |
BUILDER_FEE_RATE_SPOT | 0.005 | = BUILDER_FEE_SPOT_SELL / 100_000. Decimal fraction for spot sells |
maxFeeRate (HL approval) | '0.05%' | Builder fee approval ceiling — packages/hyperliquid/src/exchange.ts |
Fee Tracking in Database
trades table — packages/db/src/schema/core.ts:162-201
| Column | Type | Line | Description |
|---|---|---|---|
feeAmount | doublePrecision | 177 | USD value of fee charged on this trade |
referrerId | uuid (nullable) | 178 | Who referred the trading agent |
referrerFeeBps | integer (nullable) | 179 | BPS the referrer earns — currently stores Solana constant for all trade types |
treasuryFeeBps | integer (nullable) | 180 | BPS treasury keeps — same problem |
Partial index on referrer_id WHERE referrer_id IS NOT NULL — core.ts:200
trade_builds table — packages/db/src/schema/core.ts:204-214
| Column | Type | Line | Description |
|---|---|---|---|
feeBps | integer | 211 | Fee BPS stored at build time (default 100) |
referrerId | uuid (nullable) | 213 | Referrer at time of build |
dbc_fee_claims table — packages/db/src/schema/referrals.ts:21-35
| Column | Type | Line | Description |
|---|---|---|---|
poolAddress | text | 23 | DBC pool address |
agentId | uuid | 24 | Agent whose pool accrued the fee |
referrerId | uuid (nullable) | 25 | Referrer of the agent |
claimTxSignature | text (nullable) | 26 | On-chain tx for the claim |
baseFeeLamports | text | 27 | Raw base token claim amount |
quoteFeeLamports | text | 28 | Raw quote token claim amount |
solPrice | double | 29 | SOL/USD price at claim time |
feeUsd | double | 30 | Total USD value of claimed partner fees |
earnedUsd | double | 31 | Referrer's USD share (feeUsd * referrerShareRate) |
referrerShareRate | double | 32 | Fraction of fee for referrer (default 0.5) |
referral_payouts table — packages/db/src/schema/referrals.ts:92-107
| Column | Type | Description |
|---|---|---|
referrerId | uuid | Referrer being paid |
amount | double | Payout amount |
currency | text | Default 'SOL' |
txSignature | text (nullable) | On-chain payout tx (unique where not null) |
status | text | `'pending' |
periodStart/End | date | Payout period |
referral_earnings_mv — packages/db/src/schema/referrals.ts:39-89
Materialized view with three UNION ALL branches. Refreshed by indexer cron.
Branch 1 — New trades (line 48-58): referrer_id IS NOT NULL
SUM((t."valueUsd" + t."feeAmount") * t.referrer_fee_bps / 10000.0) AS total_earnedNote: volume base is valueUsd + feeAmount (gross), not just valueUsd.
Branch 2 — Legacy trades (line 62-74): referrer_id IS NULL but user.referredBy IS NOT NULL
SUM((t."valueUsd" + t."feeAmount") * 0.001) AS total_earnedHardcoded 0.1% for pre-referral-tracking trades.
Branch 3 — DBC claims (line 78-87):
SUM(earned_usd) AS total_earnedFrom dbc_fee_claims where referrer_id IS NOT NULL.
Both total_volume_usd and total_fees_generated are also computed per branch and aggregated in getReferralStats() — packages/fees/src/stats.ts.
Known Issues
-
HL referral BPS is wrong.FIXED in@cabal/feesextraction.calculateFees()now takes aprotocolparam and returns protocol-specific referrer BPS: 1 BPS forhl-perps, 10 BPS forhl-spot-sell, 0 forhl-spot-buy. -
No HL referral rate defined.FIXED in@cabal/feesextraction. HL referral rates are defined inFEE_SCHEDULES(packages/fees/src/protocols.ts): perps 1 BPS, spot sell 10 BPS. -
referrer_fee_bpssemantics are ambiguous. For Jupiter it means "BPS of gross volume". For HL it means the same thing but at different rates per protocol. The matview formulavolume * referrer_fee_bps / 10000works correctly now that per-tradereferrer_fee_bpsis protocol-accurate. -
Incubator pool is an internal virtual ledger.
depositToPool()takes 10% of the fee amount, but this is not subtracted from what treasury or referrer receives. The incubator pool is a virtual USD balance in theincubatortable, not an on-chain movement. It's an accounting fiction for tracking how much revenue has been "allocated" to birthing new agents. -
DBC fee claims are manual. No automated collection; depends on running
apps/web/scripts/claim-dbc-fees.ts. Core accounting logic is now inpackages/fees/src/dbc.ts. -
HL incubator deposit always uses perps rate.FIXED in@cabal/feesextraction. The trade route now usesFEE_SCHEDULES[hlProtocol].baseBps / 10_000— returns0.0005for perps and0.005for spot sells. -
Zero-fee fallback on exotic pairs. When neither input nor output is SOL/stablecoin and the
/swap-instructionspath fails, Jupiter re-quotes withplatformFeeBps: 0. The trade goes through but Cabal collects no fee. ThefeeAmountin the trade record will be 0. -
approveBuilderFeemax rate may be too low for spot. The one-time approval usesmaxFeeRate: '0.05%'(5 BPS) — sufficient for perps but below the 50 BPS spot sell rate. If HL enforces this ceiling across trade types, spot sell builder fees may be silently capped.
Implementation Files
@cabal/fees (single source of truth for fee policy)
| Module | Key exports |
|---|---|
packages/fees/src/protocols.ts | FEE_SCHEDULES, Protocol, FeeSchedule, all named constants (PLATFORM_FEE_BPS, BASE_FEE_BPS, REFERRER_REWARD_BPS, REFEREE_DISCOUNT_BPS, DBC_REFERRER_SHARE_RATE, BUILDER_FEE_*, INCUBATOR_FEE_PERCENT, BIRTH_THRESHOLD_USD) |
packages/fees/src/calculate.ts | calculateFees(db, agentId, protocol), calculateFeesBps(feeDiscount, referredBy, protocol) |
packages/fees/src/builder.ts | isValidBuilderFill(builderFee, isSpot, valueUsd), calculateBuilderRevenue(fills) |
packages/fees/src/trade-utils.ts | computeFeeAmount(), computeTradeValues(), TradeValueInputs, TradeValues |
packages/fees/src/incubator.ts | computeIncubatorShare(feeValueUsd, protocol) |
packages/fees/src/stats.ts | getReferralStats(db, referrerId), getPublicReferralStats(db, referrerId), getReferralList(db, referrerId) |
packages/fees/src/aggregation.ts | aggregateReferrerEarnings(), buildReferrerSummaries(), getCompletedPayoutTotals() |
packages/fees/src/lookup.ts | lookupByReferralCode(db, code) |
packages/fees/src/dbc.ts | recordDbcFeeClaim(db, input) |
packages/fees/src/types.ts | FeeCalculation, ReferralInfo, ReferralStats, PublicReferralStats, ReferralListEntry |
Other packages (consumers of @cabal/fees)
| Area | File | Key functions/exports |
|---|---|---|
| Jupiter fee embedding | packages/solana/src/jupiter/fees.ts | buildTrade(), getJupiterQuoteWithFee(), buildUnsignedSwapTx(), getTreasuryTokenAccount(). Imports PLATFORM_FEE_BPS from @cabal/fees. |
| Jupiter fee schemas | packages/solana/src/jupiter/schemas.ts | jupiterQuoteWithFeeSchema, jupiterSwapWithFeeResponseSchema |
| Jupiter provider | packages/solana/src/swap/jupiter-provider.ts | JupiterProvider (passes platformFeeBps through) |
| HL builder fee config | packages/hyperliquid/src/builder-fee.ts | getBuilderParams(), verifyBuilderApproval(), getBuilderInfo(), CABAL_BUILDER_ADDRESS. Re-exports isValidBuilderFill, calculateBuilderRevenue, BUILDER_FEE_* from @cabal/fees. |
| HL order + approval | packages/hyperliquid/src/exchange.ts | approveBuilderFee(), order() |
| HL fill recording | packages/hyperliquid/src/verification.ts | recordHLFill(), verifyHLFill() |
| Incubator pool deposit | packages/incubator/src/lifecycle/pool.ts | depositToPool(). Imports computeIncubatorShare, BIRTH_THRESHOLD_USD from @cabal/fees. |
| DBC fee claiming | apps/web/scripts/claim-dbc-fees.ts | Thin runner: on-chain claim → calls recordDbcFeeClaim() from @cabal/fees/dbc |
| Unified trade handler | apps/web/app/api/v1/trade/route.ts | handleSolanaTrade(), handleHyperliquidTrade(). Imports calculateFees, computeFeeAmount, computeTradeValues, FEE_SCHEDULES from @cabal/fees. |
| Primary trade fields | apps/web/lib/services/trade-utils.ts | selectPrimaryTradeFields() (only non-fee function, stays in apps/web) |
Database schema
| Area | File | Key exports |
|---|---|---|
| DBC fee + matview schema | packages/db/src/schema/referrals.ts | dbcFeeClaims, referralEarningsMv, referralPayouts |
| Trade schema | packages/db/src/schema/core.ts | trades table — feeAmount, referrerId, referrerFeeBps, treasuryFeeBps |
| Trade build schema | packages/db/src/schema/core.ts | tradeBuilds table — feeBps |
| User fee columns | packages/db/src/schema/core.ts | referredBy, referralCode, referralPayoutWallet, feeDiscount |
Other
| Area | File | Notes |
|---|---|---|
| Registration (sets discount) | apps/web/lib/services/registration.ts | Sets feeDiscount: 0.1 when referred. Uses lookupByReferralCode from @cabal/fees. |
| Device auth (sets discount) | apps/web/lib/services/device-auth.ts | Same feeDiscount: 0.1 path. Uses lookupByReferralCode from @cabal/fees. |
| Indexer (matview refresh) | apps/indexer/src/index.ts | db.refreshMaterializedView(referralEarningsMv).concurrently() |
Known Issues (Additional)
- Fee discount not clamped. If
feeDiscountis set to a value greater than 1.0,effectiveFeeBpsgoes negative. There is no guard against this — unlikely in practice but no validation exists. - DBC price impact always zero.
priceImpactPctis hardcoded to'0'for DBC swaps. Agents get no slippage warning for bonding curve trades. - Migration fee claims record $0 USD values. DBC migration fee claims store zero USD in the database, making it impossible to audit actual migration fee revenue.