Skip to main content
Find the same underlying market listed on Polymarket, Kalshi, Predict.fun, and Hyperliquid, compare prices across venues, and stream cross-venue arbitrage opportunities — all powered by Kairos’s correlation engine. Full parameter reference and a live tester: API Reference.

Base URL

All REST paths on this page are relative to that host. The arb feed is a WebSocket protocol on a different host — see WebSocket arb feed.

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

  1. Page the full catalog with GET /matched-markets, or use GET /matched-markets/enriched when each side also needs identifiers, outcomes, and current reference pricing.
  2. Resolve a known market to its equivalents with GET /market-clusters, selecting exact or semantic matching.
  3. Look up counterparts for a market with GET /search/simple — every search response carries a correlations map of cross-venue counterparts.
  4. Stream live opportunities over the market-data WebSocket’s arb channel — a transport-bounded metadata snapshot plus live spread updates.
  5. Sports discovery helpers: GET /sports/trending-matched and GET /sports/matching-markets (both public), plus GET /sports/poly-kalshi-pairings below for game-level pairing.
  6. Execute both legs through the order endpoints or the external execution lane.

Matched markets catalog

Page through the full catalog of verified cross-venue matched pairs — the primary way to consume the correlation dataset over REST. Draws from the verified pairs in the correlation dataset, joined against market metadata for display. Auth: none — public. Rate limit: 60/minute. Cache-Control: 10 seconds.

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 with cursor= 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: count can be less than limit, because live-status filtering happens after paging. has_more — not count — is the stop signal, and legacy offset callers must advance offset by limit, never by count.
Gotcha: a 409 means the next page belongs to a different generation. Discard every accumulated page and restart from cursor=; you cannot resume mid-walk.
Errors: 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

Returns the verified catalog with indexed identifiers, outcomes, and current reference pricing attached to both sides. Use this endpoint when a client needs immediately tradeable identifiers without making separate /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 match GET /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

Every key in the catalog response is also present here — including 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

Resolve one or more known market references to their cross-venue equivalents. Use 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

The response is keyed by each requested reference that resolved to at least two available providers. Unknown references and groups with fewer than two available members are omitted rather than returned with empty arrays. 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

Every /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/provider arb, 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.
An 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

Server-side Polymarket ↔ Kalshi pairing index for currently-live games. The correlations feed keys on individual markets; this endpoint keys on games, useful for lining up whole live events across venues. Falls back to a last-known-good snapshot if the live Kalshi game cache is empty. Auth: none — public. Rate limit: 100/minute (this route carries no limit decorator, so it falls back to the service-wide default). Cache-Control: public, max-age=10.

Request

No parameters.

Response

Gotcha: Kalshi prices in kalshiMarkets are 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 is 0.0, not null.
Gotcha: volume is always 0.0 on moneyline and draw entries — per-side volume isn’t exposed at that level upstream. Only spread and total lines carry a real figure.
An empty pairings array is normal (no live games, or no matches found); the endpoint has no “no data” error.

Combo-eligible markets

Catalog of combo-eligible markets — Polymarket markets (combos are Polymarket-only) whose YES/NO legs can be combined into multi-leg positions. Each entry carries the condition id and both position ids needed to build a leg. Auth: none — public. Rate limit: 200/minute (the shared 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.