🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
For DevelopersProgram Instructions Reference

Program Instructions Reference

Complete reference for all 38 base instructions exposed by the SportsPerp program. Every instruction except the three insurance-admin entries (deposit_insurance, withdraw_insurance, configure_insurance) ships with a typed builder in @sportsperp/sdk; admin keepers invoke the insurance trio directly via the IDL. Instruction indices match the on-chain Anchor discriminator ordering.

Canonical source: the program’s lib.rs instruction declarations.

Trading (user-invoked)

open_position

Opens a new long or short position.

client.openPosition({
  marketId: number,
  direction: Direction,        // Long (0) | Short (1)
  collateralAmount: BN,        // 10^6 scale
  leverage: number,            // 10^2 scale (500 = 5x, 200 = 2x)
})

Required accounts: market, position (init), liquidity_pool, pool_token_account, user_token_account, user (signer), USDC mint, token program, system program, rent.

Errors: MarketPaused, OracleStale, InvalidLeverage (exceeds market max), MarginTierViolation, BelowMinPositionSize, PositionExceedsCapacityCap, InsufficientCollateral, RiskFloorBreached (pool_token.amount < pool.risk_floor).

close_position

Closes an existing position at current mark EMA. Settles PnL, funding, and 10-bps taker fee.

client.closePosition({ marketId: number })

Accounts: market, position (close), liquidity_pool, pool_token_account, user_token_account, user (signer), token program.

Errors: NoPosition, OracleStale, MarketPaused, PoolInsufficient.

partial_close_position

Closes a fraction of an open position and proportionally releases collateral. PnL, funding, and the 10-bps taker fee are settled on the closed slice only.

client.partialClosePosition({
  marketId: number,
  closeBps: number,            // 1..=9999. For a full close use close_position
})

Errors: InvalidCloseBps (must be in [1, 9999]), OracleStale, MarketPaused, PoolInsufficient.

add_collateral

Deposits additional USDC into an open position. Resets anti-manipulation baseline.

client.addCollateral({ marketId: number, amount: BN })

withdraw_collateral

Removes USDC from an open position; fails if the post-withdraw margin ratio would be ≤ 20%.

client.withdrawCollateral({ marketId: number, amount: BN })

Errors: WithdrawalViolatesMargin, InsufficientCollateral, OracleStale.

Trigger Orders

place_trigger_order

Creates a SL, TP, or Limit Open order. For SL/TP, reduce_only must be true. For Limit Open, collateralAmount is escrowed in the global liquidity pool (earmarked to the TriggerOrder PDA), and the placement is gated by pool_token.amount ≥ pool.risk_floor. SL/TP placements have no risk_floor gate.

client.placeTriggerOrder({
  marketId: number,
  orderType: OrderType,        // StopLoss (0) | TakeProfit (1) | LimitOpen (2)
  direction: Direction,
  triggerPrice: BN,            // 10^6 scale
  size: BN,
  collateralAmount: BN,
  leverage: number,
  reduceOnly: boolean,
  expiry: BN,                  // Unix seconds, 0 = never
})

Errors: InvalidOrderType, InvalidTriggerCondition, MaxTriggerOrdersReached (10/market cap), ReduceOnlyRequiredForSLTP, OracleStale.

cancel_trigger_order

Owner cancels a pending order. For Limit Open, refunds escrowed collateral.

client.cancelTriggerOrder({ marketId: number, orderIdx: number })

execute_trigger_close / execute_trigger_open

Permissionless. Keeper fires an eligible order.

client.executeTriggerClose({ marketId, owner, orderIdx })
client.executeTriggerOpen({ marketId, owner, orderIdx })

Errors (both): TriggerConditionNotMet, OrderExpired, OrderAlreadyExecuted, PositionNotFound (for close), MarginTierViolation (for open).

Oracle & Funding

update_oracle

Pushes a new price, confidence interval, and live-match flag. Gated on the market’s oracle_authority (a dedicated hot key, separate from the upgrade/admin authority since the B5 devnet cutover); the deployed pusher runs 2-of-N multi-source consensus on devnet, with a fully independent second source the remaining mainnet step.

The program additionally enforces two on-chain update guards so a stolen hot key cannot bypass the off-chain consensus rules: a per-update deviation cap (the new price may differ from the last on-chain price by at most 20% — 2000 bps — with the allowance floored at one index point) and a minimum update interval (60 seconds between accepted updates per market). Larger legitimate moves are reached by walking in capped steps across successive updates; the deployed pusher does this automatically.

client.updateOracle({
  marketId: number,
  price: BN,                   // 10^6 scale
  confidenceBps: number,       // e.g., 300 = 3%
  isLive: boolean,
})

