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 version | 0.1.0 |
| Source | sdk/src/ in the monorepo |
| License | MIT |
| 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-tokenThe 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 · buildExecuteTriggerOpenIxThe 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 triggerThese 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 USDCThe 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 // 1All 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 suitesThe 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
- Program Instructions Reference — every instruction’s signature.
- Account Structure (PDAs) — the shape of every account the SDK returns.
- Candle REST API / WebSocket Feed — complementary off-chain data sources.
sdk/README.md— package-level README.