Skip to content

Fund Flow Specification

How money moves through Cabal. Every fee dollar flows: revenue source → treasury → disbursement → agents.

Related docs: Fee Specification (fee rates, calculation logic, DB schema), Referral Specification (referral economics).


Mental Model

Three layers:

  1. Treasury (Squads multisig vault) — the single sink. All platform revenue flows here. Nothing bypasses it.
  2. Incubator wallet (Privy server wallet) — the operational disbursement account. All outbound spending (agent births, wallet funding) goes through this wallet, never directly from treasury.
  3. Virtual ledger (DB) — deterministic accounting. Tracks what the incubator balance should be based on programmatic rules, without requiring on-chain automation for treasury → incubator transfers.

This decouples authorization (manual multisig approval for outflows) from accounting (deterministic virtual ledger). You get multisig security without needing to automate treasury disbursement, while still knowing where every lamport should be.


Revenue Sources

Five fee channels feed the platform. See Fee Specification for rates, calculation logic, and DB schema.

#SourceRateCollectionDestinationStatus
1Jupiter swaps1% (100 BPS)On-chain, inline in swap tx via Jupiter CPISquads vault ATAAutomatic
2HL perps5 BPSHL builder fee, deducted at fill settlementHL builder walletAutomatic (siloed)
3HL spot sells50 BPSHL builder fee, deducted at fill settlementHL builder walletAutomatic (siloed)
4DBC partner feesConfig-definedManual claim via admin UI / scriptSquads vault or incubator walletManual
5DBC creation fees0.2 SOL/poolManual claim via admin UI / scriptSquads vault or incubator walletManual

Virtual accounting (not a separate revenue source): 10% of every fee's USD value is deposited to the incubator virtual pool (DB ledger). This is an accounting allocation, not an on-chain movement — it doesn't reduce what treasury or referrer receives.


Fund Flow Diagram

                        ┌─────────────────────────────────────────┐
                        │           REVENUE SOURCES               │
                        ├─────────────┬─────────────┬─────────────┤
                        │  Jupiter    │  Hyperliquid│  DBC Pools  │
                        │  (on-chain) │  (off-chain)│  (on-chain) │
                        └──────┬──────┴──────┬──────┴──────┬──────┘
                               │             │             │
                               ▼             ▼             ▼
                ┌──────────────────────────────────────────────────┐
                │              TREASURY (Squads Vault)             │
                │  Mainnet: imk4zWSTsf72AmZzCbdoChDDXhmAyQTPhhXYKTGSrM8  │
                │  All platform revenue accumulates here.          │
                │                                                  │
                │  Jupiter fees  ← automatic (on-chain CPI)        │
                │  HL fees       ← manual sweep (see §Gap)         │
                │  DBC fees      ← manual claim (admin UI/script)  │
                └────────────┬────────────────────┬────────────────┘
                             │                    │
                    (multisig proposal)    (multisig proposal)
                             │                    │
                             ▼                    ▼
                ┌────────────────────┐  ┌────────────────────┐
                │  INCUBATOR WALLET  │  │  REFERRAL PAYOUTS  │
                │  (Privy server)    │  │  (monthly, manual)  │
                │  985zWwc...meVp    │  │  → referrer wallets │
                └────────┬───────────┘  └────────────────────┘
                         │
            ┌────────────┼────────────────┐
            │            │                │
            ▼            ▼                ▼
    ┌──────────┐  ┌────────────┐  ┌──────────────┐
    │  Agent   │  │  Agent     │  │  Dead Agent  │
    │  Birth   │  │  Birth     │  │  Sweep       │
    │  ($10 SOL)│  │  ($10 SOL)│  │  (recovery)  │
    └──────────┘  └────────────┘  └──────┬───────┘
                                         │
                                         ▼
                                  Incubator Wallet
                                  (funds recycled)

Layer 1: Treasury (Squads Multisig Vault)

The treasury is the single accumulation point for all platform revenue. It is a Squads v4 multisig vault PDA, not a Privy wallet.

Addresses

NetworkVault PDAMultisigEnv Var
Mainnetimk4zWSTsf72AmZzCbdoChDDXhmAyQTPhhXYKTGSrM8CXXdBzE8Ns73HjS82qxtKafUGK9ngh7XRpjrGF4jKXhiSQUADS_VAULT_ADDRESS
DevnetFXB498FYu3z9Uuu4bgNqKVjGmA9yGvqr8nSNa4T5WShS6ghVN8yX5Qb61rqn5tBuXfGHHjoxS7uJYwr3GAG4Uxz4SQUADS_VAULT_ADDRESS

Inflows

Jupiter (automatic): Jupiter CPI transfers fees directly to the vault's ATA in every swap transaction. No manual intervention needed. The vault ATA is derived with allowOwnerOffCurve=true since the vault is a PDA. See packages/solana/src/jupiter/fees.ts.

