Account Layouts#
On-chain account specifications for the Seesaw protocol.
Account Overview#
ConfigAccount#
Protocol-wide configuration. Singleton per program. Stores the capped-linear-decay fee curve parameters, the three-way split (protocol / creator / referral), oracle governance state, and operational-parameter tuning.
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | authority | Pubkey | 32 | Protocol admin |
| 40 | treasury_recipients | [Pubkey; 8] | 256 | Protocol fee recipient shards |
| 296 | default_settlement_mint | Pubkey | 32 | Default settlement mint |
| 328 | closer_reward_lamports | u64 | 8 | SOL per lifecycle closer |
| 336 | max_order_size | u64 | 8 | Max shares per order |
| 344 | markets_created | u64 | 8 | Total markets created |
| 352 | total_volume_low | u64 | 8 | Total volume low 64 bits |
| 360 | total_volume_high | u64 | 8 | Total volume high 64 bits |
| 368 | total_fees_collected_low | u64 | 8 | Total fees low 64 bits |
| 376 | total_fees_collected_high | u64 | 8 | Total fees high 64 bits |
| 384 | taker_fee_bps | u16 | 2 | Fee curve cap projection |
| 386 | tick_size_bps | u16 | 2 | Price tick (default: 100) |
| 388 | _reserved_limits_a | u16 | 2 | Reserved (formerly max_orders_per_user; retired, never read in production) |
| 390 | version | u8 | 1 | Config version |
| 391 | bump | u8 | 1 | PDA bump seed |
| 392 | paused | u8 | 1 | Protocol pause flag (0 = active, 1 = paused) |
| 393 | post_only_mode | u8 | 1 | Protocol-wide post-only mode |
| 394 | referral_share_bps_of_fee | u16 | 2 | Referral share (default: 4000 = 40% of fee) |
| 396 | _padding1_b | [u8; 4] | 4 | Alignment padding |
| 400 | _reserved_limits_b | u64 | 8 | Min resting notional storage |
| 408 | protocol_fee_bps | u16 | 2 | Protocol share (default: 5000 = 50% of fee) |
| 410 | default_creator_fee_bps | u16 | 2 | Creator share (default: 1000 = 10% of fee) |
| 412 | fee_cap_bps | u16 | 2 | Curve ceiling (default: 200 = 2.00%) |
| 414 | decay_rate_bps | u16 | 2 | Curve slope (default: 600 = 6.00%) |
| 416 | max_price_staleness_seconds | u64 | 8 | Oracle staleness override |
| 424 | market_expiration_window_seconds | u64 | 8 | Force-expiry delay override |
| 432 | pyth_program_id | Pubkey | 32 | Push-feed PDA derivation program ([0;32] = use hardcoded mainnet default) |
| 464 | min_market_duration_seconds | u64 | 8 | Min market duration override |
| 472 | max_market_duration_seconds | u64 | 8 | Max market duration override |
| 480 | pending_authority | Pubkey | 32 | Pending two-step authority |
| 512 | last_fee_config_update_at | i64 | 8 | Fee config cooldown timestamp |
| 520 | pending_authority_eligible_at | i64 | 8 | Authority timelock timestamp |
| 528 | last_treasury_update_at | i64 | 8 | Treasury recipient cooldown timestamp |
| 536 | last_operational_params_update_at | i64 | 8 | UpdateOperationalParams rate-limit timestamp |
| 544 | pending_pyth_program_id | [u8; 32] | 32 | Pending oracle program-id swap; [0;32] = none pending |
| 576 | pending_pyth_eligible_at | i64 | 8 | Timelock expiry for pending oracle swap (0 = not set) |
| 584 | pyth_receiver_program_id | [u8; 32] | 32 | Pull-mode Pyth Receiver owner; [0;32] = default mainnet ID |
| 616 | pending_pyth_target | u8 | 1 | Pending swap target: 0 = push PDA derivation program, 1 = receiver owner |
| 617 | _padding_pull | [u8; 7] | 7 | Pod alignment padding (total must be a multiple of 8) |
| Total | 624 |
Fee Curve Parameters#
The taker fee is computed per-fill from the fill price, not stored as a flat rate:
fee_bps(p) = min(fee_cap_bps, decay_rate_bps × (10_000 − p) / 10_000)
fee_cap_bps = 200caps the fee at 2.00% of notional (the worst-case rate for fills near p = 0).decay_rate_bps = 600means the fee decays linearly toward zero as the fill price approaches 1.00 USDT per share.- The three share fields (
protocol_fee_bps,default_creator_fee_bps,referral_share_bps_of_fee) are shares of the total fee in bps of fee, and MUST sum to exactly10_000.
Derivation#
let (config_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"config"],
&program_id,
);
MarketAccount#
Individual market state for a single epoch (configurable duration: 60s–7 days).
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | market_id | u64 | 8 | Epoch ID = floor(timestamp/duration_seconds) |
| 16 | pyth_feed | Pubkey | 32 | Oracle price feed |
| 48 | settlement_mint | Pubkey | 32 | USDT mint |
| 80 | t_start | i64 | 8 | Epoch start time |
| 88 | t_end | i64 | 8 | Epoch end time |
| 96 | created_at | i64 | 8 | Creation timestamp |
| 104 | resolved_at | i64 | 8 | Resolution timestamp (0 if pending) |
| 112 | start_price | i64 | 8 | Opening price (0 if not captured) |
| 120 | start_price_conf | u64 | 8 | Start price confidence |
| 128 | start_price_timestamp | i64 | 8 | Start snapshot time |
| 136 | end_price | i64 | 8 | Closing price (0 if not captured) |
| 144 | end_price_conf | u64 | 8 | End price confidence |
| 152 | end_price_timestamp | i64 | 8 | End snapshot time |
| 160 | start_price_expo | i32 | 4 | Start price exponent |
| 164 | end_price_expo | i32 | 4 | End price exponent |
| 168 | total_yes_shares | u64 | 8 | SPL YES supply |
| 176 | total_no_shares | u64 | 8 | SPL NO supply |
| 184 | total_collateral | u64 | 8 | Frozen reserved counter; use vault amount |
| 192 | total_volume | u64 | 8 | Trading volume |
| 200 | total_trades | u32 | 4 | Trade count |
| 204 | total_positions | u32 | 4 | Position count |
| 208 | settled_positions | u32 | 4 | Settled count |
| 212 | max_confidence_ratio_bps | u16 | 2 | Confidence gate; 0 disables the guard |
| 214 | outcome | u8 | 1 | 0=None, 1=Up, 2=Down |
| 215 | bump | u8 | 1 | Market PDA bump |
| 216 | orderbook_bump | u8 | 1 | Orderbook PDA bump |
| 217 | vault_bump | u8 | 1 | Vault PDA bump |
| 218 | _padding | [u8; 2] | 2 | Alignment padding |
| 220 | pyth_feed_id | [u8; 32] | 32 | Expected Pyth feed id |
| 252 | yes_mint | Pubkey | 32 | YES mint PDA |
| 284 | no_mint | Pubkey | 32 | NO mint PDA |
| 316 | creator | Pubkey | 32 | Market creator |
| 348 | yes_mint_bump | u8 | 1 | YES mint bump |
| 349 | no_mint_bump | u8 | 1 | NO mint bump |
| 350 | creator_fee_bps | u16 | 2 | Creator share of taker fees |
| 352 | protocol_fee_bps | u16 | 2 | Protocol share of taker fees |
| 354 | expired | u8 | 1 | Expired flag |
| 355 | _padding2 | u8 | 1 | Alignment padding |
| 356 | duration_seconds_bytes | [u8; 8] | 8 | Duration seconds, LE encoded |
| 364 | accumulated_creator_fees_bytes | [u8; 8] | 8 | Deferred creator fees, LE encoded |
| 372 | _reserved_share_ledger_bytes | [u8; 16] | 16 | Reserved; maker share credits live in ledger |
| 388 | yes_escrow_bump | u8 | 1 | YES escrow bump |
| 389 | no_escrow_bump | u8 | 1 | NO escrow bump |
| 390 | emergency_status | u8 | 1 | Per-market emergency override |
| 391 | event_sequence_bytes | [u8; 8] | 8 | Event sequence, LE encoded |
| 399 | max_total_shares_bytes | [u8; 8] | 8 | Optional total share cap, LE encoded |
| 407 | max_oracle_jump_bps_bytes | [u8; 4] | 4 | Optional oracle jump guard, LE encoded |
| 411 | num_seats_bytes | [u8; 4] | 4 | Trader-ledger v2 capacity; 0 = v1 layout |
| 415 | _reserved2 | [u8; 37] | 37 | Future use |
| 452 | _reserved3 | [u8; 60] | 60 | Future use |
| Total | 512 |
Derivation#
let (market_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"market",
pyth_feed_id,
&duration_seconds.to_le_bytes(),
&market_id.to_le_bytes(),
creator_pubkey.as_ref(),
],
&program_id,
);
Market PDAs are scoped by feed, duration, epoch, and creator. Multiple creators can therefore run distinct markets for the same asset and window; discovery and liquidity grouping are handled off-chain.
State Encoding#
| outcome | State |
|---|---|
| 0 | Not resolved |
| 1 | UP (end >= start) |
| 2 | DOWN (end < start) |
OrderbookAccount#
Order book for a market with bids and asks.
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | market | Pubkey | 32 | Parent market |
| 40 | next_order_id | u64 | 8 | Next order ID |
| 48 | best_bid_price | u16 | 2 | Best bid (0 if empty) |
| 50 | best_ask_price | u16 | 2 | Best ask (10000 if empty) |
| 52 | bid_count | u16 | 2 | Active bids |
| 54 | ask_count | u16 | 2 | Active asks |
| 56 | bump | u8 | 1 | PDA bump |
| 57 | _header_reserved | [u8; 7] | 7 | Reserved |
| 64 | bids | [Order; 63] | 5040 | Bid orders |
| 5104 | asks | [Order; 63] | 5040 | Ask orders |
| 10144 | _padding | [u8; 96] | 96 | Alignment padding |
| Total | 10,240 |
Order Structure (80 bytes)#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | order_id | u64 | 8 | Unique ID |
| 8 | owner | Pubkey | 32 | Order owner |
| 40 | price_bps | u16 | 2 | Canonical price |
| 42 | quantity | u64 | 8 | Remaining quantity |
| 50 | original_quantity | u64 | 8 | Original size |
| 58 | timestamp | i64 | 8 | Placement time |
| 66 | original_side | u8 | 1 | User's original side |
| 67 | is_active | bool | 1 | Active flag |
| 68 | _reserved | [u8; 12] | 12 | Reserved |
Side Encoding#
| Value | Original Side | Canonical Side |
|---|---|---|
| 0 | BuyYes | Bid |
| 1 | SellYes | Ask |
| 2 | BuyNo | Ask (converted) |
| 3 | SellNo | Bid (converted) |
Derivation#
let (orderbook_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"orderbook", market_pda.as_ref()],
&program_id,
);
VaultAccount#
SPL Token account holding market collateral.
Layout#
Standard SPL Token account (165 bytes):
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | mint | Pubkey | 32 | Token mint |
| 32 | owner | Pubkey | 32 | Owner (market PDA) |
| 64 | amount | u64 | 8 | Token balance |
| 72 | delegate | Option<Pubkey> | 36 | Delegate |
| 108 | state | AccountState | 1 | Account state |
| 109 | is_native | Option<u64> | 12 | Native flag |
| 121 | delegated_amount | u64 | 8 | Delegated |
| 129 | close_authority | Option<Pubkey> | 36 | Close auth |
| Total | 165 |
Derivation#
let (vault_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"vault", market_pda.as_ref()],
&program_id,
);
UserPositionAccount#
User's position in a specific market.
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | market | Pubkey | 32 | Parent market |
| 40 | owner | Pubkey | 32 | Position owner |
| 72 | yes_shares | u64 | 8 | YES shares owned |
| 80 | no_shares | u64 | 8 | NO shares owned |
| 88 | locked_yes_shares | u64 | 8 | Locked in sell orders |
| 96 | locked_no_shares | u64 | 8 | Locked in sell orders |
| 104 | collateral_deposited | u64 | 8 | Total deposited |
| 112 | collateral_locked | u64 | 8 | Locked in buy orders |
| 120 | payout | u64 | 8 | Settlement payout |
| 128 | total_bought | u64 | 8 | Shares bought |
| 136 | total_sold | u64 | 8 | Shares sold |
| 144 | total_fees_paid | u64 | 8 | Taker fees |
| 152 | total_rebates_earned | u64 | 8 | Maker rebates |
| 160 | first_trade_at | i64 | 8 | First trade time |
| 168 | last_trade_at | i64 | 8 | Last trade time |
| 176 | order_count | u16 | 2 | Active orders |
| 178 | settled | u8 | 1 | Settlement flag |
| 179 | bump | u8 | 1 | PDA bump |
| 180 | _padding | [u8; 4] | 4 | Alignment padding |
| 184 | yes_withdrawn | u64 | 8 | Slot-sourced YES minted |
| 192 | no_withdrawn | u64 | 8 | Slot-sourced NO minted |
| 200 | _reserved | [u8; 56] | 56 | Future use |
| Total | 256 |
Derivation#
let (position_pda, bump) = Pubkey::find_program_address(
&[
b"seesaw",
b"position",
market_pda.as_ref(),
user_pubkey.as_ref(),
],
&program_id,
);
TraderLedgerAccount#
Per-market trader balance ledger. Header followed by a dynamic slot array. The
account is pre-grown via EnsureTraderLedgerSpace before CreateMarket.
Layout#
Header (56 bytes):
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | market | Pubkey | 32 | Parent market |
| 40 | capacity | u32 | 4 | Total slot count (num_seats) |
| 44 | next_free_slot | u32 | 4 | Allocation search hint |
| 48 | occupied_count | u32 | 4 | Number of occupied slots |
| 52 | bump | u8 | 1 | PDA bump |
| 53 | version | u8 | 1 | Layout version (live ledgers = 2) |
| 54 | _reserved | [u8; 2] | 2 | Alignment padding |
Slot (72 bytes each, capacity slots immediately follow header):
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | trader | Pubkey | 32 | Trader this slot is assigned to |
| 32 | quote_free | [u8; 6] | 6 | Free settlement-token credit |
| 38 | yes_free | [u8; 6] | 6 | Free YES-share credit |
| 44 | no_free | [u8; 6] | 6 | Free NO-share credit |
| 50 | quote_locked | [u8; 6] | 6 | Collateral locked by open orders |
| 56 | yes_locked | [u8; 6] | 6 | YES locked by open ask orders |
| 62 | no_locked | [u8; 6] | 6 | NO locked by open ask orders |
| 68 | occupied | u8 | 1 | 0 = free; non-zero = assigned |
| 69 | _reserved | [u8; 3] | 3 | Alignment padding |
Total size: 56 + num_seats × 72 bytes.
Derivation#
let (ledger_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"trader_ledger", market_pda.as_ref()],
&program_id,
);
ReferrerEarningsAccount#
Per-referrer earnings ledger. Tracks claimable and lifetime-claimed amounts.
Credited on every attributed fill; drained by claim_referrer_earnings.
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | referrer | Pubkey | 32 | Referrer wallet |
| 40 | accumulated | u64 | 8 | Claimable amount |
| 48 | total_claimed | u64 | 8 | Lifetime claimed amount |
| 56 | referees_count | u32 | 4 | Reserved for off-chain use; never written on-chain |
| 60 | bump | u8 | 1 | PDA bump |
| 61 | treasury_index | u8 | 1 | Index of the referrer_treasury_k shard (Phase E); immutable after creation |
| 62 | _padding_a | [u8; 2] | 2 | Alignment padding |
| 64 | last_claim_at | i64 | 8 | Timestamp of the last ClaimReferrerEarnings call |
| Total | 72 |
Derivation#
let (earnings_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"referrer_earnings", referrer.as_ref()],
&program_id,
);
ReferralAccount#
Per-user first-touch referral attribution record. Written by set_referrer;
immutable for 365 days (REFERRAL_DURATION_SECONDS).
Layout#
| Offset | Field | Type | Size | Description |
|---|---|---|---|---|
| 0 | discriminator | [u8; 8] | 8 | Account type identifier |
| 8 | referee | Pubkey | 32 | User being attributed |
| 40 | referrer | Pubkey | 32 | Referrer wallet |
| 72 | created_at | i64 | 8 | Attribution creation timestamp |
| 80 | expires_at | i64 | 8 | Expiry timestamp (created_at + REFERRAL_DURATION_SECONDS) |
| 88 | total_earned | u64 | 8 | Reserved for off-chain reconstruction; never written on-chain |
| 96 | bump | u8 | 1 | PDA bump |
| 97 | _padding | [u8; 7] | 7 | Alignment padding |
| Total | 104 |
Derivation#
let (referral_pda, bump) = Pubkey::find_program_address(
&[b"seesaw", b"referral", user.as_ref()],
&program_id,
);
YES and NO Mints#
Each market has two SPL token mints for its YES and NO shares.
Seeds#
These are standard SPL Mint accounts (82 bytes). Their authority is the market PDA so minting/burning is program-controlled.
YES and NO Escrows#
Sell orders escrow tokens here; fills transfer from escrow to the taker's ATA.
Seeds#
Standard SPL Token accounts (165 bytes), owned by the market PDA.
Discriminators#
Each account type has a unique 8-byte discriminator:
fn discriminator(name: &str) -> [u8; 8] {
let hash = sha256(format!("account:{}", name));
hash[..8]
}
| Account | Discriminator Input |
|---|---|
| ConfigAccount | account:ConfigAccount |
| MarketAccount | account:MarketAccount |
| OrderbookAccount | account:OrderbookAccount |
| UserPositionAccount | account:UserPositionAccount |
| TraderLedgerAccount | account:TraderLedgerAccount |
| ReferrerEarningsAccount | account:ReferrerEarningsAccount |
| ReferralAccount | account:ReferralAccount |
Rent Requirements#
| Account | Size | Rent-Exempt (~) |
|---|---|---|
| ConfigAccount | 624 bytes | ~0.004 SOL |
| MarketAccount | 512 bytes | ~0.004 SOL |
| OrderbookAccount | 10,240 bytes | ~0.071 SOL |
| VaultAccount | 165 bytes | ~0.002 SOL |
| UserPositionAccount | 256 bytes | ~0.002 SOL |
| TraderLedgerAccount | 56 + num_seats × 72 | Variable (see Market Capacity) |
| ReferrerEarningsAccount | 72 bytes | ~0.001 SOL |
| ReferralAccount | 104 bytes | ~0.001 SOL |
Account Relationships#
Next Steps#
- Instructions — instruction details
- PDAs — derivation patterns