Base URL
Authentication
Every REST endpoint on this page is public and requires no credentials. Valid API-key or session credentials are accepted when supplied, but they are optional. The WebSocket feed has its own authentication handshake described below.How matching works
The arb-scanner continuously generates embedding-verified correlations between markets on different venues, each carrying a similarity score. Only pairs scoring ≥ 0.82 ever surface —min_similarity is clamped up server-side even if a caller asks for less. Correlations are symmetric and refreshed continuously; this dataset is the primary matching primitive everything below reads from.
Typical workflow
- Page the full catalog with
GET /matched-markets, or useGET /matched-markets/enrichedwhen each side also needs identifiers, outcomes, and current reference pricing. - Resolve a known market to its equivalents with
GET /market-clusters, selecting exact or semantic matching. - Look up counterparts for a market with
GET /search/simple— every search response carries acorrelationsmap of cross-venue counterparts. - Stream live opportunities over the market-data WebSocket’s
arbchannel — a transport-bounded metadata snapshot plus live spread updates. - Sports discovery helpers:
GET /sports/trending-matchedandGET /sports/matching-markets(both public), plusGET /sports/poly-kalshi-pairingsbelow for game-level pairing. - Execute both legs through the order endpoints or the external execution lane.
Matched markets catalog
Request
Response
Pairs whose either side has expired are excluded before paging. Pairs whose either side has resolved/closed, or whose venue indexing is paused, are dropped after paging.
Paging the catalog
For a complete live catalogue, start withcursor= and follow next_cursor until has_more is false. The cursor walks immutable pair identity and carries catalog_version; one windowed ClickHouse statement derives each page and its full-set fingerprint from the same snapshot.
Gotcha:countcan be less thanlimit, because live-status filtering happens after paging.has_more— notcount— is the stop signal, and legacy offset callers must advanceoffsetbylimit, never bycount.
Gotcha: aErrors:409means the next page belongs to a different generation. Discard every accumulated page and restart fromcursor=; you cannot resume mid-walk.
400 for an unknown provider, an undecodable cursor, an invalid sort_by, or cursor combined with a non-zero offset; 409 when a cursor generation changes and must be restarted; 503 when the correlation store is unreachable (fail-fast — never a silently degraded page). Full table under Errors.
Enriched matched markets
/markets/details and /markets/batch-prices calls.
Auth: none — public. Rate limit: 60/minute. Cache-Control: no-store.
Request
The request parameters and cursor restart semantics matchGET /matched-markets — offset, cursor, provider, min_similarity, sort_by, and include_total all behave identically. One row differs:
Gotcha: the 150 cap is not a clamp.
limit=151 returns a 422, not a silently reduced page.Response
next_cursor and catalog_version in cursor mode — with details and pricing added to each side.
details is null when no market resolves for that side. pricing is best-effort and is null for unsupported markets, missing quotes, a non-positive price, or an unavailable venue; those conditions never remove a verified pair.
pricing.price is the venue’s current reference price on a 0–1 decimal scale (Kalshi cents are divided by 100 on the way in) and is not an executable bid or ask. pricing.volume is a pre-formatted display string (e.g. "1.2M"), not a number; pricing.liquidity is a number.
A 503 means matched-market or enrichment data could not be loaded.
Equivalence clusters
exact when the markets must express the same contract, or semantic to include the broader equivalent grouping.
Auth: none — public. Rate limit: 60/minute group default; anonymous callers receive the reduced anonymous tier. Cache-Control: public, max-age=10.
Request
Response
size is the stored cluster size and may include members filtered from the response. A cluster returns at most 32 eligible members; truncated: true means additional eligible members were omitted by this limit.
Errors: 400 for an unsupported floor, no usable references, or more than 200 references; 422 when markets is missing; 503 when matching data is temporarily unavailable.
Cross-venue correlations
/search/simple response is enriched with a correlations map: for each result market, its cross-venue counterparts (matched above a similarity threshold, capped at 4 per result). This is the REST way to answer “is this market also listed on the other venue, and at what confidence?” alongside a normal search.
Auth: none — public. Rate limit: 60/minute (the shared search limit group).
Request
The full parameter set covers general search (offset, tags, statuses, sort_by, sort_order, include_total, include_expired, …) — see Search and the API Reference for all of it. Parameters relevant to matching:
Response
Abridged — the full envelope is{results, meta, groups?, singles?, correlations?}; only the correlation-specific parts are shown.
correlations is attached best-effort — it’s simply absent if no counterparts were found (or if the lookup failed), and never blocks or degrades the main search result. Counterparts whose market has resolved/closed, or whose venue’s indexing is paused, are dropped; a counterpart with no market row is kept but arrives without the display fields.
WebSocket arb feed
Filtered matched-pair snapshots and live spread updates stream over the market-data WebSocket (wss://stream.kairos.trade).
This is a WebSocket protocol, not a data.kairos.trade REST route — it has no entry in the API Reference, and there is no curl example for it: the transport is a binary protobuf stream, so it cannot be exercised with an HTTP request. API-key auth is supported on the upgrade, same headers as above.
Match snapshots are bounded by the NATS payload ceiling and prioritise metadata referenced by live spreads; use GET /matched-markets when a complete browse catalogue is required.
Subscribing
Subscribe with the fixed contract id/providerarb, topic arb, and an arb_subscription filter — the server applies it before sending either batch, so clients never see the global arb set:
Gotcha: the two list filters have opposite empty semantics. An empty
provider_pairs matches nothing — send all three combinations to get everything — while an empty categories means every category.min_arb_liquidity_scaled filters on max_arb_liquidity_scaled; zero disables liquidity filtering. Unverified fail-open matches carry similarity_scaled = 0, never a fabricated perfect-confidence score.
Subscription lifecycle
Subscriptions support a full connection-scoped lifecycle:0x0D ArbMatchList
Two binary message types follow, both using [1-byte tag][protobuf] framing, as with all market-data messages. ArbMatchList is the metadata snapshot.
Envelope fields: timestamp_us, count, matches[], subscription_id, category_counts[].
Each ArbMatch carries:
count and category_counts[] describe the catalogue before your subject filter is applied, so they will exceed what you actually receive.
0x0E ArbSpreadList
ArbSpreadList is the authoritative active-opportunity snapshot.
Envelope fields: timestamp_us, count, subscription_id, and opportunities[].
Gotcha:
ArbSpreadList uses opportunities[], not matches[] — and you must replace local state on every batch, including an empty one. Merging batches leaves dead opportunities on screen.ArbSpread uses the same scaled price/similarity/liquidity conventions as ArbMatch but a trimmed field set:
See the Protobuf Reference for full schemas.
Poly-Kalshi game pairings
public, max-age=10.
Request
No parameters.Response
Gotcha: Kalshi prices inkalshiMarketsare rescaled to 0–100, not the 0–1 scale used elsewhere in this API, to match a legacy response shape. They are rounded to 1 decimal, and a missing price is0.0, notnull.
Gotcha:An emptyvolumeis always0.0on moneyline and draw entries — per-side volume isn’t exposed at that level upstream. Only spread and total lines carry a real figure.
pairings array is normal (no live games, or no matches found); the endpoint has no “no data” error.
Combo-eligible markets
market_data limit group). Cache-Control: public, max-age=300.
Request
No parameters.Response
Served straight from Kairos’s own store — this endpoint never calls Polymarket, so an empty
markets array means the catalog is empty, not that an upstream is down.
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. An unhandled server fault returns a generic 500 An internal error occurred. Please try again later.
The Code column holds the exact detail string the service returns; — means the response carries no fixed string.
The correlation attachment on
/search/simple is the one exception: it is fail-open, so a correlation-store failure drops the correlations key instead of erroring.
