> ## 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.

# Recent trade tape for a contract

> 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`.




## OpenAPI

````yaml /openapi/market-data-api.yaml get /v1/trades
openapi: 3.1.0
info:
  title: Kairos Market Data API
  version: 1.2.0
  summary: Read-only prediction and perpetual market data across venues.
  description: >
    The Kairos Market Data API provides read-only access

    to normalized prediction-market data across every venue Kairos integrates:

    OHLCV candles, the trade tape, market metadata, resolution lifecycles, and

    latest mark prices.


    ## No signup required


    Every endpoint works **without credentials** on the anonymous free tier,

    rate-limited per source IP (120 light units/min, 20 heavy units/min). Try
    any

    request from the interactive docs at

    [app.kairos.trade/docs/market-data](https://app.kairos.trade/docs/market-data)

    or straight from curl. For production budgets (1,200 light and 150 heavy

    units/min by default, raisable per credential), request an API key from the

    Kairos team.


    ## Authentication


    Three modes, resolved per request:


    - **Anonymous** — no credential headers at all. Free tier, IP-keyed limits.

    - **API key** — `X-Client-Id` + `X-Api-Key` + `X-Api-Secret` headers
      (all three; partial header sets are rejected with 401 rather than
      degrading to anonymous).
    - **First-party session** — `Authorization: Bearer` JWT (Kairos web app
      sessions only; not offered to API consumers).

    Resolution order is fixed: an `Authorization` header takes the bearer

    path, any of the three API-key headers takes the API-key path, and only

    a request carrying neither is eligible for the anonymous tier.


    On the API-key path, an unreachable credential store surfaces on **any**

    endpoint as `500` with `code: internal` and the message

    `authentication backend unavailable`. A source IP outside a credential's

    whitelist is `403 ip_not_whitelisted`; that is the only 403 this service

    emits.


    ## Rate limiting


    Weighted sliding-window budgets over one minute, in **units**, across two

    independent buckets: light (candles, metadata, resolutions, trade

    metrics) and heavy (trade history, marks, perpetual snapshots). Most

    requests cost 1 unit; larger requests may consume more of your quota —

    each operation's `x-kairos-rate-limit` states its exact cost model.

    Every admitted response carries `X-RateLimit-Bucket`, `X-RateLimit-Tier`,

    `X-RateLimit-Remaining`, and `X-RateLimit-Reset`; 429s add `Retry-After`.


    A separate per-IP admission gate runs *before* authentication to keep a

    flood off shared infrastructure. It also answers 429 `rate_limited`, but

    with `Retry-After: 60` and no `X-RateLimit-*` headers.


    Redis backs the weighted limiter and it fails **closed**: if it is

    unreachable the request is rejected with 503 `rate_limiter_unavailable`

    rather than let through unmetered.


    ## Conventions


    - **Prediction-market prices** are on the 0–100 scale (implied probability
      × 100) in the prediction candle, trade, and mark endpoints. Prediction
      USD notional = `size × price / 100`.
    - **Perpetual prices are direct venue prices**, not probabilities and not
      0–100 values. Perpetual quantities retain their declared native unit
      (`base_asset` or `contracts`). Never divide a perpetual price by 100 or
      compute contract notional without the instrument's authoritative contract
      multiplier; the snapshot does not synthesize a cross-venue notional.
    - **Hyperliquid is two products on one venue.** Its HIP-4 outcome markets
      are prediction markets, read with `provider=hyperliquid` on the
      prediction endpoints; its perpetuals are bare coins read under
      `/v1/perpetuals/hyperliquid/{instrument}/snapshot`, where the instrument
      is the canonical id `hl-mainnet-<symbol>-<quote>` (e.g.
      `hl-mainnet-btc-usdt`). Different id schemes, different price semantics;
      they are never interchangeable.
    - **Errors** all use one envelope:
      `{"error": {"code": "...", "message": "..."}}`.
    - **Providers** are case-insensitive; `kalshi_offchain` aliases `kalshi`,
      `dome` aliases `polymarket`, and `opinion` is resolvable for historic
      reads only.
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/market-data
  termsOfService: https://kairos.trade/terms
servers:
  - url: https://md.kairos.trade
    description: Production
  - url: https://staging-md.kairos.trade
    description: Staging
security:
  - {}
  - apiKeyClientId: []
    apiKeyKey: []
    apiKeySecret: []
tags:
  - name: Candles
    description: OHLCV candle series (1s → 1d), JSON or compact columnar binary.
  - name: Trades
    description: Trade tape and aggregate volume metrics.
  - name: Markets
    description: Market metadata, batch lookup, identifier resolution, and enumeration.
  - name: Resolutions
    description: Resolution outcomes and UMA-style lifecycle state/timelines.
  - name: Marks
    description: Latest mark (last trade price) per outcome token.
  - name: Perpetuals
    description: Live perpetual books, trades, candles, funding, and market state. Beta.
  - name: Status
    description: Health and readiness probes.
paths:
  /v1/trades:
    get:
      tags:
        - Trades
      summary: Recent trade tape for a contract
      description: >
        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`.
      operationId: getTradeHistory
      parameters:
        - name: provider
          in: query
          required: true
          description: >
            Venue identifier, case-insensitive. Resolved against the central
            provider registry; `kalshi_offchain` is an alias for `kalshi` and
            `dome` is an alias for `polymarket`. `opinion` resolves but is a
            disabled provider — valid only for historic reads.
          schema:
            type: string
            enum:
              - kalshi
              - kalshi_offchain
              - polymarket
              - dome
              - opinion
              - predictfun
              - hyperliquid
          example: polymarket
        - name: contract_id
          in: query
          required: true
          description: Venue-scoped contract/token identifier to fetch trades for.
          schema:
            type: string
          example: '0x8b1c119419f622e21fc576ec8e9c2c07b2c1b09babf0e1f3d1cb9bfc1a8f9c2'
        - name: window_seconds
          in: query
          required: false
          description: >
            Lookback window, in seconds, measured back from the current server
            time (not from `before`). Valid range [3600, 86400]; out-of-range or
            non-integer values are rejected with 400, not clamped.
          schema:
            type: integer
            minimum: 3600
            maximum: 86400
            default: 86400
          example: 86400
        - name: limit
          in: query
          required: false
          description: >
            Maximum number of trades to return, newest first. Valid range is [1,
            500]; the default is also 500. Drives the rate-limit cost — see the
            operation description.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
          example: 100
        - name: before
          in: query
          required: false
          description: >
            Upper bound of the window as a positive Unix timestamp in seconds
            (exclusive: trades with `trade_ts < before`). Omit to use the
            current time. Must be a positive integer or the request is rejected
            with 400.
          schema:
            type: integer
            format: int64
            minimum: 1
          example: 1737480000
      responses:
        '200':
          description: >
            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.
          headers:
            X-RateLimit-Bucket:
              schema:
                type: string
                example: heavy
            X-RateLimit-Tier:
              schema:
                type: string
                example: api-key
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Reset:
              schema:
                type: integer
                description: Unix timestamp when the rate-limit window resets.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradeHistoryResponse'
              example:
                trades:
                  - trade_id: 0x9f2a...c11
                    contract_id: >-
                      0x8b1c119419f622e21fc576ec8e9c2c07b2c1b09babf0e1f3d1cb9bfc1a8f9c2
                    size: 250
                    price: 62.5
                    outcome: 'Yes'
                    timestamp: 1737479998.412
                    token_id: 10945...3321
                    taker_address: '0xabc1234567890abcdef1234567890abcdef1234'
                    side: buy
                has_more: true
                oldest_available_ts: 1737479950.001
                coverage_hours: 0.01
        '400':
          description: >
            Validation failure: missing `provider`, unrecognized `provider`,
            missing `contract_id`, `window_seconds`/`limit` outside their
            bounds, or a non-positive / non-integer `before`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: limit must be an integer in [1, 500]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/IPNotWhitelisted'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The trade-history query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          $ref: '#/components/responses/RateLimiterUnavailable'
