Protocol Architecture β Overview
SportsPerp is split across three layers: on-chain (the Anchor program and its PDAs), off-chain (a set of permissionless services that feed and maintain the program), and frontend (a Next.js app plus the published SDK). This page is the map; the following pages drill into each layer.
The big picture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STATSBOMB DATA β
β Direct API (REST) βββ post-match canonical OBV β
β Live API (GraphQL) βββ in-match event stream β
ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β OFF-CHAIN SERVICES (Hetzner) β
β β
β βββββββββββββββββββ βββββββββββββββββ ββββββββββββββββ β
β β Oracle Crank ββββΊβ Oracle Pusher ββββΊβ update_oracleβ β
β β (engine/) β β (oracle-pusherβ β β β
β β port 3456 β β port 3458) β β β β
β ββββββββββ¬βββββββββ βββββββββββββββββ ββββββββββββββββ β
β β β
β βββΊ Candle API (REST, SQLite-backed) β
β βββΊ WebSocket feed (ticks, live events) β
β βββΊ obv-engine Python sidecar (port 8100, XGBoost) β
β β
β ββββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββββ β
β β Keeper β β Liquidator Bot β β Monitor β β
β β (triggers + β β (3-layer β β (health + β β
β β funding crank) β β cascade) β β Telegram) β β
β β port 3457 β β port 3459 β β port 3460 β β
β ββββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β ON-CHAIN PROGRAM (Solana devnet) β
β β
β Program ID: 6d4fSCD7mNy7aDNS2mXUxYpZjFFQKBKwAsM5kojKQA6h β
β β
β 30 instructions Β· 47 error codes Β· 7 PDA account types Β· 13 events β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββ β
β β MarketConfig β β UserPosition β βLiquidityPool β βInsurance β β
β β Γ68 β β (per trade) β β (singleton) β βFund (Γ1) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββ β
β β Backstop- β β TriggerOrder β β UserOrderCounter β β
β β Position β β (per order) β β (per user) β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β FRONTEND + SDK (Vercel + npm) β
β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β Next.js 16 app ββββββββ @sportsperp/sdk β β
β β Wallet adapter β β Instruction builders β β
β β TradingView charts β β PDA helpers β β
β β HTTPS proxy routes β β Math (mirrors Rust) β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββDesign principles
Three principles keep the architecture disciplined:
1. Keep the program small
The on-chain program does the minimum necessary: custody, settlement, invariant enforcement. Everything that doesnβt have to be on-chain isnβt:
- Price calculation β off-chain oracle crank.
- TWAP sample collection β on-chain (every
update_oracleadds a premium sample); funding consumes the mean across all samples accumulated since the lastapply_funding. - Trigger-order polling β off-chain keeper.
- Liquidator candidate selection β off-chain.
- Candle history β off-chain (SQLite, exposed via REST).
The result: a small compiled binary with 30 instructions β small enough to audit fully, cheap to invoke, and minimal attack surface.
2. Make every off-chain role permissionless
Every operation the off-chain services perform can be run by anyone:
update_oraclerequires admin signature β but multi-source oracle (built, not deployed) relaxes this to a 2-of-N weighted median.apply_fundingis permissionless.partial_liquidate,backstop_liquidate,unwind_backstop,auto_deleverageare permissionless.execute_trigger_close,execute_trigger_openare permissionless.sunset_market,settle_positionare permissionless.
Our Hetzner services exist to guarantee these operations happen, not to monopolize them. If SportsPerpβs services go dark, the protocol continues: any independent keeper can pick up the work.
3. Single source of truth for everything
The same data representation flows end-to-end:
- Markets are defined in
roster.jsonβ one file, file-watched, hot-reloaded by every service. - Math is defined once in Rust (
programs/obv-perps/src/math/) and mirrored bit-for-bit in TypeScript (sdk/src/math/). A PnL computed off-chain equals the on-chain computation to the last integer. - IDL is generated by
anchor buildand copied verbatim into both the SDK and the frontend β no divergent hand-maintained bindings.
How a trade flows through the stack
Concrete example β a trader opens a 5x long on market 0 (Arsenal):
- Frontend β user clicks βLong 5xβ;
OrderPanelcallsuseProgram()to build theopen_positioninstruction via the SDK. - Wallet β Phantom signs the transaction. The instruction specifies
direction=0(long),collateral_amount=100_000_000(100 USDC at 10βΆ scale),leverage=500(5x at 10Β² scale). - On-chain β the program:
- Checks the market isnβt paused, oracle isnβt stale, caller has sufficient USDC.
- Validates
pool_token.amount β₯ pool.risk_floorβ the hard gate on new risk. Below the floor, the instruction is rejected withRiskFloorBreached. (Closes, cancels, SL/TP, and reduce-only paths are unaffected.) - Validates the tier cap. The denominator is
effective_oi = max(market_total_oi, initial_capacity)β same example: 100 USDC on a 1,000 USDCeffective_oi= 10% β tier 2, max 4x β instruction rejected at 5x. Retry at 4x. - Applies any pending funding.
- Transfers 100 USDC from the traderβs associated token account to the global
pool_tokenSPL account (one shared pool across all 68 markets β see On-Chain Program β Liquidity Pool). - Creates a
UserPositionPDA with the entry price set to the current mark EMA. - Deducts 10 bps taker fee (0.4 USDC) from collateral.
- Updates
MarketConfig.total_long_oiby the notional size.
- Off-chain visibility β the crankβs WebSocket server observes the transaction signature and broadcasts a position-opened event to subscribed frontends. TradingView shows the new position.
- Ongoing β every 8 hours the keeper invokes
apply_funding; every 5 minutes the oracle crank pushes a new oracle price; the liquidator bot monitors margin ratios; if the mark drops far enough, Layer 1 partial liquidation fires.
What lives where
| Concern | Layer | Specific location |
|---|---|---|
| Position state | On-chain | UserPosition PDA |
Market parameters (fees, leverage, weights, initial_capacity) | On-chain | MarketConfig |
| Funding accumulators | On-chain | MarketConfig.cumulative_funding_{long,short} |
| USDC custody (collateral, escrow, payouts) | On-chain | LiquidityPool (singleton) + pool_token SPL β one shared pool across all 68 markets |
| Pool risk gate | On-chain | LiquidityPool.risk_floor (hard, blocks new risk only) |
| Pool target balance | Off-chain alert threshold | LiquidityPool.target_balance (read by monitoring/index.mjs for Telegram alerts) |
Pool solvency invariant (pool β₯ Ξ£ obligations) | Off-chain probe | scripts/probe-solvency.ts (60s loop, alerts on drift) |
| Insurance balance | On-chain | InsuranceFund (single global PDA, separate from LiquidityPool) |
| OBV composite index | Off-chain | engine/index-calculator.ts |
| Data-feed credentials | Off-chain (Hetzner env) | /etc/default/sportsperp-crank |
| Trigger order storage | On-chain | TriggerOrder PDA |
| Trigger order polling | Off-chain | keeper/index.mjs |
| Candle history | Off-chain | SQLite at /root/obv-perps/engine/candles.db |
| Real-time WebSocket | Off-chain | engine/ws-server.ts, port 3456 |
| Program binary | On-chain | Solana devnet cluster |
Further reading
- On-Chain Program β 30 instructions, 7 PDA types (incl. singleton
LiquidityPool), error model, deployment. - Off-Chain Services β the systemd services running on Hetzner (6 Node services + Python
obv-enginesidecar; trade-history indexer live since 2026-05-11). - Oracle Design β how upstream event data becomes an on-chain oracle price.
- Fixed-Point Math β how Rust and TypeScript stay in lockstep.