Horse Race API
Placement prediction markets — each race asks: which of N assets finishes in 1st place? 2nd? 3rd? Every (asset, position) pair has its own CLOB orderbook, so you can take a view on the full finish order, not just the winner. Each share pays $1 if that asset lands in that position when the epoch closes.
Authentication
Read endpoints are public. Trading endpoints require a JWT obtained from POST /auth/login.
curl -X POST https://cymetica.com/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"…"}'
Use the returned access_token as Authorization: Bearer <token>.
Races
/api/v1/horse-race/races?active_only=trueList active or all races. Returns id, status (pending | trading | settling | resolved), asset list, max placement depth, and active epoch info.
curl https://cymetica.com/api/v1/horse-race/races
[
{
"id": "race_a1b2c3...",
"status": "trading",
"field_size": 4,
"max_placement": 3,
"starts_at": "2026-05-19T20:00:00Z",
"ends_at": "2026-05-19T20:05:00Z",
"assets": [
{"symbol": "BTC", "name": "Bitcoin", "asset_type": "crypto", "logo_url": "…"},
{"symbol": "ETH", "name": "Ethereum", "asset_type": "crypto", "logo_url": "…"},
{"symbol": "SOL", "name": "Solana", "asset_type": "crypto", "logo_url": "…"},
{"symbol": "AVAX", "name": "Avalanche","asset_type": "crypto", "logo_url": "…"}
],
"active_epoch": {"id": 5930741, "ends_at": "2026-05-19T20:05:00Z"}
}
]
/api/v1/horse-race/races/{race_id}Full race detail — placements, active epoch, start-snapshot prices, and live prices per asset. Use this to render the race grid.
Orderbooks
/api/v1/horse-race/races/{race_id}/book/{asset_symbol}/{position}?levels=20Per-(asset, position) CLOB orderbook. Each book represents the market for "this asset finishes in position N". Position is 1-indexed (1 = first place; max 5). symbol in the response is the internal placement symbol — {asset}_P{position}.
curl https://cymetica.com/api/v1/horse-race/races/race_a1b2.../book/BTC/1
{
"symbol": "BTC_P1",
"bids": [{"price": "0.32", "size": "50.00000000", "orders": 2}],
"asks": [{"price": "0.38", "size": "50.00000000", "orders": 1}],
"best_bid": "0.32",
"best_ask": "0.38",
"spread": "0.06",
"last_trade": "0.35",
"sequence": 17
}
/api/v1/horse-race/races/{race_id}/booksSnapshot of every (asset × position) book in a single race in one call. Useful for the placement grid.
/api/v1/horse-race/races/{race_id}/gridCompact best-bid/best-ask matrix across all asset×position cells. Drives the placement grid UI. The matrix is wrapped under grid; row sums (per asset) and column sums (per position) are returned alongside it for arbitrage checks (no-arb requires every row sum ≈ 1).
{
"grid": {
"BTC": {"1": {"mid_price": 0.35, "best_bid": 0.32, "best_ask": 0.38, "last_trade": 0.35}, "2": {…}, "3": {…}},
"ETH": {"1": {…}, "2": {…}, "3": {…}},
"SOL": {…},
"AVAX": {…}
},
"row_sums": {"BTC": 1.02, "ETH": 0.98, "SOL": 1.00, "AVAX": 1.00},
"col_sums": {"1": 1.00, "2": 1.00, "3": 1.00},
"field_size": 4,
"max_placement": 3
}
Place Order
/api/v1/horse-race/races/{race_id}/ordersPlace a limit or market order on one placement book. Each share pays $1 if the asset lands in that position when the race resolves.
curl -X POST https://cymetica.com/api/v1/horse-race/races/race_a1b2.../orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset_symbol": "BTC",
"position": 1,
"side": "buy",
"price": 0.35,
"size": 10,
"order_type": "limit",
"post_only": false
}'
Constraints: price ∈ (0, 1), size > 0, position ∈ [1, max_placement]. Selling requires you already hold ≥ size shares at that (asset, position). Buying past MAX_POSITION_SHARES per book is rejected.
Cancel Order
/api/v1/horse-race/races/{race_id}/orders/{order_id}?asset_symbol=BTC&position=1Cancel a resting limit order. asset_symbol and position are required query params (the order belongs to one specific placement book and the engine routes by them). Returns 400 if the order is already fully filled or already cancelled.
curl -X DELETE \
"https://cymetica.com/api/v1/horse-race/races/race_a1b2.../orders/{order_id}?asset_symbol=BTC&position=1" \
-H "Authorization: Bearer $TOKEN"
{"cancelled": true, "order_id": "ord_..."}
Positions
/api/v1/horse-race/races/{race_id}/my-positionsAll shares the authenticated caller holds in this race, grouped by (asset, position). Includes average entry price and unrealized PnL vs the live mid.
/api/v1/horse-race/races/{race_id}/ordersAuthenticated caller's open + recently-filled orders in this race.
Results
/api/v1/horse-race/races/{race_id}/resultsFinal placement order, $/share payouts per (asset, position), and per-user settlement summaries. Available once status == "resolved".
WebSocket
Subscribe to live grid, depth, trade and lifecycle updates for a single race:
wss://cymetica.com/ws/horse-race/{race_id}
Messages arrive as JSON with a type field. Common types:
{"type": "grid_update", "grid": { /* same shape as GET /races/{id}/grid */ }}
{"type": "depth", "asset": "BTC", "position": 1, "depth": { /* same as GET .../book/... */ }}
{"type": "placement_trade", "asset": "BTC", "position": 1, "trade": { "price": "0.35", "size": "5", "side": "buy", "ts": 1779200000.0 }}
{"type": "race_result", "results": [ /* same as GET .../results */ ]}
{"type": "status", "status": "trading"}
{"type": "price_update", "prices": {"BTC": 65000.0, "ETH": 3400.0}}
{"type": "pong"} // reply to client {"type": "ping"}
{"type": "heartbeat"}
A single socket subscription covers every (asset, position) book in the race — filter client-side on asset and position.
Rate Limits
Order placement is rate-limited per wallet/account (see X-RateLimit-* response headers). Read endpoints are server-cached and not individually rate-limited.
Errors
{"detail": "Race not found"} // 404
{"detail": "Race is pending, not accepting orders"} // 400
{"detail": "Invalid asset symbol"} // 400
{"detail": "Asset XYZ not in this race"} // 400
{"detail": "Position 4 exceeds max placement 3"} // 400
{"detail": "No active epoch"} // 400
{"detail": "Insufficient shares: holding X, trying to sell Y"} // 400
{"detail": "Position would exceed maximum N shares"} // 400
{"detail": "Order size exceeds maximum"} // 400
{"detail": "Rate limit exceeded"} // 429