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:
- Treasury (Squads multisig vault) — the single sink. All platform revenue flows here. Nothing bypasses it.
- Incubator wallet (Privy server wallet) — the operational disbursement account. All outbound spending (agent births, wallet funding) goes through this wallet, never directly from treasury.
- 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.
| # | Source | Rate | Collection | Destination | Status |
|---|---|---|---|---|---|
| 1 | Jupiter swaps | 1% (100 BPS) | On-chain, inline in swap tx via Jupiter CPI | Squads vault ATA | Automatic |
| 2 | HL perps | 5 BPS | HL builder fee, deducted at fill settlement | HL builder wallet | Automatic (siloed) |
| 3 | HL spot sells | 50 BPS | HL builder fee, deducted at fill settlement | HL builder wallet | Automatic (siloed) |
| 4 | DBC partner fees | Config-defined | Manual claim via admin UI / script | Squads vault or incubator wallet | Manual |
| 5 | DBC creation fees | 0.2 SOL/pool | Manual claim via admin UI / script | Squads vault or incubator wallet | Manual |
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
| Network | Vault PDA | Multisig | Env Var |
|---|---|---|---|
| Mainnet | imk4zWSTsf72AmZzCbdoChDDXhmAyQTPhhXYKTGSrM8 | CXXdBzE8Ns73HjS82qxtKafUGK9ngh7XRpjrGF4jKXhi | SQUADS_VAULT_ADDRESS |
| Devnet | FXB498FYu3z9Uuu4bgNqKVjGmA9yGvqr8nSNa4T5WShS | 6ghVN8yX5Qb61rqn5tBuXfGHHjoxS7uJYwr3GAG4Uxz4 | SQUADS_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.
- Incubator wallet funding — SOL transfer to keep the incubator wallet funded for agent births
- 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
| Network | Privy Wallet ID | Solana Address |
|---|---|---|
| Devnet | wun57u417vagi73x4vba4fnm | 985zWwcVcNtSnh4b2sda7KZskrcUAM5kF27j1PHvmeVp |
| Mainnet | TBD (run bootstrap-prod-privy.ts) | TBD |
What It Pays For
Agent births ($10 SOL per agent):- Virtual pool reaches $15 threshold → birth triggers
- Reconciler calls
tryBirth()inpackages/incubator/src/lifecycle/pool.ts - SOL transferred from incubator wallet to newborn's Privy Solana address
- Signed via
signAndSendSolanaTransaction(poolWalletId, tx)
- 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:
- SOL balance (minus rent reserve of 890k lamports + 5k fee buffer)
- All SPL token balances
- Sent to
INCUBATOR_WALLET_ADDRESS - Recorded as
wallet_sweptlifecycle 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):
| Column | Type | Purpose |
|---|---|---|
balanceUsd | float | Running USD balance |
birthStatus | enum | 'accruing' / 'pending' / 'birthing' |
poolWalletId | text | Privy wallet ID |
poolWalletAddress | text | Solana address |
totalAgentsBorn | int | Cumulative birth count |
currentGeneration | int | Advances every 10 births |
birthPaused | bool | Kill switch |
incubator_transactions table (audit log):
| Column | Type | Purpose |
|---|---|---|
amountUsd | float | Signed: positive = deposit, negative = debit |
source | text | 'trading_fee' / 'agent_birth' / 'dbc_fee_claim' |
sourceTradeId | uuid | FK to trade that generated the deposit |
adminNote | text | Optional 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
depositToPool()incrementsbalanceUsdafter each tradeupdateBirthStatusIfReady()flips status to'pending'when balance >= $15- 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)
- Atomically locks:
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):
| Type | Trigger | What's Claimed |
|---|---|---|
trading | Ongoing swap activity | Accumulated partner trading fees (base + quote) |
migration | Pool reaches migration cap, auto-migrates to DAMM v2 | 30 BPS of migration threshold |
creation | Pool exists | 0.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).
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
}authenticateAgent(request)— verify agent identity- Look up agent's
token_launches→ verifypoolAddressbelongs to this agent - Query unclaimed creator fees via
getPoolFeeBreakdown(rpcUrl, poolAddress).creator - If unclaimed fees > 0:
buildClaimCreatorFeeTx(rpcUrl, { pool, creator: agent.solanaAddress })— new function mirroringbuildClaimPartnerFeeTxsignAndSendSolanaTransaction(agent.privySolanaWalletId, unsignedTx)
- Record claim in new
dbc_creator_fee_claimstable (or extenddbc_fee_claimswith afeeTypecolumn) - Return tx signature + amounts
buildClaimCreatorFeeTx()inpackages/solana/src/dbc-fees.ts— calls Meteora SDKclaimCreatorTradingFee()(mirrors existing partner claim builder)POST /api/v1/agents/me/fees/claim-dbcroute inapps/web/app/api/v1/agents/me/fees/- DB tracking: either a new
dbc_creator_fee_claimstable or afeeType: 'partner' | 'creator'column ondbc_fee_claims - Admin visibility: extend
/admin/dbc-poolsto show creator fee status alongside partner fees
getPoolFeeBreakdown()already returnscreator.unclaimedBaseFee/unclaimedQuoteFee—packages/solana/src/dbc-fees.ts- Privy wallet policy already whitelists the DBC program
signAndSendSolanaTransaction()handles any unsigned txauthenticateAgent()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:
- Direct referral fees —
referrerFeeBpsof gross trade volume (Jupiter 10 BPS, HL perps 1 BPS, HL spot sell 10 BPS) - Legacy organic fees — 0.1% of volume for pre-tracking trades
- DBC partner fees — 50% of claimed partner fees (
DBC_REFERRER_SHARE_RATE)
- Admin queries
/api/v1/admin/referrals/approval-queue— filters forpending earnings >= $5 - Admin approves via
POST /api/v1/admin/referrals/approve— createsreferral_payoutsrow - Admin opens Squads app via deep-link, manually creates transfer proposal
- Multisig members vote + approve + execute
- 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.
- Manual periodic withdrawal — withdraw from HL to an EVM wallet, bridge to Solana, deposit to vault
- Accept the silo — treat HL builder wallet as a separate treasury leg, tracked off-chain
- 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_KEYunset in prod Infisical.
tasks/treasury-multisig-handoff.md):
- Run
packages/solana/scripts/create-dbc-config.tswithFEE_CLAIMER= Squads vault address (devnet for staging, mainnet for prod) - Register config in
dbc_configsDB table via admin UI or seed script - Set
METEORA_DBC_CONFIG_KEYin Infisical for both environments - Final claim of unclaimed fees on old staging config before switching
- 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_ADDRESSfrom Infisical after deploy verification
Infrastructure
- Remove
METEORA_DBC_CONFIG_KEYenv var — use DBdbc_configstable 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)