🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
For DevelopersAccount Structure (PDAs)

Account Structure (PDAs)

Reference for all 7 PDA account types the program maintains. Every field is sourced from programs/obv-perps/src/state/.

1. MarketConfig

Per-market configuration + live state. One per market (68 total at launch).

Seeds: ["market", market_id: u16]

FieldTypeScalePurpose
market_idu160–19 teams, 20–99 players (sparse)
market_typeu80 = Team, 1 = Player
adminPubkeyAuthorized caller for admin instructions
max_leverageu1610²Market cap (500 = 5x, 200 = 2x)
taker_fee_bps / maker_fee_bpsu1610⁴10 bps / 5 bps at launch
funding_intervali64seconds28800 (8h)
max_funding_rate_bpsu1610⁴±10 bps per period
pausedboolEmergency halt
total_long_oi / total_short_oiu6410⁶Open interest
cumulative_funding_long / cumulative_funding_shorti6410¹²Per-unit funding counters
last_funding_timei64Unix sLast apply_funding
Oracle fields
oracle_pricei6410⁶Latest pushed index
oracle_confidence_bpsu1610⁴Uncertainty interval
oracle_timestampi64Unix sLast push time
oracle_is_liveboolWhether match data is driving updates
TWAP for funding
premium_accumulatori12810⁶Running sum of (mark − oracle) samples
premium_sample_countu32Sample count since last funding
last_premium_samplei64Unix sLast sample time
Composite mark
mark_pricei6410⁶Composite (oracle + vAMM, no EMA)
mark_price_emai6410⁶150s EMA — the canonical settlement price
last_mark_updatei64Unix sLast EMA computation
vamm_impact_factoru3210⁶Default 1000 (0.1%)
oracle_weight_live_bpsu1610⁴Default 5000 (50%)
oracle_weight_between_bpsu1610⁴Default 3000 (30%)
Layer 3 + sunset
adl_enabledboolLayer 3 permitted
settledboolPost-sunset lock flag
settlement_pricei6410⁶Fixed at sunset time
settled_ati64Unix s0 = not settled
bumpu8PDA bump

2. UserPosition

One per trader per market. Created by open_position; closed by close_position, liquidation, or settlement.

Seeds: ["position", market_config: Pubkey, owner: Pubkey]

The first key is the MarketConfig PDA, not the raw market id. Pre-derive it from ["market", market_id] and pass that pubkey when deriving the position PDA.

FieldTypeScalePurpose
ownerPubkeyPosition owner
marketPubkeyThe market PDA
directionu80 = Long, 1 = Short
sizeu6410⁶Notional size in USDC
entry_pricei6410⁶Mark EMA at open time
collateralu6410⁶USDC posted
cumulative_funding_at_entryi6410¹²Funding snapshot at open
opened_ati64Unix sOpen timestamp
last_margin_transfer_collateralu6410⁶Collateral at last margin op (anti-manipulation baseline)
last_margin_transfer_timei64Unix sLast margin op
last_partial_liquidation_timei64Unix sLast Layer 1 hit (30s cooldown)
bumpu8PDA bump

One position per (trader, market) pair is enforced by the PDA seeds. A trader who wants two positions on the same market must use two different signers (e.g., a sub-account).

3. LiquidityPool

Singleton. Global cross-market USDC custody — one PDA + one SPL token account shared by all 68 markets. Replaces the per-market VaultAccount model that earlier versions used.

Seeds: ["liquidity_pool"]

FieldTypePurpose
adminPubkeyAuthorized for seed_pool and withdraw_pool_lp
total_lp_depositsu64Lifetime LP deposits (informational; canonical balance is pool_token.amount)
total_lp_withdrawalsu64Lifetime LP withdrawals
token_mintPubkeyUSDC mint
token_accountPubkeyThe actual pool_token SPL token account holding USDC for every market
risk_flooru64Hard on-chain gate. Below it, open_position and LimitOpen revert with RiskFloorBreached. Closes / SL / TP / cancels / margin-passing collateral withdraws are not gated. Also enforced on withdraw_pool_lp
target_balanceu64Off-chain alert threshold (read by monitoring; [warning] at < 2×, [error] at < 1.25×). Not enforced on-chain
bumpu8PDA bump

The pool_token SPL token account is at seeds ["pool_token"] with token::authority = liquidity_pool (the LiquidityPool PDA). Per-market accounting (open interest, funding accumulators, mark price) still lives on MarketConfig; only capital custody is global.