Errors: Unauthorized, InvalidOraclePrice, MarketSettled, OracleDeviationTooLarge, OracleUpdateTooFrequent.

The SP-007 update guards ship in the 59-error program build (June 2026); the devnet upgrade accompanies this release.

apply_funding

Permissionless. Consumes accumulated premium samples (plain mean), clamps to ±10 bps, updates the cumulative funding counters.

client.applyFunding({ marketId })

Liquidation (all permissionless)

partial_liquidate

Layer 1. Closes 20% of an underwater position. Liquidator earns 5%. 30s cooldown enforced.

client.partialLiquidate({ marketId, positionOwner })

Errors: NotLiquidatable, NotEligibleForBackstop (below Layer 1 threshold), LiquidationCooldownActive, ProfitableLiquidation (anti-manipulation).

backstop_liquidate

Layer 2. Insurance fund absorbs position at mark price.

client.backstopLiquidate({ marketId, positionOwner })

Errors: NotLiquidatable, InsuranceFundAtCapacity.

unwind_backstop

Layer 2. Gradually unwinds an absorbed position (max 10% per call). 3% reward.

client.unwindBackstop({ marketId, maxAmount })

auto_deleverage

Layer 3. Force-closes a profitable opposing position to cover an underwater one. Caller picks target.

client.autoDeleverage({ marketId, underwaterOwner, adlTargetOwner })

Errors: ADLNotRequired (insurance could have absorbed), NoOpposingPositionsForADL (target not opposing side or not profitable).

liquidate (legacy)

Pre-Spec-1 full-close liquidation. Preserved for backwards compatibility. New integrations should use the layer-specific instructions.

Market Lifecycle

initialize_market (admin)

client.initializeMarket({
  marketId, marketType, maxLeverage,
  takerFeeBps, makerFeeBps, fundingInterval, maxFundingRateBps
}, usdcMint)

pause_market / unpause_market

pause_market is gated on the market’s oracle_authority (the admin may also call it); unpause_market is admin-only.

client.pauseMarket({ marketId })
client.unpauseMarket({ marketId })

update_market_params (admin)

Adjusts oracle weights and vAMM impact factor.

client.updateMarketParams({
  marketId,
  oracleWeightLiveBps: number | null,
  oracleWeightBetweenBps: number | null,
  vammImpactFactor: number | null,
})

sunset_market (permissionless)

Triggers if current_time - oracle_timestamp >= 72h.

client.sunsetMarket({ marketId })

Errors: OracleNotStale (< 72h), AlreadySettled.

force_settle_market (admin)

Skip the 72h wait and settle at a specified price.

client.forceSettleMarket({ marketId, settlementPrice: BN })

settle_position (permissionless)

Close any remaining position in a settled market at the fixed settlement price.

client.settlePosition({ marketId, positionOwner })

Insurance Fund (admin-only except deposit)

initialize_insurance

One-time. Creates the global InsuranceFund PDA.

deposit_insurance

client.depositInsurance({ amount: BN })

withdraw_insurance

Hard-capped: cannot leave total_balance < target_balance.

client.withdrawInsurance({ amount: BN })

Errors: WithdrawalBelowTarget, InsufficientInsuranceBalance.

configure_insurance

client.configureInsurance({
  maxBackstopExposure: BN,
  targetBalance: BN,
})

Liquidity Pool Admin

initialize_pool (admin)

One-time creation of the singleton LiquidityPool PDA + pool_token SPL token account. Sets risk_floor (hard on-chain gate; below it open_position and LimitOpen are blocked), target_balance (off-chain alert threshold), and obligations_snapshot_authority (hot key for exact obligation snapshots).

client.initializePool({ riskFloor: BN, targetBalance: BN, obligationsSnapshotAuthority: PublicKey })

seed_pool (admin)

Deposits LP USDC into the global pool. Updates total_lp_deposits for accounting; pool_token.amount is the canonical balance.

client.seedPool({ amount: BN })

withdraw_pool_lp (admin)

Withdraws LP USDC from the global pool. Gated: the pool’s post-withdraw token balance must remain at or above risk_floor. Updates total_lp_withdrawals.

client.withdrawPoolLp({ amount: BN })

Errors: Unauthorized, RiskFloorBreached (pool_token.amount − amount < risk_floor), InsufficientPool.

update_obligations_snapshot (snapshot authority)

