Treasury and Fee Split#
The canonical home for Seesaw's taker fee curve and three-way split. This complements referrals.md (the 40% slice) and the lifecycle overview at README.md § Fee Structure.
The fee curve#
Seesaw uses a capped-linear-decay taker fee. Makers pay zero.
fee_bps(p) = min(fee_cap_bps, decay_rate_bps × (10_000 − p) / 10_000)
| Parameter | Default | Meaning |
|---|---|---|
fee_cap_bps | 200 | Ceiling: fee never exceeds 2.00% of notional |
decay_rate_bps | 600 | Slope: fee decays toward zero as p → 1.00 |
Effective rate by fill price#
| Fill price | Taker fee rate |
|---|---|
| 0.00 – 0.67 | 2.00% (capped) |
| 0.80 | 1.20% |
| 0.90 | 0.60% |
| 0.95 | 0.30% |
| 0.99 | 0.06% |
The intuition: a fill at p = 0.50 (high implied uncertainty) pays the full 2.00% cap; a fill at p = 0.95 (the market is confident) pays only 0.30%.
Example calculation#
Trade: Buy 100 YES @ 0.50 (5000 bps), notional 50 USDT
fee_bps(5000) = min(200, 600 × 5000 / 10000) = min(200, 300) = 200 (capped)
Taker fee: 50 × 0.02 = 1.00 USDT
→ Protocol: 0.50 USDT | Creator: 0.10 USDT | Referral: 0.40 USDT
Trade: Buy 100 YES @ 0.95 (9500 bps), notional 95 USDT
fee_bps(9500) = min(200, 600 × 500 / 10000) = 30 (0.30%)
Taker fee: 95 × 0.003 = 0.285 USDT
→ Protocol: 0.1425 USDT | Creator: 0.0285 USDT | Referral: 0.114 USDT
The 50/10/40 split, restated#
Every taker fee on Seesaw splits three ways. This doc is about the 50%.
| Slice | Recipient | Routing |
|---|---|---|
| 50% | Protocol treasury | One of 8 admin-configured SPL token accounts (this doc) |
| 10% | Market creator | Accumulated in the market's accumulated_creator_fees field |
| 40% | Referrer | One of 8 referrer_treasury PDAs (see referrals.md) |
Eight treasury recipients, not one#
The protocol stores treasury_recipients: [Pubkey; 8] directly in
ConfigAccount. Each entry is a plain SPL Token v1 account address owned
by whoever the protocol authority designates — typically a multisig, a
treasury custody account, or a routing wallet.
These addresses are not PDAs the program derives. They're regular SPL token accounts the admin pre-creates and registers. The admin controls them; the program just routes funds to them.
| Field | Type | Notes |
|---|---|---|
config.treasury_recipients[0..8] | [Pubkey; 8] | Eight SPL token accounts, mint = config.default_settlement_mint, all pairwise distinct, all non-zero |
Why 8?#
The same Solana write-contention argument as the referrer treasury shards:
- Solana parallelizes transactions only when their writable accounts don't overlap. If every trade in the protocol credited a single treasury account, every trade would serialize through that account.
- Eight slots let the admin spread receipts across distinct accounts so concurrent trades on different markets don't conflict.
- Eight is small enough that the indexer and any downstream reconciliation job can trivially enumerate all of them.
Unlike the referrer shards (which the program derives deterministically), the protocol treasury recipients are admin-chosen. This gives the authority flexibility — they can split across cold storage / hot wallets / operating accounts / dedicated fee accounts however they want.
Per-trade recipient selection#
When a taker places an order via PlaceOrder, the client passes
args.protocol_treasury_index: u8 to choose which of the 8 recipient
accounts receives this trade's 50% slice.
place_order validates the index before crediting any fee:
protocol_treasury_index < 8(out-of-range is rejected,TreasuryIndexOutOfRange).- The account supplied at the treasury position must match
config.treasury_recipients[protocol_treasury_index]exactly. Mismatch → reject (TreasuryRecipientMismatch). - That account must be a valid SPL Token v1 account on the settlement mint. (Already enforced when the recipient set is registered, but re-validated defense-in-depth.)
The client picks the index. The protocol enforces the index points at the right account. There's no way for a client to siphon the 50% slice to a wallet that isn't in the registered list.
How does the client pick the index?#
Clients (web, mobile, SDKs) typically use a round-robin or random selection
across [0, 8) to avoid all clients hitting the same shard. The protocol
takes no position — any valid index is accepted.
Replacing the recipient set#
The admin can replace all 8 recipients atomically via the
UpdateTreasuryRecipients (0x20) instruction.
Accounts:
0. Config PDA (writable)
1. Authority (signer, must match config.authority)
2-9. New recipient accounts (8 SPL Token accounts)
Validation#
| Check | Enforcement |
|---|---|
| Authority is the config authority | Signer must equal config.authority |
| Rate limit | TREASURY_UPDATE_RATE_LIMIT_SECONDS = 3600 (1 hour) between updates |
| Exactly 8 accounts supplied | Rejects fewer or more than 8 recipient accounts |
| Every account is owned by SPL Token program | Token-account ownership check |
Every account's mint matches config.default_settlement_mint | Token-account deserialize + mint comparison |
| All 8 are pairwise distinct | Setter rejects duplicate pubkeys |
| No zero pubkeys | Setter rejects zero/default pubkeys |
Atomicity#
The update is atomic — either all 8 entries are replaced and the new
last-update timestamp is recorded, or the entire transaction reverts. There's
no partial state where some indices point at the old set and some at the
new. This means the next PlaceOrder after a successful
UpdateTreasuryRecipients either uses the full old set or the full new
set, never a mix.
Rate limit#
A 1-hour cooldown (TREASURY_UPDATE_RATE_LIMIT_SECONDS = 3600) sits
between successive updates. This:
- Bounds how fast the recipient set can churn under an active key.
- Gives the indexer and any treasury-monitoring job time to ingest the new set before it changes again.
- Limits damage if the authority key is compromised — an attacker can only rotate the recipients once per hour, not in a tight loop.
Invariants#
Treasury recipient invariants are catalogued in
security/invariants.md as INV-FEE-T1
through INV-FEE-T5. In summary: exactly 8 recipients are always stored,
all are valid SPL token accounts on the settlement mint, all are pairwise
distinct and non-zero, trades only credit a recipient validated against
the live config, and the recipient set is replaced atomically with a 1-hour
rate limit. The 40% referral invariants (INV-REFERRAL-T1..T3) are covered
in referrals.md § Invariants.
Operational considerations (for the config authority)#
Setting recipients up#
A typical mainnet setup:
- Pre-create 8 ATAs (associated token accounts) for the settlement mint, owned by the treasury wallet(s) the protocol authority controls.
- Call
UpdateTreasuryRecipientsonce during deployment to register them. - Document the mapping (which index points where) externally — this mapping is not stored on-chain beyond the addresses themselves.
Rotating a single recipient#
There's no "rotate index N" — the only way to change the set is to call
UpdateTreasuryRecipients with all 8 new addresses. To rotate just index
N, the admin re-sends the existing 7 addresses unchanged plus the new
address at position N.
Recipient closed / lost#
If one of the 8 SPL token accounts becomes unusable (closed, owner lost
the keys, etc.), trades that pick that index will fail at the SPL Token
transfer step. The fix is UpdateTreasuryRecipients replacing that index.
Clients that observe TreasuryRecipientMismatch or token-transfer
failures on a specific index should drop that index from their selection
rotation and surface the issue to the operator.
Consolidating earnings#
Each recipient accumulates settlement-token transfers from trades. To consolidate, the recipient's owner just sweeps the SPL token balance to wherever they want. There's no on-chain "claim" — the recipient is the SPL token account; whoever controls it controls the funds.
Related docs#
how-it-works/referrals— the 40% referrer slicehow-it-works/README§ Fee Structure — fee curve, the 10% creator slice, and the high-level splithow-it-works/flow-of-funds— where every fee slice fits in the overall money flowfor-traders/referrals-and-fees— user-facing fee explanation- The multi-fee-recipients design rationale in the repository specification documents
security/invariants— INV-FEE-T1..T5 catalog