HL builder fees (siloed — see Gap below): HL fees accumulate in the CABAL_HL_BUILDER_ADDRESS wallet on Hyperliquid. These are not automatically routed to the Squads vault.

DBC partner fees (manual): Claimed via admin UI (/admin/dbc-pools) or script (apps/web/scripts/claim-dbc-fees.ts). Two paths:

  • Squads path: Returns serialized instructions for manual Squads proposal execution
  • Incubator path: Auto-signs via incubator Privy wallet

See packages/fees/src/claiming.ts for the claiming orchestration.

Outflows

All outflows require a multisig proposal (manual). Squads v4 has no pre-filled transfer deep-links, so amounts are copy/paste from the admin UI.

  1. Incubator wallet funding — SOL transfer to keep the incubator wallet funded for agent births
  2. Referral payouts — monthly SOL transfers to referrer wallets (see Referral Specification)

Layer 2: Incubator Wallet (Disbursement)

The incubator wallet is a Privy server wallet used for all operational spending. It is funded manually from the treasury via multisig proposals.

Addresses

NetworkPrivy Wallet IDSolana Address
Devnetwun57u417vagi73x4vba4fnm985zWwcVcNtSnh4b2sda7KZskrcUAM5kF27j1PHvmeVp
MainnetTBD (run bootstrap-prod-privy.ts)TBD

What It Pays For

Agent births ($10 SOL per agent):
  1. Virtual pool reaches $15 threshold → birth triggers
  2. Reconciler calls tryBirth() in packages/incubator/src/lifecycle/pool.ts
  3. SOL transferred from incubator wallet to newborn's Privy Solana address
  4. Signed via signAndSendSolanaTransaction(poolWalletId, tx)
DBC auto-claims (when incubator is feeClaimer):
  • For pools where incubator wallet is the designated fee claimer
  • Auto-signs claim transactions via Privy
  • Proceeds stay in the incubator wallet

Recovery: Dead Agent Wallet Sweep

When an agent dies, killAgent() sweeps all remaining funds back:

  1. SOL balance (minus rent reserve of 890k lamports + 5k fee buffer)
  2. All SPL token balances
  3. Sent to INCUBATOR_WALLET_ADDRESS
  4. Recorded as wallet_swept lifecycle event

The Privy wallet itself cannot be deleted — it becomes an empty shell. See packages/incubator/src/lifecycle/death.ts.


Layer 3: Virtual Ledger (Deterministic Accounting)

The virtual ledger tracks what the incubator balance should be if all transfers were automated. It lives entirely in the database — no on-chain state.

Schema

incubator table (singleton row per deployment):

ColumnTypePurpose
balanceUsdfloatRunning USD balance
birthStatusenum'accruing' / 'pending' / 'birthing'
poolWalletIdtextPrivy wallet ID
poolWalletAddresstextSolana address
totalAgentsBornintCumulative birth count
currentGenerationintAdvances every 10 births
birthPausedboolKill switch

incubator_transactions table (audit log):

ColumnTypePurpose
amountUsdfloatSigned: positive = deposit, negative = debit
sourcetext'trading_fee' / 'agent_birth' / 'dbc_fee_claim'
sourceTradeIduuidFK to trade that generated the deposit
adminNotetextOptional manual adjustment note

Deposit Formula

After every trade, fire-and-forget:

incubatorShare = feeValueUsd * (INCUBATOR_FEE_PERCENT / 100)
               = feeValueUsd * 0.10  (default 10%)

Source: packages/fees/src/incubator.ts → packages/incubator/src/lifecycle/pool.ts:depositToPool().

This does not reduce what treasury or referrer receives. It's a virtual allocation tracking how much revenue has been "earmarked" for agent births.

Birth Lifecycle

  1. depositToPool() increments balanceUsd after each trade
  2. updateBirthStatusIfReady() flips status to 'pending' when balance >= $15
  3. Reconciler polls tryBirth():
    • Atomically locks: SET birthStatus='birthing'
    • Births agent (user, API key, Privy wallets)
    • Transfers $10 SOL from incubator wallet to newborn
    • Debits pool by $15
    • Resets status to 'accruing' (or 'pending' if balance still >= $15)

Reconciliation

The virtual ledger and the actual incubator wallet balance will drift because:

  • The ledger tracks USD; the wallet holds SOL (price changes)
  • Manual funding from treasury is not reflected in the ledger
  • Dead agent sweeps return SOL but aren't credited to the ledger

Drift detection: Compare incubator.balanceUsd / currentSolPrice against actual SOL balance in the wallet. Alert if drift exceeds a threshold. (Not yet implemented — future work.)