Pushes the exact off-chain global obligations value computed by the solvency probe. The public LP withdrawal gate uses this value while it is fresh (10 minutes). When the snapshot is missing or stale the gate fails closed — public LP withdrawals are temporarily rejected (ObligationsSnapshotStale) rather than falling back to a total_collateral_locked-only reserve, because without a fresh snapshot the protocol cannot bound trader winner PnL on-chain (H4). A pusher outage becomes an LP-liveness incident, not a solvency one; withdrawals resume the moment a fresh snapshot lands. Values above the pool token balance are rejected — a true obligations sum above the entire pool is already fully blocking at snapshot == pool, so larger values are pure denial-of-service ammunition (the bundled pushers clamp client-side).

client.updateObligationsSnapshot({ obligations: BN })

Errors: Unauthorized (signer is not the snapshot authority), ObligationsSnapshotExceedsPool (obligations > pool_token.amount).

Liquidity Pool (public LP)

Permissionless liquidity provision into the global pool. Shares are flat 1:1 with USDC; LPs explicitly accept asymmetric trader-profit risk.

deposit_pool_lp

Deposits USDC into the global pool as a permissionless LP and mints 1:1 LP shares to the caller’s LpAccount (created on first deposit). The beta $500k aggregate cap is enforced on-chain: a deposit that would push total outstanding LP shares past 500,000 USDC is rejected (withdrawals free cap room; the admin seed_pool mints no shares and is outside the cap).

client.depositPoolLp({ amount: BN }, userTokenAccount)

Errors: LpAmountBelowMinimum, LpAccountOwnerMismatch, LpDepositCapExceeded.

withdraw_pool_lp_user

Burns LP shares and returns USDC. Gated: requires a fresh obligations snapshot, and is permitted only while pool_token.amount − amount ≥ max(total_collateral_locked, fresh_obligations_snapshot) + risk_floor — the pool must stay solvent against open obligations plus the risk floor. If the snapshot is missing or stale the call fails closed (ObligationsSnapshotStale) — see update_obligations_snapshot above (H4).

client.withdrawPoolLpUser({ amount: BN }, userTokenAccount)

Errors: LpSharesInsufficient, ObligationsSnapshotStale (missing/stale snapshot — withdrawals temporarily paused), LpWithdrawalUnavailable (pool_token.amount − amount < max(total_collateral_locked, fresh_obligations_snapshot) + risk_floor), PoolInsufficient.

Authority management (admin)

Rotate the per-role authority keys, introduced in the B5 oracle-authority separation and extended for the SP-013 obligations snapshot key. These are admin-only (gated on the current admin — hot keys can never rotate admin), reject the zero/default key, and reject authority/admin nullification where the account stores both roles. These ship as standalone instructions/ builders rather than SportsPerpsClient methods:

import {
  buildSetOracleAuthorityIx, buildSetMarketAdminIx,
  buildSetPoolAdminIx, buildSetInsuranceAdminIx,
  buildSetObligationsSnapshotAuthorityIx,
} from "@sportsperp/sdk";
 
buildSetOracleAuthorityIx(program, marketId, newOracleAuthority, admin)  // rotate a market's hot oracle key
buildSetMarketAdminIx(program, marketId, newAdmin, admin)                // rotate a market's admin
buildSetPoolAdminIx(program, newAdmin, admin)                            // rotate the LiquidityPool admin (rejects an admin equal to the obligations-snapshot authority)
buildSetInsuranceAdminIx(program, newAdmin, admin)                       // rotate the InsuranceFund admin
buildSetObligationsSnapshotAuthorityIx(program, newAuthority, admin)     // rotate the SP-013 obligations-snapshot hot key (new authority ≠ admin)

Error code numbering

Anchor errors start at 6000 (offset 0) and increment. The full 59 codes are declared in the program’s errors.rs (6058 ObligationsSnapshotStale added in H4). Never reused — retired codes stay retired (the legacy VaultInsufficient from the per-market-vault era is kept as a tombstone).

Common patterns:

CodeNameWhen it fires
6001UnauthorizedNon-admin tried an admin instruction
6002MarketPausedTrading blocked for this market
6003OracleStaleOracle > 2h old
6004InvalidOraclePricePrice ≤ 0
6009NotLiquidatableMargin ratio > 20%
6043ProfitableLiquidationAnti-manip check rejected

(Codes above are illustrative; see the program’s errors.rs for exact numbering.)

IDL

The Anchor IDL (obv_perps.json, ~120 KB) ships in two places:

  • Bundled with the @sportsperp/sdk package — consumed by the SDK
  • Bundled with the frontend — consumed by the app

Any contributor can inspect the IDL directly or import it to hand-build custom instruction flows:

import IDL from "@sportsperp/sdk/idl/obv_perps.json";
const program = new Program(IDL as any, provider);
// program.methods.openPosition(...).accounts(...).rpc()

Further reading

  • SDK — client-level usage.
  • Account Structure — what each account the instructions read/write looks like.