> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Markets, Resolutions & Marks

> Market metadata, token resolution, on-chain settlements, and latest prices

Everything you need to identify a market before you chart or trade it: full
metadata, batch lookup, venue-identifier resolution, catalog enumeration,
settlement outcomes, the resolution lifecycle, and last traded prices.

Most integrations start with [Identifier resolution](#identifier-resolution)
(turn a venue ticker or token id into a Kairos `market_id`), then
[Market metadata](#market-metadata) (get the outcomes and their `token_id`s).

All endpoints on this page work anonymously on the
[free tier](/market-data/authentication); add API-key headers for
production budgets.

## Market metadata

```
GET /v1/markets/{provider}/{market_id}
```

Full metadata for one market — title, status, outcomes with their token ids,
[condition id](/learn/glossary), tick size, fees, imagery.

**Auth:** none required. `light` bucket, 1 unit.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | string | yes | — | Path segment. `kalshi`, `polymarket`, `predictfun`, `hyperliquid`; `opinion` for historical reads |
| `market_id` | string | yes | — | Path segment. The provider's market identifier |

### Example

```bash theme={null}
curl https://md.kairos.trade/v1/markets/polymarket/1897067
```

### Response

```json theme={null}
{
  "exchange_id": "polymarket",
  "market_id": "1897067",
  "condition_id": "0x…",
  "title": "…",
  "neg_risk": false,
  "tick_size": 0.01,
  "status": "active",
  "end_date": "…",
  "outcomes": [
    { "outcome": "Yes", "normalized_outcome": "yes", "token_id": "77891…" },
    { "outcome": "No",  "normalized_outcome": "no",  "token_id": "50174…" }
  ]
}
```

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `304` | — | `If-None-Match` matched | Reuse your cached body |
| `400` | `invalid_request` | Unknown `provider` | Use a supported provider slug |
| `404` | `not_found` | The market does not exist | Confirm the id with [Identifier resolution](#identifier-resolution) |
| `502` | `upstream` | The metadata cache answered with an unexpected status | Retry with backoff |
| `503` | `cache_cold` | The metadata cache is still warming | Retry after `Retry-After: 5` |
| `503` | `unavailable` | No metadata cache is configured for this deployment | Do not retry-loop; this will not resolve on its own |

## Batch metadata

```
POST /v1/markets/batch
```

The same metadata for up to 200 markets of **one** provider, in one round trip.

**Auth:** none required. `light` bucket, flat 1 unit whatever the batch size.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | string | yes | — | One provider for the whole call |
| `market_ids` | array of string | yes | — | 1–200 market identifiers |

### Example

```bash theme={null}
curl -X POST https://md.kairos.trade/v1/markets/batch \
  -H "Content-Type: application/json" \
  -d '{ "provider": "polymarket", "market_ids": ["1897067", "1961527"] }'
```

### Response

```json theme={null}
{
  "exchange_id": "polymarket",
  "markets": { "1897067": { … }, "1961527": { … } },
  "misses": []
}
```

<Note>
  **Ids that could not be found are listed in `misses`, not raised as errors.**
  A batch where nothing resolved is still a `200`. Check `misses` explicitly.
</Note>

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `400` | `invalid_request` | Unknown `provider`; missing or oversized `market_ids` | Split into calls of at most 200 ids, one provider each |
| `502` / `503` | `upstream`, `cache_cold`, `unavailable` | As for single metadata, above | Same remediation |

The response is `Cache-Control: no-store`, so it never `304`s.

## Tick size

Return the price grid a venue currently enforces for one market — the same grid your limit order is validated against. Tick sizes change while a market trades (Polymarket tightens the grid near `0.04` and `0.96`), so query this before you place a limit order rather than caching a value from listing time.

```
GET /v1/markets/tick-size
```

The response describes the grid; it never snaps a price for you. It is served from the metadata cache the orderbook streamer keeps current with the venue's live tick changes, so a Polymarket flip at the `0.96` / `0.04` extremes shows up here without waiting for a catalog cycle.

**Auth:** none required. `light` bucket, 1 unit.

### Parameters

<ParamField query="provider" type="string" required>
  The venue. Accepted values: `polymarket`, `kalshi`, `predictfun`.
</ParamField>

<ParamField query="contract_id" type="string" required>
  Kalshi ticker, or Polymarket condition id / market id.
</ParamField>

<ParamField query="asset_id" type="string">
  Polymarket CLOB token id. When given, the per-token grid is returned, which is where a live tick change lands first.
</ParamField>

### Example request

```bash theme={null}
curl "https://md.kairos.trade/v1/markets/tick-size?provider=kalshi&contract_id=KXBTC15M-26JUL221600-00"
```

### Example response

```json theme={null}
{
  "provider": "kalshi",
  "contract_id": "KXBTC15M-26JUL221600-00",
  "asset_id": null,
  "ranges": [
    { "start": "0",    "end": "0.04", "step": "0.001" },
    { "start": "0.04", "end": "0.96", "step": "0.01" },
    { "start": "0.96", "end": "1",    "step": "0.001" }
  ],
  "min_tick": "0.001",
  "source": "kalshi_price_ranges",
  "synthetic": false,
  "price_level_structure": "tapered",
  "as_of": "2026-09-18T12:00:00Z"
}
```

<Note>
  **Kalshi's tick is algorithmic** — finer at the tails, coarser in the middle, with boundaries that differ per market. When the live band layout is available you get it, as above. When it is not, you get a single flat band at the finest step, with `source: "streamer_projection"` and `synthetic: true`: the minimum tick is correct (it is the orderbook streamer's projection of the venue's own finest step, tracked live), but the boundaries are not described. Render the ladder at `min_tick` in that case — a price the venue's coarser middle band would reject is caught when the order is submitted.
</Note>

### Response fields

<ResponseField name="ranges" type="array">
  The price bands, lowest first. `step` applies to prices in `[start, end)`; the last band includes `end`.

  <Expandable title="Range object fields">
    <ResponseField name="start" type="string">Band start, decimal string.</ResponseField>
    <ResponseField name="end" type="string">Band end, decimal string.</ResponseField>
    <ResponseField name="step" type="string">Tick size within the band, decimal string.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="min_tick" type="string">
  The finest step anywhere on the grid, decimal string.
</ResponseField>

<ResponseField name="source" type="string">
  Where the grid came from: `kalshi_price_ranges`, `metadata_cache`, or `streamer_projection`.
</ResponseField>

<ResponseField name="synthetic" type="boolean">
  `true` when the band layout is not currently known and a single flat band at the finest step stands in for it. The minimum tick is still correct.
</ResponseField>

<ResponseField name="price_level_structure" type="string | null">
  `tapered` for a grid that tightens at the extremes, `null` for a flat grid.
</ResponseField>

<ResponseField name="as_of" type="string">
  When the grid was read, RFC 3339.
</ResponseField>

<Warning>
  `start`, `end`, `step` and `min_tick` are decimal strings. Parse them as decimals, not floats — a float `0.001` is not exactly `0.001`, and an off-grid price is rejected by the venue.
</Warning>

Polymarket returns one flat `[0, 1]` band with `source: "metadata_cache"`. `predictfun` has no per-market tick, so it returns `{ "provider": "predictfun", "contract_id": "…", "supported": false, "reason": "…" }` rather than a fabricated grid.

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `304` | — | `If-None-Match` matched | Reuse your cached body |
| `400` | `invalid_request` | Unknown `provider`, a provider with no per-market grid (`hyperliquid`, `opinion`), or an empty `contract_id` | Only ask for `kalshi`, `polymarket`, or `predictfun` |
| `404` | `not_found` | The metadata cache has no such market, even after its catalog and live-venue read-through | Check the id; a market that was never listed has no grid |
| `502` | `tick_unavailable` | The market exists but carries no usable grid: no Polymarket tick, or a Kalshi market the cache hydrated from the catalog rather than the venue (its `raw` has no `price_ranges`) | Retry later; a venue-hydrated Kalshi row carries the grid |
| `502` / `503` | `upstream`, `cache_cold`, `unavailable` | As for [single metadata](#market-metadata), above | Same remediation |

Responses are cached for 5 seconds (`public, max-age=5`, ETagged).

<Note>
  This is the canonical tick-size endpoint — the Data API's former `GET /markets/tick-size` route was retired in its favor (the request parameters and response body are the same, so moving an integration is a base-URL change).
</Note>

## Batch tick sizes

The same grids for up to 200 markets of **one** provider, in one round trip.

```
POST /v1/markets/tick-size/batch
```

### Request body

<ParamField body="provider" type="string" required>
  One provider for the whole call.
</ParamField>

<ParamField body="items" type="array" required>
  1–200 objects of `{ "contract_id": string, "asset_id"?: string }`.
</ParamField>

### Example request

```bash theme={null}
curl -X POST "https://md.kairos.trade/v1/markets/tick-size/batch" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "polymarket", "items": [ { "contract_id": "0x…", "asset_id": "7789…" }, { "contract_id": "0x…" } ] }'
```

### Example response

```json theme={null}
{
  "provider": "polymarket",
  "results": [
    { "provider": "polymarket", "contract_id": "0x…", "asset_id": "7789…", "ranges": [ { "start": "0", "end": "1", "step": "0.01" } ], "min_tick": "0.01", "source": "metadata_cache", "synthetic": false, "price_level_structure": null, "as_of": "2026-09-18T12:00:00Z" },
    { "contract_id": "0x…", "asset_id": null, "error": { "code": "not_found", "message": "market not found" } }
  ]
}
```

`results` is positional: `results[i]` answers `items[i]`. Each entry is a grid, the `predictfun` payload, or a per-item `error` carrying the code the single endpoint would have returned, so one bad id does not fail the batch. An item can also carry `timeout` (it was not started before the 15-second batch bound) or `cancelled` (the caller disconnected). The call costs a flat 1 unit from the `light` bucket, and responses are `Cache-Control: no-store`.

## Identifier resolution

```
POST /v1/market-identifiers/resolve
```

Resolves up to 200 venue-specific identifiers — tickers, market ids,
condition-like ids, outcome token ids — to canonical Kairos market ids. Use
this when you hold an id from a venue and need the id this API expects. The
request may mix providers.

**Auth:** none required. `light` bucket, **1 unit per submitted item**.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `requests` | array | yes | — | 1–200 items |
| `requests[].provider` | string | yes | — | Provider for this item; echoed back exactly as submitted, not canonicalized |
| `requests[].identifier` | string | yes | — | The venue identifier to resolve. Must be non-empty |
| `requests[].scope` | string | yes | — | `market` for market-level identifiers (tickers, market ids, condition-like ids); `outcome` for an identifier representing one outcome |

Scopes apply consistently to both on-chain and off-chain exchanges.

<Note>
  **Id spaces.** `scope: market` matches the venue's market identity — a Kalshi ticker, a Polymarket Gamma numeric market id, or a `0x…` condition id. `scope: outcome` matches an on-chain outcome/token id only; submitting a market id with `scope: outcome` returns `found: false`. Resolution returns the canonical `market_id` only — never outcome token ids. To get a market's outcome tokens, read `token_ids`/`outcomes` from [`GET /search/markets`](/rest/search#search-markets) or `POST /markets/details`, then pass them to `POST /v1/synthetics` and `/v1/candles`.
</Note>

### Example

```bash theme={null}
curl -X POST https://md.kairos.trade/v1/market-identifiers/resolve \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      { "provider": "polymarket", "identifier": "77891…", "scope": "market" },
      { "provider": "polymarket", "identifier": "77891…", "scope": "outcome" },
      { "provider": "kalshi", "identifier": "KXBTC-26JUL", "scope": "market" }
    ]
  }'
```

### Response

```json theme={null}
{
  "results": [
    {
      "provider": "polymarket",
      "identifier": "77891…",
      "scope": "market",
      "found": false,
      "market_id": null
    },
    {
      "provider": "polymarket",
      "identifier": "77891…",
      "scope": "outcome",
      "found": true,
      "market_id": "1897067"
    },
    {
      "provider": "kalshi",
      "identifier": "KXBTC-26JUL",
      "scope": "market",
      "found": true,
      "market_id": "KXBTC-26JUL"
    }
  ]
}
```

Results preserve request order. Unresolved identifiers return `found: false`
and `market_id: null`, so callers never need to correlate separate hit and
miss collections.

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `400` | `invalid_request` | A bad `scope`, an empty `identifier`, a missing or oversized `requests` array | The message carries the failing index — e.g. `requests[2].scope must be market or outcome`. Validation stops at the first bad item, so fix it and resend to see any others |

### Notes

> **A rejected batch still costs its full size.** The charge is 1 unit per
> submitted item, applied as soon as the array length is known — *before* any
> per-item validation. A 200-item batch rejected for one bad `scope` costs 200
> units. Validate your `scope` values client-side.

> **When you don't know which shape you hold, submit it under both scopes.**
> Put the identifier in the same batch once with `scope: "market"` and once
> with `scope: "outcome"`, then use whichever came back `found`. If the two
> scopes return *different* market ids, treat the input as ambiguous rather
> than picking one.

Unlike [Batch metadata](#batch-metadata), which is a flat 1 unit, this
endpoint scales with batch size. The response is not ETagged.

## Enumeration

```
GET /v1/markets
```

Pages through a provider's active markets.

**Auth:** none required. `light` bucket, 1 unit.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | string | yes | — | Provider whose catalog to page |
| `limit` | integer | no | `100` | Page size, 1–250 |
| `cursor` | string | no | — | The `next_cursor` from the previous page |

### Example

```bash theme={null}
curl -G https://md.kairos.trade/v1/markets \
  --data-urlencode "provider=polymarket" \
  --data-urlencode "limit=250"
```

Follow `next_cursor` while `has_more` is true.

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `304` | — | `If-None-Match` matched | Reuse your cached body |
| `400` | `invalid_request` | Unknown `provider`; `limit` outside 1–250 | Fix the parameter and resend |
| `503` | `cache_cold` | The provider's listing has not been populated yet | Retry after `Retry-After: 5` |
| `503` | `unavailable` | No metadata cache is configured for this deployment | Do not retry-loop |

## Settled outcomes

```
GET /v1/resolutions
```

The settlement outcome for resolved markets, for **any** registered provider —
not Polymarket only.

**Auth:** none required. `light` bucket, 1 unit.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | string | yes | — | Provider for every id in the call |
| `market_ids` | string | yes | — | Comma-separated, up to 200 ids |

### Example

```bash theme={null}
curl -G https://md.kairos.trade/v1/resolutions \
  --data-urlencode "provider=polymarket" \
  --data-urlencode "market_ids=1897067,1961527"
```

### Response

```json theme={null}
{ "resolutions": { "1897067": 1.0, "1961527": 0.0 } }
```

The value is the YES payout fraction — `1.0` YES won, `0.0` NO won, `0.5`
split. Scalar/range markets with no payout vector report the venue's settled
value instead.

<Note>
  **Markets that haven't resolved are omitted from the map.** Treat a missing
  key as "not settled yet", not as an error. A response of
  `{"resolutions": {}}` means none of your ids have settled.
</Note>

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `304` | — | `If-None-Match` matched | Reuse your cached body |
| `400` | `invalid_request` | Unknown `provider`; missing or oversized `market_ids` | Send at most 200 ids per call |
| `500` | `internal` | The resolutions query failed | Retry with backoff |

### Notes

The lookup is keyed differently per venue: Kalshi tickers identify the market
directly, while CTF venues (`polymarket`, `predictfun`, `opinion`) are joined
to their on-chain condition id first.

## Resolution lifecycle

Where `/v1/resolutions` answers "did it settle, and how", these two answer
"where is it in the process". Both take the same `{provider}/{market_id}` path
as [Market metadata](#market-metadata).

**Auth:** none required. `light` bucket, 1 unit each.

### Current state

```
GET /v1/markets/{provider}/{market_id}/resolution
```

Status, the UMA-style proposal/dispute metadata, and — once resolved — the
payout numerators.

```bash theme={null}
curl https://md.kairos.trade/v1/markets/polymarket/1897067/resolution
```

```json theme={null}
{
  "provider": "polymarket",
  "market_id": "1897067",
  "condition_id": "0x…",
  "status": "proposed",
  "proposed_price": 1.0,
  "proposed_at": "2026-07-15T09:30:00+00:00",
  "challenge_window_ends_at": "2026-07-15T11:30:00+00:00",
  "proposer": "0x…",
  "disputer": "",
  "dispute_count": 0,
  "reset_count": 0,
  "payout_numerators": [],
  "resolved_ts": null,
  "last_event_ts": "2026-07-15T09:30:00+00:00"
}
```

Proposals move through a roughly two-hour challenge window, so this response
is only cached for a short period — long enough to absorb polling, short
enough not to report a disputed market as still merely proposed.

### Event timeline

```
GET /v1/markets/{provider}/{market_id}/resolution/events
```

The append-only timeline behind that state, oldest first, capped at 200 events.

```bash theme={null}
curl https://md.kairos.trade/v1/markets/polymarket/1897067/resolution/events
```

```json theme={null}
{
  "provider": "polymarket",
  "market_id": "1897067",
  "market_key": "0x…",
  "events": [
    { "event_type": "proposed", "source": "polygon",
      "event_ts": "2026-07-15T09:30:00+00:00", "price_norm": 1.0,
      "proposer": "0x…", "bond": "500000000000", "reward": "5000000",
      "expiration_ts": "2026-07-15T11:30:00+00:00",
      "tx_hash": "0x…", "block_number": 74812339 }
  ]
}
```

`market_key` is the key the events were actually queried under — the ticker
for Kalshi, the resolved condition id for CTF venues.

<Note>
  **Optional fields are omitted entirely when unset, not sent as `null`.** Use
  key presence, not a null check, when decoding events.
</Note>

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `304` | — | `If-None-Match` matched | Reuse your cached body |
| `400` | `invalid_request` | Unknown `provider` | Use a supported provider slug |
| `404` | `not_found` | The market has no resolution state. For CTF venues this includes "no condition id mapping was found" | Not an error condition for an unproposed market — treat it as "no lifecycle yet" |
| `500` | `internal` | The lifecycle query failed | Retry with backoff |

## Last traded prices (marks)

```
GET /v1/marks
```

The **last traded price** per `(contract_id, token_id)` pair. A mark moves only
when a new trade executes.

**Auth:** none required. `heavy` bucket, 1 unit per started 100 pairs (so 2 at
the 200-pair cap).

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `provider` | string | yes | — | Provider for every pair in the call |
| `pairs` | string | yes | — | Comma-separated `contract_id:token_id` pairs, up to 200. Both halves of each pair must be non-empty |

### Example

```bash theme={null}
curl -G https://md.kairos.trade/v1/marks \
  --data-urlencode "provider=polymarket" \
  --data-urlencode "pairs=1897067:77891…,1961527:50174…"
```

### Response

```json theme={null}
{
  "marks": [
    { "contract_id": "1897067", "token_id": "77891…", "price": 35 }
  ]
}
```

Prices are on the 0–100 scale.

<Warning>
  **Pairs that have never traded are omitted.** Treat absence as "no trade
  history", not as an error.
</Warning>

### Errors

| Status | Code | When it happens | What to do |
| - | - | - | - |
| `400` | `invalid_request` | Unknown `provider`; missing or oversized `pairs`; a malformed pair | Each pair must be exactly `contract_id:token_id` with both halves non-empty; send at most 200 |
| `500` | `internal` | The marks query failed | Retry with backoff |

### Notes

<Warning>
  **Do not poll this endpoint for real-time prices.** It draws on the `heavy`
  bucket and is explicitly uncacheable (`Cache-Control: no-store`, no ETag, so
  never a `304`). For live updates use the
  [WebSocket API](/websocket/market-data-websocket).
</Warning>

## Caching summary

Which responses on this page are ETagged and can return `304` on
`If-None-Match`:

| Endpoint | ETagged |
| - | - |
| `GET /v1/markets/{provider}/{market_id}` | Yes |
| `GET /v1/markets` (enumeration) | Yes |
| `GET /v1/resolutions` | Yes |
| `GET /v1/markets/…/resolution`, `…/resolution/events` | Yes |
| `GET /v1/markets/tick-size` | Yes — 5s |
| `POST /v1/markets/tick-size/batch` | No — `no-store` |
| `POST /v1/markets/batch` | No — `no-store` |
| `POST /v1/market-identifiers/resolve` | No |
| `GET /v1/marks` | No — `no-store` |

Shared `401` / `403` / `429` behavior is on the
[Authentication](/market-data/authentication#errors) page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.