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

SDK

The @sportsperp/sdk npm package is the canonical way to integrate with the SportsPerp on-chain program from TypeScript. It mirrors every on-chain math operation bit-for-bit, exposes a high-level SportsPerpsClient wrapping 27 of the 30 program instructions (the three insurance-admin instructions — deposit_insurance, withdraw_insurance, configure_insurance — are invoked directly via the IDL), and ships PDA derivation, account fetchers, math helpers, and event log parsing.

Package@sportsperp/sdk
Current version0.1.0
Sourcesdk/src/ in the monorepo
LicenseMIT
Test coverage~73 tests across math parity, PDA derivation, devnet integration, E2E trading, liquidation builders, and update_market_params

Install

npm install @sportsperp/sdk @coral-xyz/anchor @solana/web3.js @solana/spl-token

The package peer-depends on @coral-xyz/anchor >=0.30.0 and @solana/web3.js >=1.90.0.

Quick start

import { SportsPerpsClient, Direction } from "@sportsperp/sdk";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { Wallet, BN } from "@coral-xyz/anchor";
 
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const wallet = new Wallet(Keypair.generate());
 
const client = new SportsPerpsClient({ connection, wallet });
 
// Open a 5x long on Arsenal (market 0) with 100 USDC collateral.
// `userTokenAccount` is the trader's USDC ATA — the SPL token account that
// holds the USDC the program will pull collateral from.
const txSig = await client.openPosition(
  0,                                  // marketId
  {
    direction: Direction.Long,
    collateralAmount: new BN(100_000_000),  // 100 USDC at 10⁶ scale
    leverage: 500,                          // 5x — leverage is scaled by 100
  },
  userTokenAccount,
);
 
console.log("Opened:", txSig);

The SportsPerpsClient

The high-level client in sdk/src/client.ts wraps every program instruction with a typed method, threads the Anchor Program + AnchorProvider, and derives the PDAs each instruction needs.

Method signatures are positional(marketId, args, userTokenAccount, …). Where an instruction needs only scalar arguments, those scalars are passed directly without wrapping them in an options object.

// ── Trading ───────────────────────────────────────────────────────
client.openPosition(marketId, args, userTokenAccount)
client.closePosition(marketId, userTokenAccount)
client.partialClosePosition(marketId, closeBps, userTokenAccount)  // closeBps in [1, 9999]
client.addCollateral(marketId, amount, userTokenAccount)
client.withdrawCollateral(marketId, amount, userTokenAccount)
 
// ── Trigger orders ────────────────────────────────────────────────
client.placeTriggerOrder(marketId, args, userTokenAccount)
client.cancelTriggerOrder(marketId, orderId, userTokenAccount)
client.executeTriggerClose(marketId, orderOwner, orderId, ownerTokenAccount)
client.executeTriggerOpen(marketId, orderOwner, orderId, ownerTokenAccount)
 
// ── Permissionless cranks ─────────────────────────────────────────
client.applyFunding(marketId)
client.partialLiquidate(marketId, positionOwner, liquidatorTokenAccount)
client.backstopLiquidate(marketId, positionOwner, liquidatorTokenAccount)
client.unwindBackstop(marketId, maxAmount)
client.autoDeleverage(marketId, underwaterOwner, adlTargetOwner, adlTargetTokenAccount)
client.liquidate(marketId, positionOwner, liquidatorTokenAccount)   // legacy single-shot
 
// ── Market lifecycle ──────────────────────────────────────────────
client.sunsetMarket(marketId)                                       // permissionless after 72h stale
client.forceSettleMarket(marketId, settlementPrice)                 // admin
client.settlePosition(marketId, positionOwner, ownerTokenAccount)   // permissionless post-settle
 
// ── Admin ─────────────────────────────────────────────────────────
client.initializeMarket(args)
client.initializePool(args, usdcMint)
client.initializeInsurance(usdcMint)
client.seedPool(amount, adminTokenAccount)
client.withdrawPoolLp(amount, adminTokenAccount)
client.updateOracle(marketId, args)
client.updateMarketParams(marketId, args)                           // null = leave field unchanged
client.pauseMarket(marketId)
client.unpauseMarket(marketId)