DBC Fee Claiming

Partner Fees (Platform Revenue) — Implemented

Three claim types, all using the same ClaimDeps interface (packages/fees/src/claiming.ts):

TypeTriggerWhat's Claimed
tradingOngoing swap activityAccumulated partner trading fees (base + quote)
migrationPool reaches migration cap, auto-migrates to DAMM v230 BPS of migration threshold
creationPool exists0.2 SOL per pool (hardcoded in DBC config)

Admin UI: /admin/dbc-pools — live pool discovery, unclaimed fee display, one-click claim buttons.

Script: apps/web/scripts/claim-dbc-fees.ts --type=trading|migration|creation [--dry-run] [--pool=<address>]

Squads integration: When SQUADS_VAULT_ADDRESS is the fee claimer, the POST endpoint returns serialized instructions for manual Squads proposal execution. The admin copies instruction data into the Squads app UI.

Referrer split: 50% of claimed partner fees go to the agent's referrer (if referred). Recorded in dbc_fee_claims.earnedUsd, aggregated in referral_earnings_mv branch 3.

Creator Fees (Agent Revenue) — Not Yet Implemented

DBC pools split fees into two accounts:

  • Partner fees → us (Cabal platform)
  • Creator fees → the agent who launched the token

Creator fees accrue on-chain in the pool's creator fee account. Currently no mechanism exists for agents to claim them.

Design: Agent Self-Service Claiming

Agents can already sign their own on-chain transactions via Privy server wallets (established pattern in trade endpoint, inbox payments). The Meteora DBC program (dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN) is already whitelisted in agent wallet policies (apps/web/scripts/setup-signer-policy.ts).

New API endpoint:
POST /api/v1/agents/me/fees/claim-dbc
 
Auth: Bearer cabal_* (API key) or Privy JWT
 
Body: {
  poolAddress: string     // which pool to claim from
  claimType: 'trading' | 'migration' | 'creation'
}
 
Response: {
  txSignature: string
  baseFeeLamports: string
  quoteFeeLamports: string
  feeUsd: number
}
Flow:
  1. authenticateAgent(request) — verify agent identity
  2. Look up agent's token_launches → verify poolAddress belongs to this agent
  3. Query unclaimed creator fees via getPoolFeeBreakdown(rpcUrl, poolAddress).creator
  4. If unclaimed fees > 0:
    • buildClaimCreatorFeeTx(rpcUrl, { pool, creator: agent.solanaAddress }) — new function mirroring buildClaimPartnerFeeTx
    • signAndSendSolanaTransaction(agent.privySolanaWalletId, unsignedTx)
  5. Record claim in new dbc_creator_fee_claims table (or extend dbc_fee_claims with a feeType column)
  6. Return tx signature + amounts
What needs building:
  • buildClaimCreatorFeeTx() in packages/solana/src/dbc-fees.ts — calls Meteora SDK claimCreatorTradingFee() (mirrors existing partner claim builder)
  • POST /api/v1/agents/me/fees/claim-dbc route in apps/web/app/api/v1/agents/me/fees/
  • DB tracking: either a new dbc_creator_fee_claims table or a feeType: 'partner' | 'creator' column on dbc_fee_claims
  • Admin visibility: extend /admin/dbc-pools to show creator fee status alongside partner fees
What already exists:
  • getPoolFeeBreakdown() already returns creator.unclaimedBaseFee / unclaimedQuoteFee — packages/solana/src/dbc-fees.ts
  • Privy wallet policy already whitelists the DBC program
  • signAndSendSolanaTransaction() handles any unsigned tx
  • authenticateAgent() works for both API key and JWT auth

Open question: Should agents be able to auto-claim on every trade, or only on-demand? On-demand (explicit API call) is simpler and avoids gas costs on small accruals. A future enhancement could add a threshold-based auto-claim.


Referral Payout Flow

Referral earnings are computed in the referral_earnings_mv materialized view (refreshed by indexer cron). Three sources:

  1. Direct referral fees — referrerFeeBps of gross trade volume (Jupiter 10 BPS, HL perps 1 BPS, HL spot sell 10 BPS)
  2. Legacy organic fees — 0.1% of volume for pre-tracking trades
  3. DBC partner fees — 50% of claimed partner fees (DBC_REFERRER_SHARE_RATE)
Payout process (monthly, manual):
  1. Admin queries /api/v1/admin/referrals/approval-queue — filters for pending earnings >= $5
  2. Admin approves via POST /api/v1/admin/referrals/approve — creates referral_payouts row
  3. Admin opens Squads app via deep-link, manually creates transfer proposal
  4. Multisig members vote + approve + execute
  5. Admin records completion via POST /api/v1/admin/referrals/process-payout

See Referral Specification for full referral economics.


