Base URL
Authentication
Every endpoint accepts an API key (X-Client-Id + X-Api-Key + X-Api-Secret), a first-party session JWT, or an admin secret. Session-JWT callers additionally pass the invite gate; API-key and admin callers bypass it.
Examples below read credentials from KAIROS_CLIENT_ID, KAIROS_API_KEY, and KAIROS_API_SECRET in your shell.
Candle routes (GET /candles, POST /candles/batch) need no scope and are not platform-gated. They share one candles rate-limit group at 200 requests/minute, so the two endpoints draw on a single budget; an operator tier can raise or lower it at runtime.
/trades/* routes need the trade:read scope on API-key credentials (admin and JWT callers bypass the scope check). They also enforce platform access for API-key callers: the fixed proxy routes check their fixed provider, while history and metrics check the provider query value.
The /trades/* routes carry no explicit limit decorator, so they fall back to the service-wide default of 100 requests/minute per route, keyed on the authenticated user (or the client IP when unauthenticated).
Get candles
(provider, contract_id, outcome) series over [start, end), bucketed at timeframe_seconds.
Auth: API key, session JWT, or admin secret — no scope required. 200 requests/minute (shared candles group).
Request
rebuild=true, the handler first checks for a byte-identical cached response and streams it straight back — a miss (or rebuild=true) falls through to reconstructing the candles from the underlying trade data and repopulates the cache. There is no “wait for ingestion” mode.
end is likewise clamped forward to the current bucket. A 1-second request whose window ends more than 24 hours ago is past retention and returns "candles": [] — not an error.
Response
Batch candles
GET /candles.
Auth: same as GET /candles. 200 requests/minute — the same candles limit group, so the two endpoints share one budget.
Request
400 batch implies <n> candles; maximum is 400000. The request body itself is capped at 8 MiB (413 beyond that).
A single bad or failed item never fails the whole batch. An empty window comes back as "candles": [], not an error. If the rebuild step fails after some items were already cached, those still return normally and every uncached item instead gets "candles": [], "error": "<message>". Only when no item could be served at all does the request fail with 500. Check results[].error per item rather than relying on the HTTP status.
Response
Kalshi trade proxy
GET /trade-api/v2/markets/trades, with Kairos-computed volume metrics appended.
Auth: API key or session JWT; API-key credentials need trade:read and Kalshi must be enabled globally for them. 100 requests/minute.
Request
metrics merged in.
The metrics object is computed locally over the time-filtered trades, since Kalshi’s API doesn’t filter server-side beyond min_ts/max_ts passthrough, and its window_seconds is hardcoded to 86400 regardless of the requested span.
Gotcha: The
trades array is Kalshi’s unfiltered list. The local time filter narrows only what metrics is derived from, so metrics.trade_count can be smaller than trades.length. Re-apply min_ts/max_ts client-side if you need the two to agree.Response
Polymarket trade proxy
GET https://data-api.polymarket.com/trades, with the same metrics shape as /trades/kalshi.
Auth: same model as /trades/kalshi, gated on Polymarket API access instead of Kalshi’s. 100 requests/minute.
Request
0x values are resolved to a condition ID before querying upstream — via the market-metadata cache, with a provider-API lookup as a last resort. If resolution never yields a 0x... value, the endpoint short-circuits and returns {"trades": [], "metrics": {}} without ever calling Polymarket.
Cached briefly; a non-2xx/network error from Polymarket returns 502. metrics approximates outcome_0/outcome_1 as the top two outcomes by traded volume in the window — an approximation, not a guaranteed yes/no mapping.
Response
{ "trades": [...], "metrics": {...} } — the metrics object has the same shape as /trades/kalshi’s, and trades is Polymarket’s raw trade array (fields like proxyWallet, side, asset, price). There is no cursor field on this endpoint. As on /trades/kalshi, the returned trades are unfiltered while metrics is derived from the after-filtered subset.
Gotcha:
price on these raw Polymarket trades is on a 0–1 scale — not the 0–100 cents scale used by candles and /trades/history elsewhere in this API.Trade history
(provider, contract_id), served from Kairos’s own trade store. Unlike the two proxy endpoints above, this is not a live upstream call — coverage depends on how much history has been backfilled/streamed for that contract.
Auth: API key or session JWT; API-key credentials need trade:read and the requested provider must be enabled globally for them. 100 requests/minute.
Request
Gotcha:
window_seconds and before are independent bounds, not a sliding window. The lower bound is always now − window_seconds; before only moves the upper bound. Walking backwards by feeding the oldest returned timestamp into before therefore narrows the window rather than shifting it.before instead — so an empty trades array means the contract has no ingested trades at all, not merely none in the window.
trigger_ingest is inert. It is validated and ignored: no ingestion job is started, and indexing is always false on this endpoint.
Response
Gotcha:
trades[].timestamp and oldest_available_ts are unix seconds (fractional), not milliseconds. Multiplying by 1000 before feeding a JavaScript Date is the usual fix.Volume metrics
(provider, contract_id) over a lookback window — the same backing store as /trades/history, not a live upstream proxy. There’s no buy/sell breakdown: metrics split by outcome (outcome_0/outcome_1), since trade direction can’t be reliably derived from every provider’s exchange data.
Auth: same as /trades/history. 100 requests/minute.
Request
(provider, contract_id, window_seconds), with a shorter TTL when coverage_pct came back low.
Response
An empty window is not an error. It returns zeroed volumes,
trade_count: 0, coverage_pct: 0.0, and outcome_0_pressure_pct: 50.0 — the neutral midpoint, not a measurement. Read trade_count before you trust a pressure value.
Errors
Errors are returned as{ "detail": "<message>" }. Two shapes deviate: 422s use FastAPI’s standard validation envelope, and 429s use { "error": "Rate limit exceeded: <limit>" } alongside X-RateLimit-* and Retry-After headers. The Code column holds the exact detail string the service returns; — means the body has no fixed message.
A batch whose ClickHouse tail fails but whose cache answered part of the request does not return 500 — see Batch candles.

