Constants#
Protocol constants and configuration values for the Seesaw protocol.
Overview#
Time Constants#
Market Duration#
Market durations are configurable per-market, from 60 seconds to 7 days:
const MIN_DURATION_SECONDS = 60; // 1 minute
const MAX_DURATION_SECONDS = 604_800; // 7 days
const DEFAULT_DURATION_SECONDS = 900; // 15 minutes
Market Timing#
| Constant | Value | Description |
|---|---|---|
MIN_DURATION | 60 seconds | Minimum market duration |
MAX_DURATION | 604,800 seconds | Maximum market duration (7 days) |
DEFAULT_DURATION | 900 seconds | Default market duration (15 min) |
PRE_CREATE_WINDOW | 86,400 seconds | Markets can be created up to 24 hrs before epoch start |
EXPIRY_WINDOW (MARKET_EXPIRY_WINDOW) | 604,800 seconds | Default market expiration window after epoch end before ExpireMarket; admin-tunable up to 30 days via UpdateOperationalParams |
CLOSE_TIMEOUT_SECONDS | 604,800 seconds | 7 days after resolution before CloseMarket is eligible |
REFERRAL_DURATION_SECONDS | 31,536,000 s | 365-day referral lifetime (first-touch attribution) |
FEE_CONFIG_RATE_LIMIT_SECONDS | 3,600 seconds | Min seconds between UpdateFeeConfig calls |
TREASURY_UPDATE_RATE_LIMIT_SECONDS | 3,600 seconds | Min seconds between UpdateTreasuryRecipients calls |
OPERATIONAL_PARAMS_RATE_LIMIT_SECONDS | 3,600 seconds | Min seconds between UpdateOperationalParams calls |
ORACLE_TIMELOCK_SECONDS | 172,800 seconds | Retired Pyth program-ID governance timelock; compatibility instructions now reject with OracleProgramIdFrozen |
Worked examples: market ID and timing#
// Market ID from timestamp and duration
function getMarketId(timestamp: number, durationSeconds: number): bigint {
return BigInt(Math.floor(timestamp / durationSeconds));
}
// Market boundaries
function getMarketTimes(
marketId: bigint,
durationSeconds: number
): { tStart: number; tEnd: number } {
const tStart = Number(marketId) * durationSeconds;
const tEnd = tStart + durationSeconds;
return { tStart, tEnd };
}
// Current market ID (for a 15-minute market)
const now = Math.floor(Date.now() / 1000);
const currentMarketId = getMarketId(now, 900); // e.g., 1892160n
Price Constants#
Basis Points#
const PRICE_SCALE = 10_000; // 100% = 10,000 bps
const MIN_PRICE_BPS = 1; // 0.01%
const MAX_PRICE_BPS = 9_999; // 99.99%
const DEFAULT_TICK_SIZE_BPS = 100; // 1%
Price Calculations#
| Constant | Value | Description |
|---|---|---|
PRICE_SCALE | 10,000 | 100% in basis points |
MIN_PRICE_BPS | 1 | Minimum valid price (0.01%) |
MAX_PRICE_BPS | 9,999 | Maximum valid price (99.99%) |
DEFAULT_TICK_SIZE_BPS | 100 | Default tick (1%) |
MAX_TICK_SIZE_BPS | 1,000 | Maximum tick size (10%) |
MAKER_BAND_BPS | 1,000 | Max maker order distance from mid (±10%) |
STATIC_MIN_BPS_FOR_BAND | 100 | Static lower bound on resting maker price (1%) |
STATIC_MAX_BPS_FOR_BAND | 9,900 | Static upper bound on resting maker price (99%) |
DEFAULT_MIN_RESTING_NOTIONAL | 2 | Minimum notional for a resting order remainder (base units) |
Converting Prices#
// Basis points to percentage
function bpsToPercent(bps: number): number {
return bps / 100; // 6000 bps = 60%
}
// Percentage to basis points
function percentToBps(percent: number): number {
return percent * 100; // 60% = 6000 bps
}
// Basis points to decimal
function bpsToDecimal(bps: number): number {
return bps / PRICE_SCALE; // 6000 bps = 0.60
}
// NO price from YES price
function noFromYes(yesPriceBps: number): number {
return PRICE_SCALE - yesPriceBps; // 6000 -> 4000
}
Tick Rounding#
const TICK_SIZE = 100; // 1%
// Round bid down
function roundBid(priceBps: number): number {
return Math.floor(priceBps / TICK_SIZE) * TICK_SIZE;
}
// Round ask up
function roundAsk(priceBps: number): number {
const remainder = priceBps % TICK_SIZE;
return remainder === 0 ? priceBps : priceBps + (TICK_SIZE - remainder);
}
// Examples
roundBid(6050); // 6000
roundAsk(6050); // 6100
Order Book Limits#
Capacity#
const DEEP_ORDERBOOK_CAPACITY_TIERS = [64, 128, 256, 512, 1024, 2048, 4096];
const MAX_FILLS_PER_TX = 10; // Compute budget limit
const ORDER_SIZE_BYTES = 80; // Per order storage
| Constant | Value | Description |
|---|---|---|
DEEP_ORDERBOOK_CAPACITY_TIERS | 64, 128, 256, 512, 1024, 2048, 4096 | Deep RBT resting-order capacity tiers |
MAX_FILLS_PER_TX | 10 | Maximum fills per transaction |
MIN_ORDER_QUANTITY | 1 | Minimum order quantity |
MAX_ORDER_QUANTITY | 2^64-1 | Maximum order quantity |
Order ID#
// Order IDs are monotonically increasing per orderbook
const ORDER_ID_BITS = 64;
const MAX_ORDER_ID = BigInt(2) ** BigInt(64) - BigInt(1);
Fee Constants#
Fee Curve Parameters#
Seesaw uses a capped-linear-decay taker fee curve:
fee_bps(p) = min(fee_cap_bps, decay_rate_bps × (10_000 − p) / 10_000)
const FEE_CAP_BPS = 200; // 2.00% — maximum taker fee
const DECAY_RATE_BPS = 600; // 6.00% — curve slope
const MAX_FEE_CAP_BPS = 500; // Admin ceiling: fee_cap_bps <= 500
const FEE_CONFIG_RATE_LIMIT_SECONDS = 3_600; // UpdateFeeConfig cooldown
| Constant | Value | Description |
|---|---|---|
FEE_CAP_BPS | 200 | Fee curve ceiling (2.00% of notional) |
DECAY_RATE_BPS | 600 | Decay slope: fee → 0 as price → 1.00 |
MAX_FEE_CAP_BPS | 500 | Admin-enforced upper bound on fee_cap_bps |
The config authority updates the fee cap, decay rate, and split atomically with
UpdateFeeConfig (0x1F). Updates are rate-limited to one per hour and remain
bounded by MAX_FEE_CAP_BPS = 500 on-chain.
fee_cap_bps = 0 is a supported fee-holiday configuration: it makes the
capped-linear curve collect zero taker fee while exercising the same order,
fill, and settlement paths. See Launch Controls
for the launch ramp policy.
Three-Way Fee Split#
Every taker fee is split three ways (as bps of the fee, summing to 10_000):
const PROTOCOL_FEE_BPS = 5_000; // 50% of taker fee → protocol treasury
const DEFAULT_CREATOR_FEE_BPS = 1_000; // 10% of taker fee → market creator
const REFERRAL_SHARE_BPS_OF_FEE = 4_000; // 40% of taker fee → referrer (or protocol if none)
| Constant | Value | Description |
|---|---|---|
PROTOCOL_FEE_BPS | 5_000 | Protocol share (50% of taker fee) |
DEFAULT_CREATOR_FEE_BPS | 1_000 | Creator share (10% of taker fee) |
REFERRAL_SHARE_BPS_OF_FEE | 4_000 | Referral share (40% of taker fee) |
The three shares MUST sum to exactly 10_000 bps of the fee. Makers pay zero.
Referral Lifetime#
const REFERRAL_LIFETIME_SECONDS = 365 * 86_400; // 365 days (first-touch attribution)
Fee Calculations#
// Capped-linear-decay taker fee
function calculateTakerFee(
notional: bigint,
priceBps: number,
feeCapBps: number = FEE_CAP_BPS,
decayRateBps: number = DECAY_RATE_BPS
): bigint {
// Raw curve value in bps of notional
const raw = (BigInt(decayRateBps) * BigInt(10_000 - priceBps)) / BigInt(10_000);
const feeBps = raw < BigInt(feeCapBps) ? raw : BigInt(feeCapBps);
// Round UP (protocol-favorable)
return (notional * feeBps + BigInt(9_999)) / BigInt(10_000);
}
// Example: 1000 USDT notional at p = 0.50
const notional = 1000_000_000n; // 1000 USDT (6 decimals)
const priceBps = 5000;
const takerFee = calculateTakerFee(notional, priceBps);
// → fee_bps = min(200, 600 × 5000 / 10000) = 200 (capped) → 20_000_000 (20 USDT)
// Protocol receives: 10 USDT (50% of fee)
// Creator receives: 2 USDT (10% of fee)
// Referral receives: 8 USDT (40% of fee) — to referrer if set, else protocol
// Example: 1000 USDT notional at p = 0.95
const takerFee95 = calculateTakerFee(notional, 9500);
// → fee_bps = min(200, 600 × 500 / 10000) = 30 → 3_000_000 (3 USDT)
Crank Rewards#
const DEFAULT_CLOSER_REWARD_LAMPORTS = 200_000; // 0.0002 SOL
const CLOSER_REWARDS_PER_MARKET = 5; // Rewards reserved per market lifecycle
Closer rewards are paid from the market creator's deposited SOL when lifecycle operations are cranked.
| Operation | Reward |
|---|---|
snapshot_end | 0.0002 SOL |
resolve_market | 0.0002 SOL |
See Market Economics for the per-market rent-float and closer-budget model used to size the first-party launch catalog.
Account Sizes#
Storage Requirements#
const MARKET_ACCOUNT_SIZE = 512;
const DEEP_ORDERBOOK_ACCOUNT_SIZE = deepOrderbookAccountSize(capacity);
const POSITION_ACCOUNT_SIZE = 256;
const CONFIG_ACCOUNT_SIZE = 656; // Predeployment pauser layout
| Account | Size (bytes) | Description |
|---|---|---|
MarketAccount | 512 | Market state |
DeepOrderbookAccount | 192 + capacity × 224 | Deep RBT orderbook; capacity tier 64-4096 |
PositionAccount | 256 | User position |
ConfigAccount | 656 | Protocol config (predeployment pauser layout) |
TraderLedgerAccount | 56 + num_seats × 72 | Per-market trader balance ledger |
ReferrerEarningsAccount | 72 | Per-referrer earnings ledger |
ReferralAccount | 104 | Per-user referral attribution |
Rent Costs#
// Approximate rent-exempt minimums (varies by Solana runtime)
const MARKET_RENT_LAMPORTS = 2_600_000; // ~0.0026 SOL
const ORDERBOOK_RENT_LAMPORTS = 57_000_000; // ~0.057 SOL
const POSITION_RENT_LAMPORTS = 1_900_000; // ~0.0019 SOL
PDA Seeds#
Seed Constants#
const SEED_PREFIX = 'seesaw';
const SEED_CONFIG = 'config';
const SEED_MARKET = 'market';
const SEED_ORDERBOOK = 'orderbook';
const SEED_VAULT = 'vault';
const SEED_POSITION = 'position';
PDA Derivation#
import { PublicKey } from '@solana/web3.js';
// Config PDA
function deriveConfigPda(programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from(SEED_PREFIX), Buffer.from(SEED_CONFIG)],
programId
);
}
// Market PDA (includes feed, duration, market ID, and creator)
function deriveMarketPda(
pythFeedId: Uint8Array, // 32-byte feed ID
durationSeconds: bigint,
marketId: bigint,
creatorPubkey: PublicKey,
programId: PublicKey
): [PublicKey, number] {
const durationBuffer = Buffer.alloc(8);
durationBuffer.writeBigUInt64LE(durationSeconds);
const marketIdBuffer = Buffer.alloc(8);
marketIdBuffer.writeBigUInt64LE(marketId);
return PublicKey.findProgramAddressSync(
[
Buffer.from(SEED_PREFIX),
Buffer.from(SEED_MARKET),
pythFeedId,
durationBuffer,
marketIdBuffer,
creatorPubkey.toBuffer(),
],
programId
);
}
// Orderbook PDA
function deriveOrderbookPda(marketPda: PublicKey, programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from(SEED_PREFIX), Buffer.from(SEED_ORDERBOOK), marketPda.toBuffer()],
programId
);
}
// Vault PDA
function deriveVaultPda(marketPda: PublicKey, programId: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from(SEED_PREFIX), Buffer.from(SEED_VAULT), marketPda.toBuffer()],
programId
);
}
// Position PDA
function derivePositionPda(
marketPda: PublicKey,
userPubkey: PublicKey,
programId: PublicKey
): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[
Buffer.from(SEED_PREFIX),
Buffer.from(SEED_POSITION),
marketPda.toBuffer(),
userPubkey.toBuffer(),
],
programId
);
}
Instruction Discriminators#
Discriminator Values#
const DISCRIMINATORS = {
INITIALIZE_CONFIG: 0x00,
UPDATE_AUTHORITY: 0x01,
CLAIM_AUTHORITY: 0x02,
CREATE_MARKET: 0x03,
SNAPSHOT_END: 0x04,
RESOLVE_MARKET: 0x05,
EXPIRE_MARKET: 0x06,
MINT_SHARES: 0x07,
DEPOSIT_FUNDS: 0x08,
WITHDRAW_FUNDS: 0x09,
WITHDRAW_SHARES: 0x0a,
PLACE_ORDER: 0x0b,
PLACE_MULTIPLE_POST_ONLY_ORDERS: 0x0c,
SWAP_WITH_FREE_FUNDS: 0x0d,
PLACE_LIMIT_ORDER_WITH_FREE_FUNDS: 0x0e,
PLACE_MULTIPLE_POST_ONLY_ORDERS_WITH_FREE_FUNDS: 0x0f,
CANCEL_ORDER: 0x10,
CANCEL_MULTIPLE_ORDERS_BY_ID: 0x11,
CANCEL_ALL_ORDERS: 0x12,
CANCEL_UP_TO: 0x13,
REDUCE_ORDER: 0x14,
CANCEL_MULTIPLE_ORDERS_BY_ID_WITH_FREE_FUNDS: 0x15,
CANCEL_ALL_ORDERS_WITH_FREE_FUNDS: 0x16,
CANCEL_UP_TO_WITH_FREE_FUNDS: 0x17,
REDUCE_ORDER_WITH_FREE_FUNDS: 0x18,
RECLAIM_EXPIRED_ORDER: 0x19,
REDEEM: 0x1a,
FORCE_CLOSE: 0x1b,
MARK_POSITION_SETTLED: 0x1c,
CLOSE_POSITION: 0x1d,
CLOSE_MARKET: 0x1e,
UPDATE_FEE_CONFIG: 0x1f,
UPDATE_TREASURY_RECIPIENTS: 0x20,
SET_REFERRER: 0x21,
INIT_REFERRER_EARNINGS_ACCOUNT: 0x22,
CLAIM_CREATOR_FEES: 0x23,
CLAIM_REFERRER_EARNINGS: 0x24,
PAUSE: 0x25,
UNPAUSE: 0x26,
ENABLE_POST_ONLY_MODE: 0x27,
DISABLE_POST_ONLY_MODE: 0x28,
UPDATE_TICK_SIZE: 0x29,
UPDATE_MIN_RESTING_NOTIONAL: 0x2a,
UPDATE_MARKET_CAP: 0x2b,
SET_MARKET_EMERGENCY_STATUS: 0x2c,
FORCE_CANCEL_MARKET_ORDERS: 0x2d,
ENSURE_TRADER_LEDGER_SPACE: 0x2e,
UPDATE_OPERATIONAL_PARAMS: 0x2f,
PROPOSE_PYTH_PROGRAM_ID: 0x30,
APPLY_PYTH_PROGRAM_ID: 0x31,
UPDATE_MARKET_DEFAULTS: 0x32,
ENSURE_DEEP_ORDERBOOK_SPACE: 0x33,
SET_PAUSER: 0x34,
TOP_UP_CLOSER_REWARDS: 0x35,
LOG: 0xff,
} as const;
Public instructions are dense across 0x00..=0x35 (54 instructions). LOG = 0xFF
is the internal binary recorder self-CPI tag.
Oracle Constants#
Pyth Configuration#
// Pyth sponsored price-feed program ID (mainnet, upgraded Pyth Core)
const PYTH_PROGRAM_ID = new PublicKey('pyt2F414BA6dPttK6RddPZUdHfapoBN24GL5wbrPCou');
// Common feed IDs
const PYTH_FEEDS = {
BTC_USD: 'e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43',
ETH_USD: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
SOL_USD: 'ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d',
};
// Confidence ratio (optional gating)
const MAX_CONFIDENCE_RATIO = 100; // 1% = conf/price <= 0.01
Compute Budget#
CU Estimates#
const COMPUTE_ESTIMATES = {
PLACE_ORDER_NO_MATCH: 15_000,
PLACE_ORDER_ONE_FILL: 25_000,
PLACE_ORDER_MAX_FILLS: 60_000,
CANCEL_ORDER: 10_000,
REDEEM: 20_000,
RESOLVE_MARKET: 30_000,
};
// Recommended CU limits
const RECOMMENDED_CU = {
SIMPLE_OPERATION: 100_000,
COMPLEX_OPERATION: 200_000,
MAX_FILLS_OPERATION: 400_000,
};
Network Constants#
RPC Endpoints#
// Public endpoints
const RPC_ENDPOINTS = {
MAINNET: 'https://api.mainnet-beta.solana.com',
DEVNET: 'https://api.devnet.solana.com',
};
// WebSocket endpoints
const WS_ENDPOINTS = {
MAINNET: 'wss://api.mainnet-beta.solana.com',
DEVNET: 'wss://api.devnet.solana.com',
};
Quick Reference#
Next Steps#
- Review Error Codes for troubleshooting
- See Glossary for terminology