---
title: "Recent trade tape for a contract"
method: GET
path: "/v1/trades"
tags: ["Trades"]
---

# Recent trade tape for a contract

`GET /v1/trades`

## Request

- Base URL: `https://md.kairos.trade`
- URL: `https://md.kairos.trade/v1/trades`
- Auth: one of:
  - none
  - API key in header `X-Client-Id` and API key in header `X-Api-Key` and API key in header `X-Api-Secret`

Returns the most recent individual trades for a single (provider, contract_id) pair,
newest first, deduplicated by trade id. Only rows with `price > 0 AND price <= 100 AND size > 0` are eligible.

**Price unit**: `price` is on the integer-cents scale **0–100 for every provider**
(Kalshi, Polymarket, predict.fun, Hyperliquid alike) — it is NOT a 0–1 probability and
NOT already divided into dollars. Multiply `size * price / 100` to get USD notional.
`price` is returned as a float because some venues (e.g. Kalshi) report sub-penny ticks.

**Window semantics**: the query window is `[now - window_seconds, before_or_now]`. The
lower bound is always computed from the *current* server time, not from `before` —
passing a `before` older than `now - window_seconds` can invert the window. If the
primary window yields zero rows, the handler transparently retries with an unbounded
lower bound `[0, before_or_now]` so contracts with no recent activity still return
their most recent historical trades. `has_more` reflects an internal over-fetch of
`limit + 1` rows, truncated back to `limit` before serialization.

**Rate-limit cost (HEAVY bucket)**: priced by requested depth — a larger `limit`
consumes more of your quota.

**Caching**: responses are cached for 3s per replica (with singleflight coalescing) to
absorb duplicate concurrent requests, and the implicit `now` upper bound is quantized to
that same 3s so the cache is usable at all. The HTTP response itself is
`Cache-Control: private, max-age=5` with no ETag — this route never returns 304.
Responses ≥1KB are gzip-compressed when the client sends `Accept-Encoding: gzip`.

## Query parameters

- `provider` 'kalshi' | 'kalshi_offchain' | 'polymarket' | 'dome' | 'opinion' | 'predictfun' | 'hyperliquid', required
- `contract_id` string, required
- `window_seconds` integer
- `limit` integer
- `before` integer

## Response `200`

Trade page for the window, newest first. `trades` may be empty (and `oldest_available_ts` null) if the contract has no recorded trades at all, even after the unbounded fallback query.

- TradeHistoryResponse
  - `trades` TradeRecord[], required: Trades in the resolved window, newest first, capped at `limit` entries.
    - `trade_id` string, required: Venue-scoped trade identifier (part of the dedup key together with provider_id and contract_id).
    - `contract_id` string, required: Echoes the requested contract_id.
    - `size` integer, required: Trade size, truncated to an integer.
    - `price` number, required: Trade price on the **0–100 cents scale, for every provider** (never a 0–1 probability, never already-divided dollars). May include sub-cent fractions on venues with sub-penny ticks (e.g. Kalshi). USD notional = size * price / 100.
    - `outcome` string, required: Outcome label as stored on the trade row (venue-dependent free text, e.g. "Yes"/"No").
    - `timestamp` number, required: Unix timestamp in seconds, with fractional (millisecond) precision preserved.
    - `token_id` string: Venue outcome-token identifier, when the venue is token-keyed (e.g. Polymarket CTF token id). Omitted when empty.
    - `metadata` string: Raw JSON-encoded metadata blob from the ingestion pipeline, passed through as a string (not parsed). Omitted when empty or the literal "{}".
    - `taker_address` string: On-chain taker address, when known. Omitted when empty.
    - `side` string: Canonical trade side as stored on the row. Omitted when empty.
  - `has_more` boolean, required: True when the number of trades returned equals the requested `limit`, meaning more trades likely exist beyond this page.
  - `oldest_available_ts` number, nullable, required: Unix timestamp (seconds, fractional) of the oldest trade in the returned page, or null when `trades` is empty. Reflects the oldest trade *in this response*, not necessarily the oldest trade ever recorded for the contract.
  - `coverage_hours` number, required: Hours between `oldest_available_ts` and the request time, rounded to 1 decimal place. 0.0 when `trades` is empty.

## Other responses

- `400`: Validation failure: missing `provider`, unrecognized `provider`, missing `contract_id`, `window_seconds`/`limit` outside their bounds, or a non-positive / non-integer `before`.
- `401`: No valid credential presented. Every path returns `code: unauthorized`; the triggers differ: - An `Authorization` header on a deployment where bearer auth is not enabled (`bearer authentication is not enabled`). - A bearer token that fails verification (`invalid or expired session`), or whose token/session has been revoked (`session has been revoked`). - An incomplete or wrong API-key header set. Sending *any* of `X-Client-Id`, `X-Api-Key`, `X-Api-Secret` commits the request to the API-key path, so a partial set is rejected rather than silently degrading to anonymous limits (`valid X-Client-Id, X-Api-Key, and X-Api-Secret headers are required`). - No credential material at all while the anonymous free tier is unavailable — switched off for the deployment, forced off through the runtime kill switch, or not yet resolvable on a replica that has never read the flag (it fails closed).
- `403`: API key presented from a source IP not in that credential's whitelist.
- `429`: Two different gates return this status, both with `code: rate_limited`: - **Per-credential (or per-IP, when anonymous) weighted budget exhausted.** The message is `rate limit exceeded`, or, for anonymous callers, `anonymous rate limit exceeded — request an API key for higher limits (…)`. `Retry-After` is the whole number of seconds until the current one-minute window rolls over, floored at 1. All four `X-RateLimit-*` headers are present. - **Per-IP admission gate.** An in-process, pre-authentication guard that keeps a flood off the shared credential cache and database. The message is `too many requests from this address` and `Retry-After` is a flat `60`. Because this fires *before* authentication, the response carries **no** `X-RateLimit-*` headers at all.
- `500`: The trade-history query failed.
- `503`: The rate limiter is unreachable. Fails CLOSED — the request is rejected rather than let through unmetered.

---

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