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

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_oracle adds a premium sample); funding consumes the mean across all samples accumulated since the last apply_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_oracle requires admin signature β€” but multi-source oracle (built, not deployed) relaxes this to a 2-of-N weighted median.
  • apply_funding is permissionless.
  • partial_liquidate, backstop_liquidate, unwind_backstop, auto_deleverage are permissionless.
  • execute_trigger_close, execute_trigger_open are permissionless.
  • sunset_market, settle_position are 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 build and 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):

  1. Frontend β€” user clicks β€œLong 5x”; OrderPanel calls useProgram() to build the open_position instruction via the SDK.
  2. 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).
  3. 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 with RiskFloorBreached. (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 USDC effective_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_token SPL account (one shared pool across all 68 markets β€” see On-Chain Program β†’ Liquidity Pool).
    • Creates a UserPosition PDA with the entry price set to the current mark EMA.
    • Deducts 10 bps taker fee (0.4 USDC) from collateral.
    • Updates MarketConfig.total_long_oi by the notional size.
  4. 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.
  5. 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

ConcernLayerSpecific location
Position stateOn-chainUserPosition PDA
Market parameters (fees, leverage, weights, initial_capacity)On-chainMarketConfig
Funding accumulatorsOn-chainMarketConfig.cumulative_funding_{long,short}
USDC custody (collateral, escrow, payouts)On-chainLiquidityPool (singleton) + pool_token SPL β€” one shared pool across all 68 markets
Pool risk gateOn-chainLiquidityPool.risk_floor (hard, blocks new risk only)
Pool target balanceOff-chain alert thresholdLiquidityPool.target_balance (read by monitoring/index.mjs for Telegram alerts)
Pool solvency invariant (pool β‰₯ Ξ£ obligations)Off-chain probescripts/probe-solvency.ts (60s loop, alerts on drift)
Insurance balanceOn-chainInsuranceFund (single global PDA, separate from LiquidityPool)
OBV composite indexOff-chainengine/index-calculator.ts
Data-feed credentialsOff-chain (Hetzner env)/etc/default/sportsperp-crank
Trigger order storageOn-chainTriggerOrder PDA
Trigger order pollingOff-chainkeeper/index.mjs
Candle historyOff-chainSQLite at /root/obv-perps/engine/candles.db
Real-time WebSocketOff-chainengine/ws-server.ts, port 3456
Program binaryOn-chainSolana 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-engine sidecar; 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.