See Program Instructions Reference for every method’s args shape and the error codes it can return.

Instruction builders (no client)

If you want to build transactions yourself — multi-instruction bundles, transaction sponsoring, custom signing — the raw instruction builders live in sdk/src/instructions/. The package top-level re-exports the common path:

import { buildOpenPositionIx } from "@sportsperp/sdk";
 
const ix = await buildOpenPositionIx(
  client.program,
  marketId,
  {
    direction: Direction.Long,
    collateralAmount: new BN(100_000_000),
    leverage: 500,
  },
  wallet.publicKey,
  userTokenAccount,
);
 
// Compose with other instructions, sign, send
const tx = new Transaction().add(priorityFeeIx, computeUnitIx, ix);

Top-level re-exports cover the 23 most common instructions:

buildInitializeMarketIx · buildInitializeInsuranceIx · buildInitializePoolIx · buildSeedPoolIx ·
buildWithdrawPoolLpIx · buildUpdateOracleIx · buildUpdateMarketParamsIx · buildPauseMarketIx ·
buildUnpauseMarketIx · buildOpenPositionIx · buildClosePositionIx · buildAddCollateralIx ·
buildWithdrawCollateralIx · buildApplyFundingIx · buildLiquidateIx · buildPartialLiquidateIx ·
buildBackstopLiquidateIx · buildUnwindBackstopIx · buildAutoDeleverageIx · buildPlaceTriggerOrderIx ·
buildCancelTriggerOrderIx · buildExecuteTriggerCloseIx · buildExecuteTriggerOpenIx

The settlement / partial-close builders (buildPartialClosePositionIx, buildSunsetMarketIx, buildForceSettleMarketIx, buildSettlePositionIx) live in sdk/src/instructions/ but are typically used through the client wrapper. To invoke the three insurance-admin instructions, build the instruction directly from the IDL via program.methods.depositInsurance(amount).accounts(…).instruction().

Account fetchers

The client exposes typed getters:

const market = await client.getMarket(marketId);          // MarketConfig
const allMarkets = await client.getAllMarkets();          // MarketConfig[] — all 68
const position = await client.getPosition(marketId);      // UserPosition (defaults to wallet)
const positions = await client.getAllUserPositions();     // UserPosition[] for the wallet
const allMarketPositions = await client.getAllMarketPositions(marketId);
const pool = await client.getLiquidityPool();             // singleton LiquidityPool
const insurance = await client.getInsuranceFund();        // singleton InsuranceFund
const triggers = await client.getAllTriggerOrders();      // wallet's trigger orders
const everyTrigger = await client.getEveryTriggerOrder(); // all keeper-visible trigger orders
const backstops = await client.getAllBackstopPositions(); // active backstop positions
const nextId = await client.getNextOrderId(marketId);     // for placing the next trigger

These delegate to the top-level fetchers exported from sdk/src/accounts/fetchMarketConfig, fetchAllMarkets, fetchPosition, fetchAllUserPositions, fetchAllMarketPositions, fetchAllPositions, fetchLiquidityPool, fetchInsuranceFund, fetchAllUserTriggerOrders, fetchAllMarketTriggerOrders, fetchAllTriggerOrders, fetchAllBackstopPositions, fetchOrderCounter, getNextOrderId, positionExists — which use getProgramAccounts with memcmp filters under the hood.

Never call getProgramAccounts without filters against devnet RPC — you will be rate-limited.

Math helpers

Every on-chain math operation has an equivalent TypeScript function in sdk/src/math/. These mirror the Rust implementation bit-for-bit. The client also exposes the most common ones as instance methods:

import { calculatePnl, marginRatioBps, isLiquidatable, liquidationPrice, Direction } from "@sportsperp/sdk";
 
// PnL on an open long (positional args — direction, size, entryPrice, currentPrice)
const pnl = calculatePnl(Direction.Long, position.size, position.entryPrice, market.markPriceEma);
 