The full solvency invariant pool_token.amount ≥ Σ obligations is enforced off-chain by scripts/probe-solvency.ts (60s cadence with Telegram alerts on drift). On-chain instructions do cheap local checks only — see On-Chain Program → Capital model.

4. InsuranceFund

Global insurance fund. One PDA, shared across all markets.

Seeds: ["insurance"]

FieldTypePurpose
adminPubkeyAuthorized for admin instructions
token_mintPubkeyUSDC mint
token_accountPubkeyHolds the USDC balance
total_balanceu64Current USDC (redundant with token_account.amount; kept for o11y)
max_backstop_exposureu64Cap on outstanding absorbed positions
current_backstop_exposureu64Live — decreases on unwind
total_absorbedu64Lifetime counter (monotonic)
total_unwoundu64Lifetime counter (monotonic)
target_balanceu64Withdrawal floor — admin cannot go below
bumpu8PDA bump

5. BackstopPosition

Created when Layer 2 absorbs an underwater position. Lives until fully unwound. At most one outstanding BackstopPosition per market at a time — the next absorption on the same market reuses the PDA after the previous one is fully unwound.

Seeds: ["backstop", market_id: u16]

FieldTypeScalePurpose
marketPubkeyThe MarketConfig PDA
market_idu16
directionu80 = Long, 1 = Short (mirror of original position)
sizeu6410⁶Remaining unwound size
entry_pricei6410⁶Mark price at absorption
created_ati64Unix sAbsorption time
bumpu8PDA bump

6. TriggerOrder

One per placed SL, TP, or Limit Open. Closed on execute, cancel, or expiry-triggered cleanup.

Seeds: ["trigger", market_config: Pubkey, owner: Pubkey, order_id: u64 (LE)]

The first key is the MarketConfig PDA (not the raw market id), and the order id is serialized as little-endian u64.

FieldTypeScalePurpose
ownerPubkeyOrder creator
marketPubkeyMarket PDA
market_idu16Duplicated for convenience
order_idu32User-scoped monotonic index
order_typeu80 = StopLoss, 1 = TakeProfit, 2 = LimitOpen
directionu80 = Long, 1 = Short
trigger_pricei6410⁶Fire threshold
trigger_conditionu80 = Above, 1 = Below
sizeu6410⁶Position size (LimitOpen) or close amount (SL/TP)
collateralu6410⁶Escrowed (LimitOpen only)
leverageu1610³LimitOpen only
reduce_onlyboolRequired true for SL/TP
expiryi64Unix s0 = never
created_ati64Unix sPlacement time
bumpu8PDA bump

Max 10 TriggerOrder accounts per (user, market) pair.

7. UserOrderCounter

Monotonic order ID allocator per user.

Seeds: ["order_counter", owner: Pubkey, market_config: Pubkey]

One counter per (user, market) pair — independent monotonic sequences keep order ids tightly packed within a market without colliding across markets.

FieldTypePurpose
ownerPubkeyUser
marketPubkeyThe MarketConfig PDA scope
next_order_idu64Next order_id to assign (used as the u64-LE seed of the next TriggerOrder)
bumpu8PDA bump

next_order_id never decrements — cancelled orders don’t free their slot for reuse. This guarantees no two of a user’s orders ever share a PDA.

Deriving PDAs from TypeScript

import { PROGRAM_ID } from "@sportsperp/sdk";
import { PublicKey } from "@solana/web3.js";
import { struct, u16 } from "@coral-xyz/anchor";
 
// Market
const [marketPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("market"), new BN(marketId).toArrayLike(Buffer, "le", 2)],
  PROGRAM_ID
);
 
// Position
const [positionPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("position"), marketPda.toBuffer(), user.toBuffer()],
  PROGRAM_ID
);

Or just use the SDK helpers (sdk/src/pda.ts) — they’re exactly the same derivations, with typed signatures.

Account sizes (approximate)

For rent calculations and bulk queries:

AccountApproximate size
MarketConfig~350 bytes
UserPosition~138 bytes
LiquidityPool~120 bytes
InsuranceFund~200 bytes
BackstopPosition~140 bytes
TriggerOrder~160 bytes
UserOrderCounter~50 bytes

Tip: When using getProgramAccounts with memcmp filters, match on dataSize: 138 to find UserPositions, dataSize: 350 for MarketConfigs, etc. Don’t fetch without filters — devnet will rate-limit.

Further reading