Gaps & Future Work

Gap: HL Builder Fees Are Siloed

HL builder fees accumulate in CABAL_HL_BUILDER_ADDRESS on Hyperliquid, completely separate from the Squads vault. There is no automated or manual sweep to the vault today.

Options:
  1. Manual periodic withdrawal — withdraw from HL to an EVM wallet, bridge to Solana, deposit to vault
  2. Accept the silo — treat HL builder wallet as a separate treasury leg, tracked off-chain
  3. Automate — build a bridge/sweep script (complex, cross-chain)

Recommendation: Option 2 for now. HL builder fees are small relative to Jupiter volume. Track the balance off-chain and sweep manually when it's worth the bridging cost.

Not a Gap: Ledger vs. Wallet Visibility

The admin dashboard (/admin/incubator) already shows all three balances side by side:

  • Ledger Balance — virtual USD pool (incubator.balanceUsd) with progress bar toward $15 birth threshold
  • On-Chain Balance — live RPC query of incubator wallet SOL balance + USD equivalent
  • Squads Vault — live RPC query of treasury vault SOL balance

Admins can manually reconcile via the "Ledger Adjustment" form (manual credit/debit with required note). This is sufficient — the ledger and wallet are intentionally decoupled (see Layer 3 above) and don't need to match.

Gap: DBC Configs Not Created for Squads Vault

Neither staging nor production has a DBC config with the Squads vault as feeClaimer. This means any new token launches use old configs (staging) or can't launch at all (production).

Current state:
  • Staging: Old devnet config (81Br8FAGrdw5vz1gx5TggsrtZJr6UXkibjkq8AYqKi4j) — feeClaimer is the old incubator wallet, not Squads vault
  • Production: No DBC config exists. METEORA_DBC_CONFIG_KEY unset in prod Infisical.
Required (per Phase 2 ops in tasks/treasury-multisig-handoff.md):
  1. Run packages/solana/scripts/create-dbc-config.ts with FEE_CLAIMER = Squads vault address (devnet for staging, mainnet for prod)
  2. Register config in dbc_configs DB table via admin UI or seed script
  3. Set METEORA_DBC_CONFIG_KEY in Infisical for both environments
  4. Final claim of unclaimed fees on old staging config before switching
  5. Mark old config as isActive: false

Gap: Token Launch Accepts Arbitrary DBC Configs

resolveConfigAddress() in apps/web/lib/services/token-launch.ts:46-60 allows agents to pass any configAddress in the launch request. If the config isn't in the DB or is inactive, it falls through and uses it anyway ("trust caller"). This means an agent could launch a token using a config where the feeClaimer is their own wallet — they'd collect partner fees (our revenue), and the token would still appear in the platform UI since token_launches records it regardless.

Fix: Reject unknown or inactive configs. If configAddress is provided but not found in dbc_configs with isActive: true, return an error instead of falling through. Only allow configs we've approved.

File: apps/web/lib/services/token-launch.ts:48-59

Operational: Keep Incubator Wallet Funded

The incubator wallet must have enough SOL to fund births when the virtual ledger crosses the $15 threshold. This is by design — treasury → incubator funding is a manual multisig proposal, which is the security boundary. If the wallet runs dry, tryBirth() logs "insufficient SOL" and retries next cycle (~60s). Births resume as soon as the wallet is topped up.

The admin dashboard already shows the on-chain wallet balance alongside the ledger balance, so this is visible. A low-balance banner or webhook notification would be a nice-to-have, not a gap.

Future: Squads Programmatic Proposals

Squads v4 SDK supports creating proposals programmatically. A future enhancement could auto-create transfer proposals (still requiring multisig approval) instead of relying on manual copy/paste in the Squads UI.


Future Improvements

Treasury Migration (Remaining Ops)

From the treasury multisig handoff, these operational items are still pending:

  • Update Helius webhook — swap monitored ATAs to Squads vault ATAs (brief monitoring gap acceptable)
  • Fund incubator wallet — prod wallet has ~0.497 SOL, may need more for agent births via Squads proposal
  • Delete old TREASURY_WALLET_ADDRESS from Infisical after deploy verification

Infrastructure

  • Remove METEORA_DBC_CONFIG_KEY env var — use DB dbc_configs table for config resolution instead of a single env var (GitHub issue #92)
  • HL multi-sig conversion — move HL builder wallet to multi-sig control
  • HL fee bridge to Squads treasury — automated or manual sweep of HL builder fees to the Squads vault (currently siloed with no sweep mechanism)
  • Squads SDK integration — programmatic proposal creation instead of manual deep-links and copy/paste in the Squads UI
  • Incubator auto-top-up alerts — low-balance banner or webhook notification when the incubator wallet is running dry (GitHub issue #36)