// Margin health (collateral, pnl, size → bps as number)
const ratioBps = marginRatioBps(position.collateral, pnl, position.size);
const liq = ratioBps <= 2000;   // 20% — MAINTENANCE_MARGIN_BPS
 
// Or use the client wrappers
const pnl2 = client.calculatePnl(position, market.markPriceEma);
const ratio2 = client.marginRatioBps(position, market.markPriceEma);
const willLiquidate = client.isLiquidatable(position, market.markPriceEma);
const liqPrice = client.liquidationPrice(position);

Funding, solvency, and obligation helpers are also exported: calculateFundingRate, calculateFundingPayment, cumulativeFundingDelta, positionObligation, triggerOrderObligation, backstopObligation, computeGlobalObligations, positionObligationFromAccounts, computeGlobalObligationsFromAccounts, plus calculateFee and effectiveCollateral.

Rule: if a value is computed on-chain, use the SDK math helper — not a hand-rolled calculation. The helpers use BN (arbitrary-precision integers), match the Rust multiply-first-divide-last order, and are covered by parity tests against the on-chain program.

PDA derivation

import {
  getMarketConfigPda, getPositionPda, getLiquidityPoolPda, getPoolTokenPda,
  getInsurancePda, getInsuranceTokenPda, getBackstopPda,
  getOrderCounterPda, getTriggerOrderPda, getAllPdas,
} from "@sportsperp/sdk";
 
const [marketConfig, marketBump] = getMarketConfigPda(marketId);
const [position, positionBump]   = getPositionPda(marketConfig, userPubkey);
const [trigger, triggerBump]     = getTriggerOrderPda(marketConfig, userPubkey, orderId);
const [pool, poolBump]           = getLiquidityPoolPda();
const [poolToken, ptBump]        = getPoolTokenPda();    // the SPL token account holding pool USDC

The client mirrors the most common helpers as client.getMarketPda(...), client.getPositionPda(...), client.getLiquidityPoolPda(), client.getPoolTokenPda(), and client.getInsurancePda(). All seed shapes match the program’s #[account] constraints exactly — don’t hand-derive.

Note: there is no getVaultPda — per-market vaults were removed pre-mainnet in favor of the singleton LiquidityPool. Use getLiquidityPoolPda / getPoolTokenPda instead.

Event parsing

Every significant state change on-chain emits an Anchor event. The SDK ships log helpers; for fully typed decoding, use Anchor’s EventParser directly against the bundled IDL:

import { parseTransactionLogs, extractEventsFromMeta } from "@sportsperp/sdk";
 
// Lightweight: capture "Program data: ..." and "Program log: ..." lines
const tx = await connection.getTransaction(sig, { commitment: "confirmed" });
const lines = extractEventsFromMeta(tx?.meta ?? null);
// → [{ type: "ProgramData", raw: "<base64>" }, { type: "ProgramLog", raw: "Instruction: ..." }, ...]
 
// Fully typed: use Anchor's EventParser + BorshCoder against the SDK's IDL
import * as anchor from "@coral-xyz/anchor";
const eventParser = new anchor.EventParser(client.program.programId, client.program.coder);
for (const event of eventParser.parseLogs(tx?.meta?.logMessages ?? [])) {
  // event.name: "PositionOpened" | "PositionClosed" | "OraclePriceUpdated" | ...
  // event.data: typed payload matching programs/obv-perps/src/events.rs
}

The trade-history indexer (indexer/event-decoder.ts) uses exactly this EventParser + BorshCoder pattern against sdk/idl/obv_perps.json.

Constants

Exported from sdk/src/types/:

import {
  PROGRAM_ID, PRICE_SCALE, FUNDING_SCALE, BPS_DENOMINATOR,
  MAINTENANCE_MARGIN_BPS, LIQUIDATOR_REWARD_BPS, INSURANCE_ALLOCATION_BPS,
  EIGHT_HOURS,
  MarketType, Direction, TriggerOrderType, TriggerCondition,
} from "@sportsperp/sdk";
 
