---
title: "Find markets by what their book and tape are doing"
method: GET
path: "/search/screener"
tags: ["Search"]
---

# Find markets by what their book and tape are doing

`GET /search/screener`

## Request

- Base URL: `https://data.kairos.trade`
- URL: `https://data.kairos.trade/search/screener`
- Auth: API key in header `X-Client-Id` and API key in header `X-Api-Key` and API key in header `X-Api-Secret`

Screens the whole live catalogue by spread, resting depth, rolling volume, open interest and price, rather than by text. Numbers are live from the market-vitals cache (sub-second book and tape), joined to the discover cache for titles; a market the discover cache has never seen is still returned, under its id, with `hasMetadata: false`.

Prices and spread are in cents, money in dollars. **At least one metric bound is required** — an unfiltered screen is a ranking, not a screen, and is rejected with 400.

`spread` is `ask - bid` on one outcome, and a book with only one side cannot be crossed, so it has no spread: any query bounding spread only matches markets quoting both sides. `two_sided` asks for that explicitly without bounding the width. Bounds are compared on one canonical price scale, so a threshold means the same width on every venue whatever scale that venue publishes.

## Query parameters

- `min_spread` number
- `max_spread` number
- `min_price` number
- `max_price` number
- `min_volume_24h` number
- `min_volume_1h` number
- `min_liquidity` number
- `max_liquidity` number
- `min_outcome_liquidity` number
- `min_open_interest` number
- `two_sided` boolean
- `max_book_age_minutes` number
- `provider` string[]
- `sort` 'volume_24h' | 'volume_1h' | 'spread_desc' | 'spread_asc' | 'liquidity' | 'open_interest'
- `limit` integer
- `offset` integer

## Response `200`

A page of matching markets, most relevant to the chosen sort first.

- ScreenerResponse
  - `markets` ScreenerMarket[], required
    - `id` string, required: The id this market's book is published under. For Polymarket that is the venue condition id, not the market id search returns — see `marketId`.
    - `marketId` string, nullable, required: The id to address this market by everywhere else — the terminal, the market-data websocket, the trades API. Always present, and equal to `id` on every venue keyed by its market id. Null only on a Polymarket row the metadata cache could not resolve; a null row cannot be opened, and `id` is not a substitute for it.
    - `provider` string, required
    - `title` string, nullable: Null when the discover cache has never seen this market, alongside `hasMetadata` false. Presenting that — usually as the id — is the client's call.
    - `eventTitle` string, nullable
    - `image` string, nullable
    - `icon` string, nullable
    - `category` string, nullable
    - `status` string, nullable
    - `hasMetadata` boolean: False when only live numbers are known and the title is the id.
    - `outcomeIndex` integer, nullable: The outcome the quote figures below describe.
    - `matchedLeg` boolean: True when the screen picked this outcome. False when the screen was contract-level only and this is just the primary outcome.
    - `outcomeName` string, nullable
    - `bid` number, nullable: Cents.
    - `ask` number, nullable: Cents.
    - `mid` number, nullable: Cents.
    - `spread` number, nullable: Cents wide on this outcome; null when it is not quoting both sides.
    - `price` number: Cents; the record's headline price.
    - `volume24h` number: Dollars.
    - `volume1h` number: Dollars.
    - `liquidity` number: Dollars resting across every outcome, both sides.
    - `outcomeLiquidity` number, nullable: Dollars resting on this outcome alone, both sides.
    - `bookAgeSeconds` number, nullable: Seconds since this leg's top of book was published; null when it never has been. A large value means nothing is streaming this market.
    - `openInterest` number: Contracts.
    - `openInterestNotional` number: Dollars.
    - `updatedAt` string, date-time
  - `total` integer, required: Matches before paging. **-1** means the ordered walk stopped at the page bound and the rest were not counted — render it as "top N", not as a count.
  - `available` boolean, required: False when the live vitals service could not be reached. Distinct from a screen that matched nothing, which is available with an empty `markets`.
  - `plan` string: How the scan ran (`index_ordered`, `index_range`, `scan`). Diagnostic.
  - `limit` integer
  - `offset` integer

## Other responses

- `400`: No metric bound given, an inverted range, an unknown provider or sort, or a query the vitals service refused.
- `401`: No valid credential presented — missing/invalid API-key headers, or an invalid/expired session token.
- `403`: Authenticated but not permitted. Three distinct causes: the session user is not invited (`Invite required`), the API key lacks the operation's scope, or API-key access to the requested venue is switched off (`API access is disabled for <provider>`). Admin and API-key callers bypass the invite check; session and admin callers bypass scope and venue checks.
- `422`: Request failed FastAPI parameter validation.
- `429`: Rate limit exceeded for this route's sliding window, keyed on the session `sub` when authenticated and on the trusted client IP otherwise. Honor `Retry-After`. A per-API-key data ceiling (set per credential) rejects with `{"detail": "API key data rate limit exceeded"}` instead of the `error` envelope below.
- `500`: Screener query failed.

---

[API](https://skmtc.dev/kairos/apis/kairos-data-api.md) - [All operations](https://skmtc.dev/kairos/apis/kairos-data-api/llms.txt) - [OpenAPI document](https://skmtc.dev/kairos/apis/kairos-data-api/revisions/cf6b76079e38?raw)
