đźš§ SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
For DevelopersCandle REST API

Candle REST API

The candle REST API exposes historical OHLC price bars, market roster metadata, live match state, and fixture schedules. It is the easiest entry point for building charts, dashboards, or backtesting tools.

Host (devnet, raw)http://178.104.120.151:3456
HTTPS (Caddy reverse proxy)https://api.sportsperp.xyz
HTTPS (Next.js proxy, browser clients)https://app.sportsperp.xyz/api/candles/…
Sourceengine/candle-api.ts
StorageSQLite (engine/candle-store.ts)
CORSWide-open (Access-Control-Allow-Origin: *, GET only)

HTTPS / mixed-content note

The raw service serves HTTP only. Browser clients running on HTTPS cannot call http://178.104.120.151:3456 directly — the browser will block it as mixed content. Two HTTPS-safe paths exist:

  • Server-side / Node.js / cURL: hit the raw host on port 3456, or https://api.sportsperp.xyz for TLS.
  • Browser: route through the Next.js API proxies in app/src/app/api/ — /api/candles/[marketKey], /api/roster, /api/fixtures. The proxies’ upstream is configured via CANDLE_BACKEND_URL (defaults to https://api.sportsperp.xyz).

Endpoints

GET /api/candles/:marketKey

Returns OHLC bars for a single market.

Path parameters:

ParamExampleFormat
marketKeyteam-arsenal, player-sakaSee “Market keys” below

Query parameters:

ParamDefaultRange
timeframe1H1m · 1H · 4H · 1D
limit200Clamped to min(limit, 1000)

Example:

curl "http://178.104.120.151:3456/api/candles/team-arsenal?timeframe=1H&limit=50"

Response — a raw array of OHLC bars (no envelope, no volume):

[
  { "time": 1745280000, "open": 598.4, "high": 604.1, "low": 597.9, "close": 602.3 },
  { "time": 1745283600, "open": 602.3, "high": 605.0, "low": 601.7, "close": 604.5 }
]

time is Unix seconds at bar start. The store records 1m bars (one per crank tick) and aggregates higher timeframes on read. Trading volume is not yet tracked server-side and is not part of the response.

Invalid timeframe returns HTTP 400 with { "error": "Invalid timeframe. Use: 1m, 1H, 4H, 1D" }.

GET /api/candles/stats

High-level stats about the candle store.

{
  "totalCandles": 184302,
  "markets": 68,
  "oldestTimestamp": 1733000000,
  "newestTimestamp": 1745400000
}

oldestTimestamp / newestTimestamp are Unix seconds and may be null if the store is empty. Useful for health checks and capacity planning.

GET /api/roster

Full roster.json contents — the single source of truth for market metadata.

{
  "roster_version": 4,
  "competition_id": 2,
  "season_id": 281,
  "teams": [
    { "market_id": 0, "name": "Arsenal", "short_name": "ARS", "source_team_id": 1, "...": "..." }
  ],
  "players": [
    { "market_id": 20, "name": "Bukayo Saka", "team_id": 1, "position_group": "Forward", "sb_player_id": 39461, "...": "..." }
  ]
}

v4 adds sb_player_id on every player entry to harden market ↔ StatsBomb identity across transfers and roster rotations. Clients should fetch this at startup or on a long interval — the roster changes weekly, not per-second. The engine itself hot-reloads via fs.watch, so the served data is always current.

GET /api/fixtures

Next upcoming or in-progress match per team market ID.

{
  "fixtures": {
    "0": { "kickoff": 1745452800, "opponent": "Chelsea", "home": true },
    "1": { "kickoff": 1745539200, "opponent": "Manchester City", "home": false }
  }
}

Keys are team-market IDs as strings (player markets do not appear here — fixtures are per-team). kickoff is Unix seconds UTC. The window includes matches that kicked off up to 3 hours ago so callers can also use this endpoint to detect live matches. Cached server-side for 1 hour; if the upstream StatsBomb fetch fails, the previous cached snapshot is returned (or {} if there is none).

GET /api/live/matches

Active and recently completed matches with per-team/player live OBV aggregates.

{
  "active": [
    {
      "matchId": 1371134,
      "homeTeam": "Crystal Palace",
      "awayTeam": "West Ham",
      "homeTeamId": 13,
      "awayTeamId": 20,
      "eventsProcessed": 1347,
      "teamObv":   { "13": 0.12, "20": -0.08 },
      "playerObv": { "45": 0.03, "67": -0.01 }
    }
  ],
  "completed": [
    {
      "matchId": 1371129,
      "homeTeam": "Arsenal",
      "awayTeam": "Everton",
      "officialDataExpectedAt": 1745382000
    }
  ],
  "wsConnections": 12
}

teamObv / playerObv keys are REST IDs (the canonical IDs used in roster.json); the Live↔REST ID bridge translates live-side IDs before they appear here. If the crank has not yet attached a LiveProcessor, the endpoint returns { "active": [], "completed": [] }.

GET /health / GET /api/health

{
  "status": "ok",
  "uptime": 842391,
  "lastCycleAt": "2026-05-12T11:30:00.000Z",
  "crankCycles": 4317,
  "wsConnections": 12,
  "liveObvSources": [
    {
      "matchId": 1371134,
      "team":   { "source": "authoritative", "reason": "obv-engine returned totals", "since": 1745399700000 },
      "player": { "source": "authoritative", "reason": "obv-engine returned totals", "since": 1745399700000 }
    }
  ],
  "liveProcessor": {
    "lastSuccessfulPollMs": 1745399945123,
    "pollErrorsInLastMinute": 0,
    "lastPollSummary": "12 matches polled, 1 active"
  },
  "shadowObv": null,
  "idBridge":  null
}

shadowObv is present only when the crank runs V2 in shadow mode (SHADOW_REALTIME_OBV=true). idBridge is present when the Live↔REST ID bridge has registered a provider. liveProcessor is null when no live processor is attached.

Returns HTTP 200 unconditionally — the monitor service parses the body. Use status === "ok" as the readiness signal.

Market keys

Market keys are the URL-safe identifier format:

FormatExample
team-{slug}team-arsenal, team-manchester-united
player-{slug}player-saka, player-haaland

The slug is the team or player name lowercased, diacritics stripped, spaces → hyphens. Both the exact and normalized variants are stored in engine/market-map.ts so lookups by either resolve cleanly.

Always prefer market_id for programmatic access. Use /api/roster to resolve name → ID and then build the key, or store the key as returned by the roster — IDs are the authoritative identifier and survive renames.

Timeframes and aggregation

The crank writes a tick every cycle (every 5 minutes outside live windows; faster during live matches as event-driven updates arrive). The store records raw 1-minute bars and aggregates on read:

TimeframeBucketBars per crank cycle outside live
1m60 s1 (sparse — only populated for recorded ticks)
1H3600 severy 12 ticks
4H14400 severy 48 ticks
1D86400 severy 288 ticks

Bar boundaries are aligned to UTC midnight (floor(time / bucket) * bucket). 1m candles may be sparse between the 5-minute oracle ticks — charts typically render 1H or 4H for the main view and 1m only during live matches where event-driven updates populate the gaps.

Rate limits

No hard rate limit today. The service runs on a single Hetzner CPX22 (8 GB RAM, 3 vCPU) and comfortably serves ~100 req/s. Be a good citizen:

  • Cache the roster response client-side — it changes at most weekly.
  • Poll candles no more often than the bar duration allows.
  • Use the WebSocket feed for real-time updates instead of polling — see WebSocket Feed.

If you are planning high-volume consumption (backtesting, academic research), reach out — we can provide a direct dump from the SQLite store rather than hammering the API.

Historical backfill gaps

The candle store is append-only since the crank was deployed. Markets initialized after deployment only have candles from that point forward. There is no backfill of candles from before a market’s on-chain initialization — even if the engine calculated indices privately, they were not persisted.

If you need longer historical data for modeling, the underlying event data is available directly from the source vendor with appropriate credentials; composite index values can be replayed using engine/index-calculator.ts against historical inputs.

Further reading

  • WebSocket Feed — real-time updates, complementary to this REST API.
  • SDK — typed client for the on-chain program (the complementary “state” API to this REST “data” API).
  • engine/candle-api.ts — source.