PROGRAM_ID                  // PublicKey: 6d4fSCD7mNy7aDNS2mXUxYpZjFFQKBKwAsM5kojKQA6h (devnet)
PRICE_SCALE                 // 1_000_000           — 10⁶ (prices)
FUNDING_SCALE               // 1_000_000_000_000   — 10¹² (cumulative funding)
BPS_DENOMINATOR             // 10_000              — 100.00%
MAINTENANCE_MARGIN_BPS      // 2000                — 20% maintenance margin (Layer 1 threshold)
LIQUIDATOR_REWARD_BPS       // 500                 — 5% Layer 1 reward
INSURANCE_ALLOCATION_BPS    // 5000                — 50% of liquidation proceeds to insurance
EIGHT_HOURS                 // 28_800              — funding interval (seconds)
 
MarketType.Team             // 0
MarketType.Player           // 1
Direction.Long              // 0
Direction.Short             // 1
TriggerOrderType.StopLoss   // 0
TriggerOrderType.TakeProfit // 1
TriggerOrderType.LimitOpen  // 2
TriggerCondition.Above      // 0
TriggerCondition.Below      // 1

All numeric constants are plain numbers, not bigints. PDA seed strings ("market", "position", "liquidity_pool", …) are also exported as MARKET_SEED, POSITION_SEED, LIQUIDITY_POOL_SEED, etc.

Args shapes

The most common typed args (full set in sdk/src/types/):

interface OpenPositionArgs {
  direction: number;          // Direction enum value
  collateralAmount: BN;       // 10⁶ scale (USDC)
  leverage: number;           // scaled by 100; 500 = 5x
}
 
interface PlaceTriggerOrderArgs {
  orderType: number;          // TriggerOrderType enum
  direction: number;          // Direction enum (the position direction, not the trigger comparison)
  triggerPrice: BN;           // 10⁶ scale
  size: BN;                   // 10⁶ scale (reduce-only for SL/TP; notional for LimitOpen)
  collateralAmount: BN;       // 10⁶ scale (LimitOpen escrow; 0 for SL/TP)
  leverage: number;           // scaled by 100
  reduceOnly: boolean;
  expiry: BN;                 // unix seconds; 0 = no expiry
}
 
interface UpdateOracleArgs {
  price: BN;                  // 10⁶ scale
  confidenceBps: number;
  isLive: boolean;
}
 
interface UpdateMarketParamsArgs {
  oracleWeightLiveBps:    number | null;   // null = leave unchanged
  oracleWeightBetweenBps: number | null;
  vammImpactFactor:       number | null;
  adlEnabled:             boolean | null;
}
 
interface InitializePoolArgs {
  riskFloor:     BN;          // 10⁶ scale (USDC) — gate for open_position + LP withdrawal
  targetBalance: BN;          // 10⁶ scale (USDC) — informational
}

Testing against devnet

cd sdk
npm test                        # all Vitest suites

The math + PDA tests are pure and run anywhere. The devnet-integration, e2e-trading, liquidation-builders, and update-market-params suites hit live devnet and require:

  • a funded admin/upgrade-authority wallet at ~/.config/solana/id.json
  • at least 0.5 SOL for rent + fees
  • the devnet USDC ATA seeded for the test wallet

Frontend wiring

The app/ Next.js frontend consumes the same SDK. See app/src/hooks/useProgram.ts for the standard pattern:

export function useProgram() {
  const wallet = useAnchorWallet();
  const connection = useMemo(() => new Connection(RPC_URL), []);
  const client = useMemo(
    () => (wallet ? new SportsPerpsClient({ connection, wallet }) : null),
    [connection, wallet],
  );
  return client;
}

Each data concern lives in its own hook: useMarkets, usePositions, usePool (singleton liquidity pool — the former useVault is gone), useInsurance, useTriggerOrders, useCandles, useRealtimeCandles, useFixtures, useTradeHistory, useRecentMarkets.

Further reading