components:
  schemas:
    TradeHistoryResponse:
      type: object
      required:
        - trades
        - has_more
        - oldest_available_ts
        - coverage_hours
      properties:
        trades:
          type: array
          description: >-
            Trades in the resolved window, newest first, capped at `limit`
            entries.
          items:
            $ref: '#/components/schemas/TradeRecord'
        has_more:
          type: boolean
          description: >
            True when the number of trades returned equals the requested
            `limit`, meaning more trades likely exist beyond this page.
          example: true
        oldest_available_ts:
          type:
            - number
            - 'null'
          description: >
            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.
          example: 1737479950.001
        coverage_hours:
          type: number
          description: >
            Hours between `oldest_available_ts` and the request time, rounded to
            1 decimal place. 0.0 when `trades` is empty.
          example: 0.01
    ErrorResponse:
      type: object
      description: Canonical error envelope emitted by every Market Data API endpoint.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: invalid_request
            message:
              type: string
              description: Human-readable detail.
              example: market_ids exceeds maximum 200
    TradeRecord:
      type: object
      description: >
        A single deduplicated trade row. `token_id`, `metadata`,
        `taker_address`, and `side` are omitted entirely from the JSON (not
        emitted as null/empty) when the underlying value is empty — `metadata`
        is additionally omitted when it equals the default `"{}"`.
      required:
        - trade_id
        - contract_id
        - size
        - price
        - outcome
        - timestamp
      properties:
        trade_id:
          type: string
          description: >-
            Venue-scoped trade identifier (part of the dedup key together with
            provider_id and contract_id).
          example: '0x9f2ac3f1b0e2f4a9c8d7b6a5f4e3d2c1b0a9f8e7'
        contract_id:
          type: string
          description: Echoes the requested contract_id.
          example: '0x8b1c119419f622e21fc576ec8e9c2c07b2c1b09babf0e1f3d1cb9bfc1a8f9c2'
        size:
          type: integer
          format: int64
          description: Trade size, truncated to an integer.
          example: 250
        price:
          type: number
          description: >
            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.
          minimum: 0
          maximum: 100
          example: 62.5
        outcome:
          type: string
          description: >-
            Outcome label as stored on the trade row (venue-dependent free text,
            e.g. "Yes"/"No").
          example: 'Yes'
        timestamp:
          type: number
          description: >-
            Unix timestamp in seconds, with fractional (millisecond) precision
            preserved.
          example: 1737479998.412
        token_id:
          type: string
          description: >-
            Venue outcome-token identifier, when the venue is token-keyed (e.g.
            Polymarket CTF token id). Omitted when empty.
          example: '109451234567890332112345678903321'
        metadata:
          type: string
          description: >-
            Raw JSON-encoded metadata blob from the ingestion pipeline, passed
            through as a string (not parsed). Omitted when empty or the literal
            "{}".
          example: '{"maker_order_id":"0xabc"}'
        taker_address:
          type: string
          description: On-chain taker address, when known. Omitted when empty.
          example: '0xabc1234567890abcdef1234567890abcdef1234'
        side:
          type: string
          description: Canonical trade side as stored on the row. Omitted when empty.
          example: buy
  responses:
    Unauthorized:
      description: >
        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).
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: >-
            `Bearer realm="market-data-api"` on bearer-verification failure,
            `APIKey realm="market-data-api", header="X-Client-Id, X-Api-Key,
            X-Api-Secret"` on missing/invalid API-key credentials. Absent on the
            revoked-session and bearer-disabled paths.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: unauthorized
              message: >-
                valid X-Client-Id, X-Api-Key, and X-Api-Secret headers are
                required
    IPNotWhitelisted:
      description: API key presented from a source IP not in that credential's whitelist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: ip_not_whitelisted
              message: source IP not in credential whitelist
    RateLimited:
      description: >
        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.
      headers:
        Retry-After:
          schema:
            type: integer
          description: >-
            Seconds until the caller may retry — window-rollover seconds for the
            weighted limiter, a flat 60 for the IP admission gate.
        X-RateLimit-Bucket:
          schema:
            type: string
            enum:
              - light
              - heavy
        X-RateLimit-Tier:
          schema:
            type: string
            enum:
              - anonymous
              - api-key
              - user
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
            description: Unix timestamp (seconds) when the current window resets.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limited
              message: rate limit exceeded
    RateLimiterUnavailable:
      description: >
        The rate limiter is unreachable. Fails CLOSED — the request is rejected
        rather than let through unmetered.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limiter_unavailable
              message: rate limiter temporarily unavailable
  securitySchemes:
    apiKeyClientId:
      type: apiKey
      in: header
      name: X-Client-Id
      description: >-
        Credential client id (`kairos_ck_...`). Must be sent together with
        X-Api-Key and X-Api-Secret.
    apiKeyKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: >-
        64-char hex API key. Must be sent together with X-Client-Id and
        X-Api-Secret.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: >-
        64-char hex API secret. Must be sent together with X-Client-Id and
        X-Api-Key.

````

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