# Kairos Data API (data.kairos.trade) — OpenAPI 3.1 specification.
# Canonical source of truth for this service's public API reference.
#
# Consumers: services/web/scripts/build-openapi.ts publishes it (and the
# other specs in docs/api/) as /openapi/* static assets; the unified docs
# reference at /docs/api-reference renders it with live try-it panels.
#
# When you change this service's public surface, update this file in the
# same PR. Extensions the reference UI renders: x-kairos-auth
# (public | api-key | session), x-kairos-rate-limit, x-kairos-scope.
openapi: 3.1.0
info:
  title: Kairos Data API
  version: 1.0.0
  summary: The full Kairos data plane — markets, candles, trades, search, discover, sports, trader analytics,
    and PnL.
  description: |
    The Kairos Data API at `data.kairos.trade` is the platform's primary data
    plane: market metadata and prices, OHLCV candles, live venue trade proxies,
    normalized trade history, full-text search, discovery feeds, the sports
    catalog, public trader analytics, and account PnL.

    ## Authentication

    Most endpoints accept a Kairos **API key** (`X-Client-Id` + `X-Api-Key` +
    `X-Api-Secret`, all three) or a first-party session JWT. Endpoints tagged
    `public` (trader analytics, provider configs, several sports feeds) need
    no credentials at all. Some endpoints additionally require an API-key
    **scope** (`trade:read`, `position:read`) — stated per operation.

    For high-volume market-data consumption (candles/trades/metadata at
    scale), prefer the dedicated **Market Data API** at `md.kairos.trade` —
    it has higher budgets, ETag caching, and a binary candle format.

    ## Rate limits

    Rate limits use a sliding window keyed on the authenticated user (JWT
    `sub`) or, for anonymous callers, the trusted client IP. Routes belong to
    a named **bucket** (`x-kairos-bucket`) whose per-minute budget is shared
    by every route in it; routes with no bucket run under the service default
    of 100/minute. `x-kairos-rate-limit` states the bucket's compile-time
    default — operators can raise or lower a bucket at runtime, so treat the
    documented number as the baseline, not a contract. 429 responses carry
    `Retry-After` and `X-RateLimit-*`.

    ## Conventions

    - Prices are on the **0–100 cents scale** unless a field says otherwise
      (trader-analytics position/trade prices use the 0–1 scale; each field's
      description states its scale).
    - Errors: `{"detail": "<message>"}` for handler errors; FastAPI's
      standard validation envelope for 422s. Errors surfaced from a venue
      adapter instead use `{"error", "provider", "operation", "message"}` —
      see `DataProviderError`. Unhandled failures always return the generic
      500 body; internal details are logged, never returned.
    - Every request body is capped at 8 MiB service-wide (413).
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/api-reference
  termsOfService: https://kairos.trade/terms
servers:
- url: https://data.kairos.trade
  description: Production
- url: https://staging-data.kairos.trade
  description: Staging
security:
- apiKeyClientId: []
  apiKeyKey: []
  apiKeySecret: []
components:
  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.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 64-char hex API secret.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: First-party Kairos session JWT (web app sessions). Not issued to API consumers.
  schemas:
    DataError:
      type: object
      description: Handler error envelope (FastAPI HTTPException).
      required:
      - detail
      properties:
        detail:
          type: string
          description: Human-readable error message.
          example: Invalid provider
    DataProviderError:
      type: object
      description: |
        Adapter-layer error envelope. Returned instead of `DataError` whenever the failure comes from
        a venue adapter rather than the handler: 400 (validation), 401 (venue auth), 404 (not found),
        429 (venue rate limit), 502 (provider/parse error), 503 (connection error or open circuit).
      required:
      - error
      - message
      properties:
        error:
          type: string
          description: Machine-readable class of failure.
          enum:
          - validation_error
          - authentication_failed
          - not_found
          - rate_limit_exceeded
          - provider_error
          - parse_error
          - service_unavailable
        provider:
          type:
          - string
          - 'null'
          description: Venue the adapter was talking to.
          example: kalshi
        operation:
          type:
          - string
          - 'null'
          description: Adapter operation that failed.
        message:
          type: string
          description: Human-readable error message.
    MarketsActivePageResponse:
      type: object
      description: One page of the active-market listing for a provider, with `provider`/`source`
        stamped on by the API.
      required:
      - exchange_id
      - markets
      - count
      - next_cursor
      - has_more
      - provider
      - source
      properties:
        exchange_id:
          type: string
          description: Lower-cased exchange id the market is listed under.
          example: polymarket
        markets:
          type: array
          items:
            $ref: '#/components/schemas/MarketsActiveMarket'
        count:
          type: integer
          description: Number of markets in this page (`markets.length`).
          example: 100
        next_cursor:
          type:
          - string
          - 'null'
          description: Opaque cursor for the next page, or null if this is the last page.
        has_more:
          type: boolean
        provider:
          type: string
          description: Echo of the validated/lowercased `provider` query param.
          example: polymarket
        source:
          type: string
          description: >-
            Opaque internal origin marker. NOT a stable part of the contract —
            its value may change without notice; do not depend on or switch on it.
    MarketsActiveMarket:
      type: object
      description: One market's cached metadata, including its full upstream `raw`
        payload.
      properties:
        exchange_id:
          type: string
        market_id:
          type: string
        condition_id:
          type: string
        event_id:
          type: string
        title:
          type: string
        neg_risk:
          type: boolean
        tick_size:
          type:
          - number
          - 'null'
        taker_base_fee_bps:
          type:
          - integer
          - 'null'
        fees_enabled:
          type:
          - boolean
          - 'null'
        category:
          type:
          - string
          - 'null'
        group_slug:
          type:
          - string
          - 'null'
        fee_type:
          type:
          - string
          - 'null'
        status:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
        icon:
          type:
          - string
          - 'null'
        end_date:
          type:
          - string
          - 'null'
        open_time:
          type:
          - string
          - 'null'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/MarketsActiveMarketOutcome'
        raw:
          type: object
          additionalProperties: true
          description: Full upstream provider payload as last captured.
        resolution_status:
          type: string
          description: Present only once the market has a resolution status recorded.
        payout_numerators:
          type: array
          items:
            type: integer
          description: Present only once CTF payout numerators are known.
        resolved_ts:
          type: string
        proposed_price:
          type: number
        challenge_window_ends_at:
          type: string
    MarketsActiveMarketOutcome:
      type: object
      properties:
        outcome:
          type: string
          example: 'Yes'
        normalized_outcome:
          type: string
          example: 'yes'
        token_id:
          type: string
        outcome_index:
          type: integer
          description: Present only for exchanges keyed by market_id, index < 2.
        side:
          type: string
          enum:
          - true
          - 'no'
          description: Present only for exchanges keyed by market_id, index < 2.
    MarketsDetailsRequest:
      type: object
      required:
      - markets
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/MarketsDetailsRequestItem'
    MarketsDetailsRequestItem:
      type: object
      required:
      - market_id
      - provider_id
      properties:
        market_id:
          type: string
          description: Matched against market_id OR condition_id OR token_id.
        provider_id:
          type: integer
    MarketsDetailsResponse:
      type: object
      required:
      - markets
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/MarketsDetail'
    MarketsDetail:
      type: object
      required:
      - market_id
      - provider_id
      - event_id
      - name
      - category
      - status
      - condition_id
      - token_id
      - token_ids
      - outcomes
      properties:
        market_id:
          type: string
        provider_id:
          type: integer
        event_id:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        category:
          type:
          - string
          - 'null'
        status:
          type:
          - string
          - 'null'
        condition_id:
          type:
          - string
          - 'null'
        token_id:
          type:
          - string
          - 'null'
        token_ids:
          type: array
          items:
            type: string
          description: Full provider token roster, aligned by index with outcomes.
        outcomes:
          type: array
          items:
            type: string
          description: Outcome labels aligned by index with token_ids.
    MarketsBatchPricesRequest:
      type: object
      required:
      - markets
      properties:
        markets:
          type: array
          maxItems: 300
          items:
            $ref: '#/components/schemas/MarketsBatchPricesRequestItem'
    MarketsBatchPricesRequestItem:
      type: object
      required:
      - market_id
      - provider_id
      properties:
        market_id:
          type: string
        provider_id:
          type: integer
    MarketsBatchPricesResponse:
      type: object
      description: Prices keyed by the requested market_id.
      additionalProperties:
        $ref: '#/components/schemas/MarketsPrice'
    MarketsPrice:
      type: object
      required:
      - price
      properties:
        price:
          type: number
          description: Current venue reference price. Scale is venue-dependent and is not normalized.
        volume:
          type:
          - string
          - 'null'
        liquidity:
          type:
          - number
          - 'null'
    MarketsTickSizeResponse:
      oneOf:
      - $ref: '#/components/schemas/MarketsTickSizeResolved'
      - $ref: '#/components/schemas/MarketsTickSizeUnsupported'
      description: A resolved tick grid (kalshi/polymarket) or predict.fun's explicit unsupported payload
        — distinguish by the presence of `ranges`/`supported`.
    MarketsTickSizeResolved:
      type: object
      required:
      - provider
      - contract_id
      - ranges
      - min_tick
      - source
      - synthetic
      properties:
        provider:
          type: string
          enum:
          - kalshi
          - polymarket
        contract_id:
          type: string
        asset_id:
          type:
          - string
          - 'null'
          description: Echoed back only for polymarket; null for kalshi.
        ranges:
          type: array
          items:
            $ref: '#/components/schemas/MarketsTickRange'
          description: Polymarket always has exactly one range covering [0,1]; Kalshi may have several
            tapered ranges.
        min_tick:
          type: string
          description: Decimal string — the finest step across all ranges.
          example: '0.01'
        source:
          type: string
          enum:
          - metadata_cache
          - polymarket_clob
          - kalshi_price_ranges
          - kalshi_tick_size_deprecated
        synthetic:
          type: boolean
          description: True only for the deprecated Kalshi flat-tick_size fallback (price_ranges absent).
        price_level_structure:
          type:
          - string
          - 'null'
          description: Kalshi's raw price_level_structure field, when present. Always null for polymarket.
    MarketsTickRange:
      type: object
      properties:
        start:
          type: string
          example: '0'
        end:
          type: string
          example: '1'
        step:
          type: string
          example: '0.001'
    MarketsTickSizeUnsupported:
      type: object
      required:
      - provider
      - contract_id
      - supported
      - reason
      properties:
        provider:
          type: string
          enum:
          - predictfun
        contract_id:
          type: string
        supported:
          type: boolean
          enum:
          - false
        reason:
          type: string
          example: predict.fun exposes no per-market tick endpoint or tick change event; treat its tick
            as static/default. See docs/exchange-docs.
    MarketsMetadataResponse:
      type: object
      required:
      - ticker
      - provider
      - name
      - description
      - resolution_rules
      - contract
      - images
      - ctf_neg_risk
      - extra
      - active
      properties:
        ticker:
          type: string
        provider:
          type: string
        name:
          type: string
        description:
          type: string
        resolution_rules:
          $ref: '#/components/schemas/MarketsResolutionRules'
        contract:
          $ref: '#/components/schemas/MarketsContractInfo'
        images:
          $ref: '#/components/schemas/MarketsImages'
        external_url:
          type:
          - string
          - 'null'
        condition_id:
          type:
          - string
          - 'null'
          description: Only populated for some response paths; null when the response comes
            from a provider-API fallback.
        event_id:
          type:
          - string
          - 'null'
          description: Only populated for some response paths.
        ctf_neg_risk:
          type: boolean
          default: false
        extra:
          type: object
          additionalProperties: true
          description: |
            Raw upstream/provider payload, plus router-side enrichment: for polymarket, `clobRewards` ([{rewardsDailyRate}] or []), `rewardsMaxSpread`, `rewardsMinSize`; for predictfun, `rewards.current` (active reward-window object or null).
        active:
          type: boolean
          default: true
          description: False if the market is delisted/resolved and not tradable.
    MarketsResolutionRules:
      type: object
      required:
      - primary
      properties:
        primary:
          type: string
        secondary:
          type:
          - string
          - 'null'
        source:
          type:
          - string
          - 'null'
    MarketsContractInfo:
      type: object
      properties:
        tick_size:
          type:
          - number
          - 'null'
        min_price:
          type:
          - number
          - 'null'
        max_price:
          type:
          - number
          - 'null'
        lot_size:
          type:
          - number
          - 'null'
        quote_currency:
          type:
          - string
          - 'null'
        settlement_ts:
          type:
          - string
          - 'null'
          description: Only populated for some response paths; null otherwise.
        expires_at:
          type:
          - string
          - 'null'
    MarketsImages:
      type: object
      properties:
        icon:
          type:
          - string
          - 'null'
        banner:
          type:
          - string
          - 'null'
    MarketsBatchMetadataRequest:
      type: object
      required:
      - contracts
      properties:
        contracts:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/MarketsBatchMetadataRequestItem'
        titles_only:
          type: boolean
          default: false
          description: When true, skips the API fallback, category inference, and tag-icon enrichment
            passes — returns whatever was already cached directly.
    MarketsBatchMetadataRequestItem:
      type: object
      required:
      - ticker
      - provider
      properties:
        ticker:
          type: string
        provider:
          type: string
    MarketsBatchMetadataResponse:
      type: object
      description: Metadata items keyed by the requested ticker.
      additionalProperties:
        $ref: '#/components/schemas/MarketsBatchMetadataItem'
    MarketsBatchMetadataItem:
      type: object
      required:
      - ticker
      - found
      properties:
        ticker:
          type: string
        found:
          type: boolean
        title:
          type:
          - string
          - 'null'
        event_title:
          type:
          - string
          - 'null'
        provider:
          type:
          - string
          - 'null'
        status:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
        category:
          type:
          - string
          - 'null'
          description: May be backfilled by ticker-pattern/title-keyword inference or by a tag slug when
            the underlying record has none.
        end_date:
          type:
          - string
          - 'null'
        open_time:
          type:
          - string
          - 'null'
        tag_icon:
          type:
          - string
          - 'null'
          description: Highest-precedence active PlatformTag icon for this market.
        yes_sub_title:
          type:
          - string
          - 'null'
          description: Custom binary side label (e.g. Hyperliquid team names). Null falls back to "Yes"
            on the frontend.
        no_sub_title:
          type:
          - string
          - 'null'
        outcome_label:
          type:
          - string
          - 'null'
          description: Short sibling-differentiating label for multi-outcome events (Polymarket groupItemTitle
            / Kalshi yes_sub_title).
        resolved_outcome:
          type:
          - string
          - 'null'
          enum:
          - true
          - 'no'
          - void
          - null
          description: Winning side for already-resolved binary markets; null if unresolved or not surfaced
            by the provider.
        outcome_pair:
          type:
          - array
          - 'null'
          items:
            type: string
          description: The two real outcome labels for a non-Yes/No binary market (moneyline/spread/O-U),
            ordered to match outcome index 0/1.
    MarketsOutcomesResponse:
      type: object
      required:
      - event_title
      - outcomes
      - is_grouped
      - event_groups
      - event_titles
      properties:
        event_title:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
        icon:
          type:
          - string
          - 'null'
        provider:
          type:
          - string
          - 'null'
        category:
          type:
          - string
          - 'null'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/MarketsOutcomeItem'
        is_grouped:
          type: boolean
        matched_market_id:
          type:
          - string
          - 'null'
          description: The outcome entry that corresponds to the queried market_id, if found among the
            returned outcomes.
        event_groups:
          type: object
          additionalProperties:
            type: string
          description: Maps every requested id (market_id plus each all_ids entry, plus every sibling
            id discovered while resolving) to its event_id.
        event_titles:
          type: object
          additionalProperties:
            type: string
          description: Maps every event_id referenced in event_groups to its title.
    MarketsOutcomeItem:
      type: object
      properties:
        market_id:
          type: string
        title:
          type: string
        outcome_label:
          type: string
        price:
          type: number
          description: 0-1 decimal probability (stored cents / 100.0) — NOT the platform's usual 0-100
            cents scale.
          example: 0.62
        volume_total:
          type: number
        volume_24h:
          type: number
        volume_1h:
          type: number
        token_id:
          type:
          - string
          - 'null'
        condition_id:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
          description: Only present when served from the discover cache, not from the direct
            upstream-provider fallback path.
        icon:
          type:
          - string
          - 'null'
        expiration:
          type:
          - string
          - 'null'
    MarketsCryptoResponse:
      type: object
      required:
      - markets
      - window_offset
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/MarketsCryptoMarket'
        window_offset:
          type: integer
    MarketsCryptoMarket:
      type: object
      required:
      - symbol
      - name
      - market_id
      - provider_id
      - is_settled
      properties:
        symbol:
          type: string
          enum:
          - BTC
          - ETH
          - SOL
          - XRP
          - DOGE
          - HYPE
          - BNB
        name:
          type: string
          description: Human-readable market title (Kalshi titles get an appended open/close time range).
        market_id:
          type: string
          description: Kalshi ticker or Polymarket/predict.fun market id.
        token_id:
          type:
          - string
          - 'null'
          description: Polymarket/predict.fun CLOB token id (the "Yes" token where resolvable). Absent
            for Kalshi entries.
        provider_id:
          type: integer
        price:
          type:
          - number
          - 'null'
          description: 0-1 decimal probability (mid of best bid/ask, last trade, or settlement snap to
            0/1 for past windows) — NOT the platform's usual 0-100 cents scale. Null while a market has
            no orderbook yet.
          example: 0.47
        is_settled:
          type: boolean
          description: True for past windows and any market whose settlement/resolution was found; the
            price is then snapped to exactly 0.0 or 1.0.
        is_upcoming:
          type: boolean
          description: Only present on some current-window Kalshi entries — true if the market is in Kalshi's
            "initialized"/"unopened" pre-launch state.
        open_time:
          type:
          - string
          - 'null'
          description: Only present on some current-window Kalshi entries.
    MarketsOracleHistoryResponse:
      type: object
      description: Price-history points keyed by oracle symbol (btc-usd/eth-usd/sol-usd/xrp-usd), each
        array sorted ascending by timestamp.
      additionalProperties:
        type: array
        items:
          $ref: '#/components/schemas/MarketsOraclePricePoint'
    MarketsOraclePricePoint:
      type: object
      properties:
        price:
          type: number
          example: 97123.45
        timestamp:
          type: integer
          description: Epoch milliseconds.
    MarketsPtbResponse:
      type: object
      description: Keyed by window (1m/5m/15m/1h/4h/1d), each value keyed by symbol.
      additionalProperties:
        $ref: '#/components/schemas/MarketsPtbWindowValues'
    MarketsPtbWindowValues:
      type: object
      description: Keyed by oracle symbol (btc-usd/eth-usd/sol-usd/xrp-usd). A symbol is omitted entirely
        if no price could be resolved for this window.
      additionalProperties:
        $ref: '#/components/schemas/MarketsPtbValue'
    MarketsPtbValue:
      type: object
      required:
      - price
      - timestamp_ms
      - game_start_ms
      properties:
        price:
          type: number
          example: 97000.5
        timestamp_ms:
          type: integer
          description: Actual epoch-ms timestamp of the oracle sample used (may differ slightly from game_start_ms).
        game_start_ms:
          type: integer
          description: Epoch-ms start of the requested window (the "price to beat" anchor).
    MarketsEquitySnapshotResponse:
      type: object
      required:
      - prices
      - cached
      properties:
        prices:
          type: object
          description: Keyed by the requested (upper-cased) symbol. Either the full cache hit set or just
            the freshly-fetched subset — see `cached`.
          additionalProperties:
            $ref: '#/components/schemas/MarketsEquityPrice'
        cached:
          type: boolean
          description: True only if every requested symbol was already cached and `prices`
            is that full cached set. False means `prices` contains only symbols that had to
            be freshly fetched for this request.
    MarketsEquityPrice:
      type: object
      properties:
        symbol:
          type: string
          description: The internal (Kairos-side) symbol, which may differ from the upstream ticker.
          example: AAPL
        price:
          type: number
          example: 231.45
        previousClose:
          type:
          - number
          - 'null'
        currency:
          type: string
          default: USD
        marketState:
          type: string
          example: REGULAR
        timestamp:
          type: integer
          description: Epoch milliseconds of the quote (regularMarketTime * 1000).
    CandlesCandle:
      type: object
      description: One OHLCV bar. `token_id` is only present (never emitted as null) when the underlying
        series is keyed by an outcome token rather than a plain contract id.
      required:
      - contract_id
      - timeframe_seconds
      - bucket_start
      - open
      - high
      - low
      - close
      - volume
      properties:
        contract_id:
          type: string
          description: Contract/market identifier this bar belongs to.
          example: KXPRESPOLAND-24-DT
        timeframe_seconds:
          type: integer
          description: Bucket width in seconds.
          example: 60
        bucket_start:
          type: string
          format: date-time
          description: Bucket start time, ISO 8601 UTC.
          example: '2026-07-21T14:32:00+00:00'
        open:
          type: number
          description: Opening price, 0-100 cents scale.
          example: 63.5
        high:
          type: number
          description: High price in the bucket, 0-100 cents scale.
          example: 64.0
        low:
          type: number
          description: Low price in the bucket, 0-100 cents scale.
          example: 63.0
        close:
          type: number
          description: Closing price, 0-100 cents scale.
          example: 63.8
        volume:
          type: integer
          description: Traded size (contracts/shares) within the bucket.
          example: 1250
        token_id:
          type: string
          description: Outcome token identifier, present only when this series is token-scoped (e.g. multi-outcome
            Polymarket markets).
          example: '71321045679252212594626385532706912750332728571942532289631379312455583992563'
    CandlesSeriesResponse:
      type: object
      required:
      - candles
      properties:
        candles:
          type: array
          items:
            $ref: '#/components/schemas/CandlesCandle'
    CandlesBatchRequestItem:
      type: object
      required:
      - provider
      - contract_id
      - timeframe_seconds
      - start
      - end
      properties:
        provider:
          type: string
          example: kalshi
        contract_id:
          type: string
          maxLength: 128
          example: KXPRESPOLAND-24-DT
        timeframe_seconds:
          type: integer
          enum:
          - 1
          - 60
          - 300
          - 900
          - 3600
          - 14400
          - 86400
          example: 60
        start:
          type: string
          format: date-time
          example: '2026-07-21T00:00:00Z'
        end:
          type: string
          format: date-time
          example: '2026-07-22T00:00:00Z'
        outcome:
          type: integer
          minimum: 0
          default: 0
          description: Zero-based outcome index. Omitted/null defaults to 0; negative or non-integer values
            return 400.
        rebuild:
          type: boolean
          default: false
          description: Bypass the cache for this item and force a fresh fetch.
    CandlesBatchRequest:
      type: object
      required:
      - requests
      description: |
        `requests` is the primary field name; the router also accepts a legacy `items` key with the identical array shape as a fallback if `requests` is absent.
      properties:
        requests:
          type: array
          minItems: 1
          maxItems: 200
          items:
            $ref: '#/components/schemas/CandlesBatchRequestItem'
    CandlesBatchResultItem:
      type: object
      required:
      - index
      - candles
      properties:
        index:
          type: integer
          description: Position of this result, matching the index of the corresponding item in the request's
            `requests` array.
          example: 0
        candles:
          type: array
          items:
            $ref: '#/components/schemas/CandlesCandle'
        error:
          type: string
          description: Present only when this specific item failed while other items in the
            batch succeeded from cache. `candles` is `[]` in that case.
          example: Candle batch fetch failed
    CandlesBatchResponse:
      type: object
      required:
      - results
      properties:
        results:
          type: array
          description: Index-ordered, one entry per request item.
          items:
            $ref: '#/components/schemas/CandlesBatchResultItem'
    TradesMetrics:
      type: object
      description: |
        Aggregate volume/pressure metrics. Split by outcome (outcome_0 vs outcome_1), not by buy/sell direction — trade direction is not reliably derivable from every provider's exchange data.
      required:
      - volume_usd
      - outcome_0_volume_usd
      - outcome_1_volume_usd
      - outcome_0_pressure_pct
      - trade_count
      - window_seconds
      - coverage_pct
      - source
      - indexing
      properties:
        volume_usd:
          type: number
          description: Total notional volume in the window, in USD.
          example: 154320.55
        outcome_0_volume_usd:
          type: number
          description: Notional volume attributed to the first/primary outcome (e.g. "yes").
          example: 98210.1
        outcome_1_volume_usd:
          type: number
          description: Notional volume attributed to the second outcome (e.g. "no").
          example: 56110.45
        outcome_0_pressure_pct:
          type: number
          description: Share of total volume_usd attributed to outcome_0, as a percentage (0-100).
          example: 63.65
        trade_count:
          type: integer
          description: Number of trades included in the aggregation.
          example: 842
        window_seconds:
          type: integer
          description: Window width the metrics were computed over, in seconds.
          example: 86400
        coverage_pct:
          type: number
          description: Estimated data coverage for the window as a percentage (0-100); 100 for live upstream
            proxies (Kalshi/Polymarket) and for fully-ingested windows.
          example: 100.0
        source:
          type: string
          description: >-
            Opaque internal marker indicating how the response was produced.
            NOT a stable part of the contract — its value may change without
            notice; do not depend on or switch on it.
        indexing:
          type: boolean
          description: True if an ingestion/backfill job was triggered or is in progress for this window
            (only meaningful when the caller passed `trigger_ingest=true`); coverage may be incomplete
            when true.
          example: false
    TradesTrade:
      type: object
      description: |
        A single normalized trade, as returned by `/trades/history`. Includes legacy `yes_price`/`no_price`/`taker_side` fields derived from `price`/`outcome` for backward compatibility with older consumers.
      required:
      - trade_id
      - order_id
      - contract_id
      - size
      - price
      - outcome
      - timestamp
      - yes_price
      - no_price
      - taker_side
      properties:
        trade_id:
          type: string
          example: kalshi:KXPRESPOLAND-24-DT:9f2a1c
        order_id:
          type: string
          nullable: true
          description: Venue order identifier or hash shared by fills from the same order. Null when the
            source venue does not provide an order identifier. This is not a Kairos order UUID.
          example: 0x9f2a1c...7bd4
        contract_id:
          type: string
          example: KXPRESPOLAND-24-DT
        size:
          type: integer
          description: Traded size (contracts/shares).
          example: 50
        price:
          type: number
          description: Trade price of the traded outcome, 0-100 cents scale.
          example: 63.0
        outcome:
          type: string
          description: Outcome name/index the trade was executed against (e.g. "yes", "no", or a token-based
            outcome label).
          example: 'yes'
        timestamp:
          type: number
          description: Unix timestamp, seconds.
          example: 1753142400.0
        token_id:
          type: string
          description: Outcome token identifier, present only when applicable.
        metadata:
          type: string
          description: Provider-specific JSON blob serialized as a string, present only when available.
        taker_address:
          type: string
          description: Taker wallet address, present only for on-chain venues where it's known.
        maker_address:
          type: string
          description: Counterparty wallet/contract address; may be the canonical exchange contract on
            aggressor-summary rows or a real wallet on maker-fill legs. Present only when known.
        side:
          type: string
          enum:
          - BUY
          - SELL
          description: Trade direction (distinct from `outcome`), present only when known.
        yes_price:
          type: number
          description: 'Legacy field: price expressed on the ''yes'' outcome, 0-100 scale, derived from
            `price`/`outcome`.'
          example: 63.0
        no_price:
          type: number
          description: 'Legacy field: price expressed on the ''no'' outcome, 0-100 scale, derived from
            `price`/`outcome`.'
          example: 37.0
        taker_side:
          type: string
          description: Legacy alias, always equal to `outcome`.
          example: 'yes'
    TradesHistoryResponse:
      type: object
      required:
      - trades
      - has_more
      - source
      - coverage_hours
      - indexing
      properties:
        trades:
          type: array
          items:
            $ref: '#/components/schemas/TradesTrade'
        has_more:
          type: boolean
          description: True if more trades exist before the oldest trade in this response (pagination
            via `before`).
        oldest_available_ts:
          type: number
          nullable: true
          description: Unix timestamp (seconds) of the oldest trade Kairos has ingested for this contract,
            or null if unknown.
          example: 1750000000.0
        source:
          type: string
          description: >-
            Opaque internal marker indicating how the response was served.
            NOT a stable part of the contract — its value may change without
            notice; do not depend on or switch on it.
        coverage_hours:
          type: number
          description: Hours of trade history coverage available/considered for this response.
          example: 24.0
        indexing:
          type: boolean
          description: True if ingestion was triggered/in progress for this window (only meaningful with
            `trigger_ingest=true`).
          example: false
    TradesMetricsResponse:
      type: object
      required:
      - metrics
      properties:
        metrics:
          $ref: '#/components/schemas/TradesMetrics'
    TradesKalshiRawTrade:
      type: object
      description: |
        Raw trade object as returned by Kalshi's `GET /trade-api/v2/markets/trades`, passed through unmodified. Fields shown are the ones Kalshi documents/observed in practice; the API does not restrict or re-type them.
      additionalProperties: true
      properties:
        trade_id:
          type: string
        ticker:
          type: string
        count:
          type: integer
          description: Contracts traded.
        created_time:
          type: string
          format: date-time
        yes_price:
          type: integer
          description: 0-100 cents scale.
        no_price:
          type: integer
          description: 0-100 cents scale.
        taker_side:
          type: string
          enum:
          - true
          - false
    TradesKalshiResponse:
      type: object
      description: |
        The raw Kalshi API response object, spread as-is, with a `metrics` field injected by Kairos. Any other fields Kalshi returns (e.g. a pagination `cursor`) pass through unchanged.
      additionalProperties: true
      required:
      - trades
      - metrics
      properties:
        trades:
          type: array
          items:
            $ref: '#/components/schemas/TradesKalshiRawTrade'
        cursor:
          type: string
          description: Upstream pagination cursor, passed through when present.
        metrics:
          $ref: '#/components/schemas/TradesMetrics'
    TradesPolymarketRawTrade:
      type: object
      description: |
        Raw trade object as returned by Polymarket's `GET https://data-api.polymarket.com/trades`, passed through unmodified.
      additionalProperties: true
      properties:
        proxyWallet:
          type: string
        side:
          type: string
          enum:
          - BUY
          - SELL
        asset:
          type: string
          description: Outcome token id.
        conditionId:
          type: string
        size:
          type: number
        price:
          type: number
          description: Upstream price, 0-1 decimal scale (not the 0-100 scale Kairos normalizes to elsewhere).
        timestamp:
          type: integer
          description: Unix seconds.
        transactionHash:
          type: string
        outcome:
          type: string
    TradesPolymarketResponse:
      type: object
      required:
      - trades
      - metrics
      properties:
        trades:
          type: array
          items:
            $ref: '#/components/schemas/TradesPolymarketRawTrade'
        metrics:
          type: object
          additionalProperties: true
          description: |
            A `TradesMetrics` object in the normal case. When `market` could not be resolved to a condition ID, this is a literal empty object `{}` and `trades` is `[]` — no upstream call was made.
    TraderPnlDataPoint:
      type: object
      description: Single point on a trader's realized-PnL time series.
      required:
      - timestamp
      - realized_pnl
      properties:
        timestamp:
          type: string
          format: date-time
          description: UTC timestamp of this data point.
          example: '2026-07-15T00:00:00Z'
        realized_pnl:
          type: string
          format: decimal
          description: Cumulative realized PnL (USD) at this point in time. Serialized as a decimal string.
          example: '1250.4382'
    TraderPnlHistoryResponse:
      type: object
      description: |
        Time-series realized-PnL data for a wallet over a requested time range, plus a summary of the change over that range.
      required:
      - wallet_address
      - time_range
      - data_points
      - start_pnl
      - end_pnl
      - pnl_change
      - current_realized_pnl
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        time_range:
          type: string
          enum:
          - 1D
          - 1W
          - 1M
          - ALL
          example: 1W
        data_points:
          type: array
          items:
            $ref: '#/components/schemas/TraderPnlDataPoint'
        start_pnl:
          type: string
          format: decimal
          description: Realized PnL at the start of the window.
          example: '800.00'
        end_pnl:
          type: string
          format: decimal
          description: Realized PnL at the end of the window.
          example: '1250.4382'
        pnl_change:
          type: string
          format: decimal
          description: Absolute change in realized PnL over the window (end_pnl - start_pnl).
          example: '450.4382'
        pnl_change_percent:
          type: string
          format: decimal
          nullable: true
          description: Percentage change over the window; null if start_pnl is zero/undefined.
          example: '56.30'
        current_realized_pnl:
          type: string
          format: decimal
          description: Wallet's current total realized PnL (as of now, not bounded to the window).
          example: '1250.4382'
        current_total_pnl:
          type: string
          format: decimal
          nullable: true
          description: Current realized + unrealized PnL combined, when available.
          example: '1875.10'
    TraderPosition:
      type: object
      description: A single current or historical position held by a trader.
      required:
      - market_id
      - token_id
      - outcome
      - size
      - entry_price
      - cost_basis
      - realized_pnl
      - status
      - provider
      properties:
        market_id:
          type: string
          example: 0xabc123condition
        market_name:
          type: string
          nullable: true
          example: Will the Fed cut rates in September?
        icon:
          type: string
          nullable: true
          description: Market icon/image URL.
          example: https://cdn.kairos.trade/markets/abc123.png
        token_id:
          type: string
          example: 10723948572...4
        outcome:
          type: string
          description: Outcome label — YES/NO or a token symbol.
          example: 'YES'
        size:
          type: string
          format: decimal
          description: Position size (>= 0).
          example: '150.0'
        entry_price:
          type: string
          format: decimal
          description: Average entry price (>= 0), 0-1 scale.
          example: '0.42'
        current_price:
          type: string
          format: decimal
          nullable: true
          description: Current mark price (>= 0), 0-1 scale.
          example: '0.55'
        cost_basis:
          type: string
          format: decimal
          description: Total USD cost basis for this position (>= 0).
          example: '63.00'
        unrealized_pnl:
          type: string
          format: decimal
          nullable: true
          example: '19.50'
        realized_pnl:
          type: string
          format: decimal
          description: Realized PnL booked against this position so far.
          example: '0.00'
        status:
          type: string
          enum:
          - OPEN
          - CLOSED
        opened_at:
          type: string
          format: date-time
          nullable: true
        closed_at:
          type: string
          format: date-time
          nullable: true
        provider:
          type: string
          description: Venue the position was traded on.
          default: polymarket
          example: polymarket
    TraderPerformance:
      type: object
      description: Aggregated trader performance metrics, always computed over the full inventory (not
        a paginated page).
      required:
      - wallet_address
      - total_realized_pnl
      - total_unrealized_pnl
      - total_pnl
      - open_positions
      - closed_positions
      - total_positions
      - winning_positions
      - losing_positions
      - win_rate
      - total_volume
      - markets_traded
      - last_updated
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        total_realized_pnl:
          type: string
          format: decimal
          example: '1250.4382'
        total_unrealized_pnl:
          type: string
          format: decimal
          example: '300.00'
        total_pnl:
          type: string
          format: decimal
          example: '1550.4382'
        roi_percent:
          type: string
          format: decimal
          default: '0'
          example: '12.75'
        open_positions:
          type: integer
          minimum: 0
          example: 4
        closed_positions:
          type: integer
          minimum: 0
          example: 27
        total_positions:
          type: integer
          minimum: 0
          example: 31
        positions_value:
          type: string
          format: decimal
          default: '0'
          description: Cost basis (deployed capital) of OPEN positions, summed over the full inventory.
          example: '420.00'
        winning_positions:
          type: integer
          minimum: 0
          example: 18
        losing_positions:
          type: integer
          minimum: 0
          example: 9
        win_rate:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Win rate over closed, resolved positions.
          example: 0.6667
        total_volume:
          type: string
          format: decimal
          minimum: '0'
          example: '18420.55'
        markets_traded:
          type: integer
          minimum: 0
          example: 12
        join_date:
          type: string
          format: date-time
          nullable: true
          description: Polymarket account join date, when known.
        profile_views:
          type: integer
          default: 0
          example: 340
        largest_win:
          type: string
          format: decimal
          nullable: true
          example: '512.30'
        last_updated:
          type: string
          format: date-time
          example: '2026-07-22T14:03:11Z'
    TraderSummaryResponse:
      type: object
      description: |
        A wallet's performance stats alone, the same figures as `performance` on the positions endpoint. `performance` is null for a venue whose stats come only from the full positions build.
      required:
      - wallet_address
      - performance
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        performance:
          nullable: true
          allOf:
          - $ref: '#/components/schemas/TraderPerformance'
    TraderAnalysisWindow:
      type: object
      description: Realized PnL and buys over whole UTC days ending today.
      required:
      - window
      - realized_pnl
      - buy_count
      properties:
        window:
          type: string
          enum: [1D, 7D, 30D, ALL]
        realized_pnl:
          type: number
          description: Realized PnL in USD.
          example: 15.3
        buy_count:
          type: integer
          minimum: 0
          example: 16
    TraderAnalysisDay:
      type: object
      required:
      - day
      - realized_pnl
      properties:
        day:
          type: string
          format: date
          example: '2026-09-28'
        realized_pnl:
          type: number
          description: Realized PnL in USD booked that UTC day.
          example: -25.82
    TraderAnalysisWinLoss:
      type: object
      description: Lifetime closed-position counts. `win_rate` is winning over winning plus losing, 0 when none has closed.
      required:
      - winning_positions
      - losing_positions
      - win_rate
      properties:
        winning_positions:
          type: integer
          minimum: 0
          example: 13
        losing_positions:
          type: integer
          minimum: 0
          example: 11
        win_rate:
          type: number
          minimum: 0
          maximum: 1
          example: 0.5417
    TraderRoiDistribution:
      type: object
      description: Closed positions counted by ROI, the position's realized PnL over the total cost bought for it. Positions with no bought cost are left out.
      required:
      - gt_500
      - between_200_500
      - between_0_200
      - between_neg_50_0
      - lt_neg_50
      properties:
        gt_500:
          type: integer
          minimum: 0
          description: ROI above 500%.
        between_200_500:
          type: integer
          minimum: 0
          description: ROI above 200% up to 500%.
        between_0_200:
          type: integer
          minimum: 0
          description: ROI from 0% to 200%.
        between_neg_50_0:
          type: integer
          minimum: 0
          description: ROI from -50% up to, but not including, 0%.
        lt_neg_50:
          type: integer
          minimum: 0
          description: ROI below -50%.
    TraderAnalysisResponse:
      type: object
      description: |
        A trader profile's analysis panel. `supported` is false, with empty `windows` and `daily` and null `win_loss` and `roi_distribution`, for a venue whose trades do not flow through the lot allocator.
      required:
      - wallet_address
      - supported
      - windows
      - daily
      - win_loss
      - roi_distribution
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        provider:
          type: string
          nullable: true
          example: predictfun
        supported:
          type: boolean
        windows:
          type: array
          items:
            $ref: '#/components/schemas/TraderAnalysisWindow'
        daily:
          type: array
          description: The last 90 UTC days that have activity, oldest first.
          items:
            $ref: '#/components/schemas/TraderAnalysisDay'
        win_loss:
          nullable: true
          allOf:
          - $ref: '#/components/schemas/TraderAnalysisWinLoss'
        roi_distribution:
          nullable: true
          description: Null when unsupported, or when the ROI read failed and the rest of the panel was served without it.
          allOf:
          - $ref: '#/components/schemas/TraderRoiDistribution'
    TraderPositionsResponse:
      type: object
      description: |
        A page of a wallet's positions plus performance stats derived from the full inventory in the same scan.
      required:
      - wallet_address
      - performance
      - open_positions
      - closed_positions
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        performance:
          $ref: '#/components/schemas/TraderPerformance'
        open_positions:
          type: array
          items:
            $ref: '#/components/schemas/TraderPosition'
        closed_positions:
          type: array
          items:
            $ref: '#/components/schemas/TraderPosition'
        has_more:
          type: boolean
          default: false
          description: True when more positions exist beyond this page (open+closed combined, sorted by
            value).
    TraderTradeRecord:
      type: object
      description: A single trade/fill record.
      required:
      - trade_id
      - order_id
      - market_id
      - token_id
      - side
      - size
      - price
      - timestamp
      - provider
      properties:
        trade_id:
          type: string
          example: trd_9f2c3a1b
        order_id:
          type: string
          nullable: true
          description: Parent venue order identifier/hash shared by fills from the same order. For Predict.fun
            this is the venue EIP-712 order hash. Null when the provider or lifecycle row has no authoritative
            venue order identifier. This is not a Kairos order UUID.
          example: 0x9f2a1c...7bd4
        market_id:
          type: string
          example: 0xabc123condition
        market_name:
          type: string
          nullable: true
          example: Will the Fed cut rates in September?
        icon:
          type: string
          nullable: true
          example: https://cdn.kairos.trade/markets/abc123.png
        token_id:
          type: string
          example: 10723948572...4
        outcome:
          type: string
          nullable: true
          example: 'YES'
        side:
          type: string
          enum:
          - BUY
          - SELL
        size:
          type: string
          format: decimal
          description: Trade size (>= 0).
          example: '25.0'
        price:
          type: string
          format: decimal
          description: Fill price (>= 0), 0-1 scale.
          example: '0.47'
        timestamp:
          type: string
          format: date-time
          example: '2026-07-20T09:14:02Z'
        tx_hash:
          type: string
          nullable: true
          example: 0xfeedface...beef
        realized_pnl:
          type: string
          format: decimal
          nullable: true
          description: Realized PnL booked by this fill (populated for SELLs from the FIFO ledger; null/0
            for BUYs).
          example: '3.75'
        provider:
          type: string
          default: polymarket
          example: polymarket
    TraderRecentTradesResponse:
      type: object
      description: |
        A standalone page of trade history, served independently of the full profile build.
      required:
      - wallet_address
      - trades
      properties:
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        trades:
          type: array
          items:
            $ref: '#/components/schemas/TraderTradeRecord'
        has_more:
          type: boolean
          default: false
    TraderTopHolder:
      type: object
      description: A single holder of a market's outcome token.
      required:
      - proxyWallet
      - amount
      - outcomeIndex
      - displayUsernamePublic
      - verified
      properties:
        proxyWallet:
          type: string
          description: Holder's wallet address.
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        amount:
          type: number
          format: double
          description: Token amount held.
          example: 15230.5
        outcomeIndex:
          type: integer
          description: Index of the outcome token held (0 = first outcome, 1 = second, ...).
          example: 0
        displayUsernamePublic:
          type: boolean
          description: Whether the holder has opted to publicly display their username.
        verified:
          type: boolean
          description: Whether the holder has a verified badge.
        name:
          type: string
          description: Present only when the provider returned a display name.
          example: whale_trader_99
        pseudonym:
          type: string
          example: SilentOwl-4821
        bio:
          type: string
          example: Prediction market degen.
        asset:
          type: string
          description: Underlying asset identifier for the held token, when the provider supplies one.
        profileImage:
          type: string
          example: https://cdn.polymarket.com/avatars/abc.png
        profileImageOptimized:
          type: string
          example: https://cdn.polymarket.com/avatars/abc-opt.png
    TraderTopHoldersToken:
      type: object
      description: Top holders for a single outcome token.
      required:
      - token
      - holders
      properties:
        token:
          type: string
          description: Outcome token id.
          example: 10723948572...4
        holders:
          type: array
          items:
            $ref: '#/components/schemas/TraderTopHolder'
    TraderTopHoldersResponse:
      type: array
      description: |
        Top holders grouped by outcome token, one entry per requested market/token combination returned by the provider. The endpoint returns this array directly as the response body (not wrapped in an object).
      items:
        $ref: '#/components/schemas/TraderTopHoldersToken'
    TraderSearchUserInfo:
      type: object
      description: An associated Polymarket user record linked to a profile (e.g. multi-role accounts).
      required:
      - id
      - creator
      - mod
      properties:
        id:
          type: string
          example: usr_polymarket_884211
        creator:
          type: boolean
          description: Whether this user record has market-creator privileges.
        mod:
          type: boolean
          description: Whether this user record has moderator privileges.
    TraderSearchProfile:
      type: object
      description: Public trader profile fields returned by the upstream provider.
      required:
      - displayUsernamePublic
      - verifiedBadge
      - users
      properties:
        proxyWallet:
          type: string
          nullable: true
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        name:
          type: string
          nullable: true
          example: Jane Trader
        pseudonym:
          type: string
          nullable: true
          example: QuietFalcon-2201
        bio:
          type: string
          nullable: true
          example: Macro and elections.
        profileImage:
          type: string
          nullable: true
          example: https://cdn.polymarket.com/avatars/jane.png
        xUsername:
          type: string
          nullable: true
          example: janetrades
        verifiedBadge:
          type: boolean
        displayUsernamePublic:
          type: boolean
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: '2023-11-02T18:20:44Z'
        users:
          type: array
          items:
            $ref: '#/components/schemas/TraderSearchUserInfo'
    TraderSearchResponse:
      type: object
      description: |
        Result of a trader-profile search. `profile` is null (with `error` populated) when the provider found no profile for the address.
      required:
      - provider
      - address
      - profile
      - error
      properties:
        provider:
          type: string
          example: polymarket
        address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        profile:
          allOf:
          - $ref: '#/components/schemas/TraderSearchProfile'
          nullable: true
        error:
          type: string
          nullable: true
          description: Provider-supplied error message when no profile was found.
          example: null
    PnlProviderInfo:
      type: object
      description: A PnL provider available through the merged-PnL pipeline.
      required:
      - name
      - description
      properties:
        name:
          type: string
          example: polymarket
        description:
          type: string
          example: Polymarket prediction market PnL
    PnlExchangeSummary:
      type: object
      description: Per-exchange PnL summary within a merged PnL response.
      required:
      - provider
      - wallet_address
      - total_realized_pnl
      - total_fees
      - total_cost_basis
      - winning_positions
      - losing_positions
      properties:
        provider:
          type: string
          example: polymarket
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        total_realized_pnl:
          type: number
          format: double
          example: 1250.4382
        total_fees:
          type: number
          format: double
          example: 12.5
        total_cost_basis:
          type: number
          format: double
          example: 8400.0
        winning_positions:
          type: integer
          example: 18
        losing_positions:
          type: integer
          example: 9
    PnlSummary:
      type: object
      description: Merged PnL summary across all providers/wallets supplied for a user.
      required:
      - user_id
      - total_realized_pnl
      - total_fees
      - total_cost_basis
      - total_winning_positions
      - total_losing_positions
      - by_exchange
      properties:
        user_id:
          type: string
          example: usr_9f3c2a1b
        total_realized_pnl:
          type: number
          format: double
          example: 1875.1
        total_fees:
          type: number
          format: double
          example: 18.2
        total_cost_basis:
          type: number
          format: double
          example: 12400.0
        total_winning_positions:
          type: integer
          example: 25
        total_losing_positions:
          type: integer
          example: 11
        by_exchange:
          type: object
          description: Keyed by provider name.
          additionalProperties:
            $ref: '#/components/schemas/PnlExchangeSummary'
    PnlRecord:
      type: object
      description: A single merged PnL record.
      required:
      - provider
      - market_id
      - timestamp
      - realized_pnl
      - fees
      properties:
        provider:
          type: string
          example: polymarket
        market_id:
          type: string
          example: 0xabc123condition
        timestamp:
          type: string
          description: ISO-8601 timestamp string (empty string if the underlying record has no timestamp).
          example: '2026-07-20T09:14:02+00:00'
        realized_pnl:
          type: number
          format: double
          example: 24.15
        fees:
          type: number
          format: double
          example: 0.35
    PnlUserPnlResponse:
      type: object
      description: |
        Full merged PnL response for a user, across the requested wallets/providers.
      required:
      - user_id
      - summary
      - records
      - filters_applied
      properties:
        user_id:
          type: string
          example: usr_9f3c2a1b
        summary:
          $ref: '#/components/schemas/PnlSummary'
        records:
          type: array
          items:
            $ref: '#/components/schemas/PnlRecord'
        filters_applied:
          type: object
          description: Echoes the effective filters used to build this response (wallets, providers, etc.).
          additionalProperties: true
        total:
          type: integer
          default: 0
          description: |
            Count of merged records the aggregator returned before pagination slicing, bounded by the per-provider fetch ceiling (min(limit+offset, 1000)) — not an authoritative full-history count.
          example: 143
        has_more:
          type: boolean
          default: false
          description: True only when the aggregator definitively saw more records than this page returned.
    PnlWalletMarketTokenPnL:
      type: object
      description: Per-token PnL row within a wallet/market hover payload.
      required:
      - token_id
      - outcome
      - balance
      - avg_entry_price
      - cost_basis_usd
      - realized_pnl_usd
      - mark_price
      - position_value_usd
      - unrealized_pnl_usd
      properties:
        token_id:
          type: string
          example: 10723948572...4
        outcome:
          type: string
          example: 'YES'
        balance:
          type: number
          format: double
          example: 150.0
        avg_entry_price:
          type: number
          format: double
          example: 0.42
        cost_basis_usd:
          type: number
          format: double
          example: 63.0
        realized_pnl_usd:
          type: number
          format: double
          example: 0.0
        mark_price:
          type: number
          format: double
          example: 0.55
        position_value_usd:
          type: number
          format: double
          example: 82.5
        unrealized_pnl_usd:
          type: number
          format: double
          example: 19.5
    PnlWalletMarketPnlResponse:
      type: object
      description: |
        Live hover-on-wallet PnL for a single (wallet, market) pair. Returns a zeroed/empty payload (`tokens: []`) instead of erroring when the underlying PnL pipeline is disabled.
      required:
      - provider_id
      - wallet_address
      - contract_id
      - tokens
      - total_unrealized_pnl_usd
      - total_realized_pnl_usd
      - total_position_value_usd
      properties:
        provider_id:
          type: string
          enum:
          - polymarket
          - opinion
          - predictfun
          example: polymarket
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        contract_id:
          type: string
          example: 0xabc123condition
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/PnlWalletMarketTokenPnL'
        total_unrealized_pnl_usd:
          type: number
          format: double
          example: 19.5
        total_realized_pnl_usd:
          type: number
          format: double
          example: 0.0
        total_position_value_usd:
          type: number
          format: double
          example: 82.5
    PnlWalletTotalsResponse:
      type: object
      description: |
        Wallet-wide PnL totals across all markets, refreshed hourly. Returns a zeroed payload (`market_count: 0`, `token_count: 0`) instead of erroring when the underlying PnL pipeline is disabled.
      required:
      - provider_id
      - wallet_address
      - total_unrealized_pnl_usd
      - total_realized_pnl_usd
      - total_position_value_usd
      - total_cost_basis_usd
      - market_count
      - token_count
      - snapshot_ts
      properties:
        provider_id:
          type: string
          enum:
          - polymarket
          - opinion
          - predictfun
          example: polymarket
        wallet_address:
          type: string
          example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
        total_unrealized_pnl_usd:
          type: number
          format: double
          example: 340.2
        total_realized_pnl_usd:
          type: number
          format: double
          example: 1250.4382
        total_position_value_usd:
          type: number
          format: double
          example: 980.0
        total_cost_basis_usd:
          type: number
          format: double
          example: 640.0
        market_count:
          type: integer
          example: 12
        token_count:
          type: integer
          example: 19
        snapshot_ts:
          type: string
          format: date-time
          description: ISO-8601 timestamp of the hourly snapshot this data was read from (or the current
            time when native_pnl_pipeline is disabled).
          example: '2026-07-22T13:00:00Z'
    SearchBadRequestError:
      type: object
      description: FastAPI `HTTPException(400)` body shape used across the search router.
      required:
      - detail
      properties:
        detail:
          type: string
          example: 'Invalid provider: acme'
    DiscoverBadRequestError:
      type: object
      description: FastAPI `HTTPException(400)` body shape used across the discover router.
      required:
      - detail
      properties:
        detail:
          type: string
          example: 'Invalid sort_by: bogus. Must be one of: [''liquidity'', ''newest'', ''price'', ''rewards'',
            ''volume'', ''volume_1h'', ''volume_24h'']'
    ProvidersNotFoundError:
      type: object
      required:
      - detail
      properties:
        detail:
          type: string
          example: Provider 'acme' not found
    SearchResult:
      type: object
      description: One market or event row from search results, post provider-visibility
        filtering.
      required:
      - market_id
      - provider_id
      - provider
      - relevance_score
      properties:
        market_id:
          type: string
          example: KXPRES-28-DJT
        provider_id:
          type: integer
          description: Numeric provider id (1=kalshi, 2=polymarket, 3=opinion, 8=predictfun).
          example: 1
        provider:
          type: string
          example: kalshi
        event_id:
          type: string
          nullable: true
        event_name:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
          description: Market title.
        symbol:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
          example: open
        expires_at:
          type: string
          format: date-time
          nullable: true
        relevance_score:
          type: number
          description: Composite final score = text_score + business_score - penalty.
        volume_24h:
          type: number
          default: 0
        outcome_label:
          type: string
          nullable: true
        series_key:
          type: string
          nullable: true
          description: Polymarket seriesSlug or Kalshi series_ticker.
        series_title:
          type: string
          nullable: true
        text_score:
          type: number
          default: 0
        business_score:
          type: number
          default: 0
        penalty:
          type: number
          default: 0
          minimum: 0
        price:
          type: number
          nullable: true
        volume_1h:
          type: number
          default: 0
        liquidity:
          type: number
          default: 0
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        token_id:
          type: string
          nullable: true
          description: First outcome's on-chain token id (legacy singular field). For
            a market with more than one outcome use `token_ids`/`outcomes` instead.
        condition_id:
          type: string
          nullable: true
          description: Venue condition id (Polymarket 0x… condition hash), when the
            venue has one.
        token_ids:
          type: array
          items:
            type: string
          description: Full on-chain outcome token roster, aligned with `outcomes`.
            Feed these to `/v1/synthetics` legs and `/v1/candles` without a second
            resolve call. Empty when the market has no on-chain tokens.
        outcomes:
          type: array
          items:
            type: string
          description: Outcome labels aligned positionally with `token_ids`.
        type:
          type: string
          enum:
          - event
          nullable: true
          description: Only present (and only ever "event") on rows returned by /search/markets-and-events
            when type=event or both.
    SearchMeta:
      type: object
      properties:
        query:
          type: string
        returned:
          type: integer
          description: Number of rows in `results` after provider-visibility filtering.
        requested:
          type: integer
          description: The `limit` that was requested.
        query_time_ms:
          type: number
        include_expired:
          type: boolean
        provider_id:
          type: string
          nullable: true
          description: Resolved/validated provider filter, or null if none.
    SearchMarketsResponse:
      type: object
      required:
      - results
      - meta
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'
        meta:
          $ref: '#/components/schemas/SearchMeta'
    SearchMarketsAndEventsMeta:
      type: object
      properties:
        query:
          type: string
        returned:
          type: integer
        requested:
          type: integer
          description: The `limit` that was requested.
        search_type:
          type: string
          enum:
          - market
          - event
          - both
        include_expired:
          type: boolean
        provider_id:
          type: string
          nullable: true
    SearchMarketsAndEventsResponse:
      type: object
      required:
      - results
      - meta
      properties:
        results:
          type: array
          description: Merged market + event rows, sorted by relevance_score descending, truncated to
            limit.
          items:
            $ref: '#/components/schemas/SearchResult'
        meta:
          $ref: '#/components/schemas/SearchMarketsAndEventsMeta'
    SearchEventGroup:
      type: object
      description: One event's outcome markets, grouped server-side.
      required:
      - event_id
      - market_count
      - markets
      properties:
        event_id:
          type: string
        event_name:
          type: string
          nullable: true
        market_count:
          type: integer
          description: Count of markets kept after provider-visibility filtering.
        representative:
          allOf:
          - $ref: '#/components/schemas/SearchResult'
          nullable: true
          description: Falls back to the first remaining market if the original representative's provider
            was filtered out.
        markets:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'
    SearchOutcomeRef:
      type: object
      description: One outcome within a classified MarketGroup (interactive ladder).
      properties:
        id:
          type: string
        provider:
          type: string
        title:
          type: string
          nullable: true
        outcomeLabel:
          type: string
          nullable: true
        tokenId:
          type: string
          nullable: true
        price:
          type: number
          nullable: true
        op:
          type: string
          nullable: true
          description: Comparison operator this outcome represents on the group's axis, e.g. '>=', '<'.
        value:
          type: number
          nullable: true
          description: Threshold value on the group's axis.
        hi:
          type: number
          nullable: true
          description: Upper bound, for range-shaped outcomes.
    SearchMarketGroup:
      type: object
      description: An "interactive ladder" grouping of related markets (e.g. a set of over/under threshold
        markets on one axis), pre-computed by a background job and served from cache at search
        time.
      required:
      - type
      - groupKey
      - provider
      - outcomes
      properties:
        type:
          type: string
          enum:
          - group
        groupKey:
          type: string
        provider:
          type: string
        title:
          type: string
          nullable: true
        kind:
          type: string
          nullable: true
          description: Classification kind, e.g. threshold ladder.
        axisLabel:
          type: string
          nullable: true
        unit:
          type: string
          nullable: true
        direction:
          type: string
          nullable: true
        confidence:
          type: number
          description: Rounded to 3 decimal places.
        marketCount:
          type: integer
        category:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        eventId:
          type: string
          nullable: true
        eventTitle:
          type: string
          nullable: true
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/SearchOutcomeRef'
    SearchCorrelationCounterpart:
      type: object
      description: A cross-venue market judged similar (similarity at or above a threshold) to the
        keyed result market_id. Enrichment fields (title/ticker/image/icon/expires_at) are only present
        when the counterpart market's metadata is available; otherwise they're absent.
      required:
      - provider_id
      - market_id
      - similarity
      - method
      properties:
        provider_id:
          type: integer
        market_id:
          type: string
        similarity:
          type: number
          minimum: 0
          maximum: 1
        method:
          type: string
          description: How the correlation was derived, e.g. embedding/manual.
        title:
          type: string
        ticker:
          type: string
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
    SearchSimpleMeta:
      type: object
      properties:
        query:
          type: string
        returned:
          type: integer
          description: Count of results after provider-visibility filtering.
        query_time_ms:
          type: number
        total:
          type: integer
          nullable: true
          description: 'Present only when include_total=true. Best-effort: the upstream total if no rows
            were filtered by visibility, else the filtered count.'
    SearchSimpleResponse:
      type: object
      description: Shared envelope for /search/simple and /search/resolve-url. Both always include results/meta;
        groups/singles appear only when the underlying search was grouped (always true for /search/simple
        with group_results=true, and for /search/resolve-url on an event URL). classifiedGroups/absorbedMarketIds
        are only ever attached by /search/simple (group_results=true); /search/resolve-url never sets
        them. correlations is attached by both, best-effort, and absent if no counterpart markets were
        found.
      required:
      - results
      - meta
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'
        groups:
          type: array
          items:
            $ref: '#/components/schemas/SearchEventGroup'
        singles:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'
        meta:
          $ref: '#/components/schemas/SearchSimpleMeta'
        classifiedGroups:
          type: array
          items:
            $ref: '#/components/schemas/SearchMarketGroup'
        absorbedMarketIds:
          type: array
          items:
            type: string
          description: Market/outcome ids already rendered inside classifiedGroups — the overlay should
            not also render them as standalone rows.
        correlations:
          type: object
          description: Map of result market_id -> up to 4 similar cross-venue counterpart markets, sorted
            by similarity descending.
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/SearchCorrelationCounterpart'
    SearchSuggestion:
      type: object
      required:
      - text
      - type
      - frequency
      properties:
        text:
          type: string
          description: Suggested query text (market/series/category name).
        type:
          type: string
          description: Suggestion source type, e.g. market, category.
        frequency:
          type: integer
          description: May be an aggregated count, or always 1, depending on how the suggestion
            was generated.
    SearchSuggestResponse:
      type: object
      required:
      - suggestions
      - meta
      properties:
        suggestions:
          type: array
          items:
            $ref: '#/components/schemas/SearchSuggestion'
        meta:
          type: object
          properties:
            query:
              type: string
            returned:
              type: integer
    ScreenerMarket:
      type: object
      description: One screener hit — the market, and the outcome the screen matched on. Prices and
        spread are cents; money is dollars.
      required:
      - id
      - marketId
      - provider
      properties:
        id:
          type: string
          description: 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:
          type: string
          nullable: true
          description: 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:
          type: string
        title:
          type: string
          nullable: true
          description: 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:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
        hasMetadata:
          type: boolean
          description: False when only live numbers are known and the title is the id.
        outcomeIndex:
          type: integer
          nullable: true
          description: The outcome the quote figures below describe.
        matchedLeg:
          type: boolean
          description: True when the screen picked this outcome. False when the screen was contract-level
            only and this is just the primary outcome.
        outcomeName:
          type: string
          nullable: true
        bid:
          type: number
          nullable: true
          description: Cents.
        ask:
          type: number
          nullable: true
          description: Cents.
        mid:
          type: number
          nullable: true
          description: Cents.
        spread:
          type: number
          nullable: true
          description: Cents wide on this outcome; null when it is not quoting both sides.
        price:
          type: number
          description: Cents; the record's headline price.
        volume24h:
          type: number
          description: Dollars.
        volume1h:
          type: number
          description: Dollars.
        liquidity:
          type: number
          description: Dollars resting across every outcome, both sides.
        outcomeLiquidity:
          type: number
          nullable: true
          description: Dollars resting on this outcome alone, both sides.
        bookAgeSeconds:
          type: number
          nullable: true
          description: 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:
          type: number
          description: Contracts.
        openInterestNotional:
          type: number
          description: Dollars.
        updatedAt:
          type: string
          format: date-time
    ScreenerResponse:
      type: object
      required:
      - markets
      - total
      - available
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/ScreenerMarket'
        total:
          type: integer
          description: 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:
          type: boolean
          description: 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:
          type: string
          description: How the scan ran (`index_ordered`, `index_range`, `scan`). Diagnostic.
        limit:
          type: integer
        offset:
          type: integer
    ScreenerPreset:
      type: object
      required:
      - id
      - label
      - description
      - params
      properties:
        id:
          type: string
        label:
          type: string
        description:
          type: string
        params:
          type: object
          description: Query parameters for /search/screener, including `sort`.
          additionalProperties: true
    ScreenerPresetsResponse:
      type: object
      required:
      - presets
      - sorts
      properties:
        presets:
          type: array
          items:
            $ref: '#/components/schemas/ScreenerPreset'
        sorts:
          type: array
          items:
            type: string
    DiscoverMarketOutcome:
      type: object
      description: One outcome market inside a grouped (isGrouped=true) DiscoverMarket, as returned by
        /api/markets/discover/v2.
      properties:
        id:
          type: string
        ticker:
          type: string
        title:
          type: string
        outcomeLabel:
          type: string
        price:
          type: number
          description: Cents/100.
        volume:
          type: number
        volumeTotal:
          type: number
          description: Same value as volume.
        volume24h:
          type: number
        volume1h:
          type: number
          description: Dollars traded in the last hour, from our trade tape (or the venue's
            own hourly figure where it publishes one). 0 where neither reports an hour —
            that is "not measured", not "no trades". Rank on volume1hRank instead.
        volume1hRank:
          type: number
          description: Ranking score for the hourly dimension — the hour above where it is
            known, the 24h volume spread over its hours where it is not. Never display it.
        token_id:
          type: string
          nullable: true
        condition_id:
          type: string
          nullable: true
        rewardRate:
          type: number
    DiscoverMarket:
      type: object
      description: One row of /api/markets/discover/v2's `markets` array. When isGrouped=true this represents
        an event with its outcomes nested (eventTitle + outcomes populated, outcomeLabel/yes_sub_title/
        no_sub_title/token_id/condition_id absent at the top level); when isGrouped=false it's a single
        standalone market (outcomeLabel/ yes_sub_title/no_sub_title/token_id/condition_id populated, eventTitle/outcomes
        absent).
      required:
      - id
      - ticker
      - title
      - price
      - provider
      - status
      - isGrouped
      properties:
        id:
          type: string
        ticker:
          type: string
        event_id:
          type: string
          nullable: true
        eventTitle:
          type: string
          description: Grouped rows only.
        title:
          type: string
        outcomeLabel:
          type: string
          nullable: true
          description: Single (isGrouped=false) rows only.
        yes_sub_title:
          type: string
          nullable: true
          description: Single rows only.
        no_sub_title:
          type: string
          nullable: true
          description: Single rows only.
        price:
          type: number
          description: Cents/100.
        volume:
          type: number
        volume24h:
          type: number
        volume1h:
          type: number
          description: Dollars traded in the last hour, from our trade tape (or the venue's
            own hourly figure where it publishes one). 0 where neither reports an hour —
            that is "not measured", not "no trades". Rank on volume1hRank instead.
        volume1hRank:
          type: number
          description: Ranking score for the hourly dimension — the hour above where it is
            known, the 24h volume spread over its hours where it is not. Never display it.
        liquidity:
          type: number
        rewardRate:
          type: number
        provider:
          type: string
        status:
          type: string
        expiration:
          type: string
          format: date-time
          nullable: true
        token_id:
          type: string
          nullable: true
          description: Single rows only.
        condition_id:
          type: string
          nullable: true
          description: Single rows only.
        category:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        isGrouped:
          type: boolean
        outcomes:
          type: array
          description: Grouped rows only.
          items:
            $ref: '#/components/schemas/DiscoverMarketOutcome'
    DiscoverV2Response:
      type: object
      required:
      - markets
      - total
      - offset
      - limit
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverMarket'
        total:
          type: integer
        offset:
          type: integer
        limit:
          type: integer
        timestamp:
          type: string
          description: ISO timestamp of the underlying cache build; empty string when the response
            falls back to a live query, or on the empty-filter short-circuit path.
    DiscoverSearchIndexOutcome:
      type: object
      description: One outcome market inside a grouped search-index entry. Distinct field set from DiscoverMarketOutcome
        (no `volume`/`condition_id`; adds `token_id`).
      properties:
        id:
          type: string
        title:
          type: string
        ticker:
          type: string
        price:
          type: number
        volume1h:
          type: number
        volume24h:
          type: number
        volumeTotal:
          type: number
        outcomeLabel:
          type: string
        token_id:
          type: string
          nullable: true
        rewardRate:
          type: number
    DiscoverSearchIndexMarket:
      type: object
      description: One row of /api/markets/discover/v2/search-index's `markets` array. Same grouped/single
        split as DiscoverMarket but this shape omits `status`/`event_id` and always includes `token_id`
        at the top level.
      required:
      - id
      - ticker
      - title
      - price
      - provider
      - isGrouped
      properties:
        id:
          type: string
        ticker:
          type: string
        title:
          type: string
        eventTitle:
          type: string
          description: Grouped rows only.
        yes_sub_title:
          type: string
          nullable: true
          description: Single rows only.
        no_sub_title:
          type: string
          nullable: true
          description: Single rows only.
        price:
          type: number
        volume:
          type: number
        volume24h:
          type: number
        volume1h:
          type: number
        liquidity:
          type: number
        rewardRate:
          type: number
        provider:
          type: string
        expiration:
          type: string
          format: date-time
          nullable: true
        category:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        isGrouped:
          type: boolean
        token_id:
          type: string
          nullable: true
        outcomes:
          type: array
          description: Grouped rows only.
          items:
            $ref: '#/components/schemas/DiscoverSearchIndexOutcome'
    DiscoverSearchIndexResponse:
      type: object
      required:
      - markets
      - total
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverSearchIndexMarket'
        total:
          type: integer
        timestamp:
          type: string
    DiscoverTickerMarket:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        provider:
          type: string
        price:
          type: number
        volume1h:
          type: number
        token_id:
          type: string
          nullable: true
    DiscoverTickerResponse:
      type: object
      required:
      - markets
      - count
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverTickerMarket'
        count:
          type: integer
        timestamp:
          type: string
    DiscoverBreakingMarket:
      type: object
      properties:
        id:
          type: string
        ticker:
          type: string
        title:
          type: string
        provider:
          type: string
        price:
          type: number
        volume1h:
          type: number
        priceChange24hSigned:
          type: number
          description: Signed percentage/point change over 24h.
        token_id:
          type: string
          nullable: true
        condition_id:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
    DiscoverBreakingResponse:
      type: object
      required:
      - markets
      - count
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverBreakingMarket'
        count:
          type: integer
        timestamp:
          type: string
    DiscoverExpiringMarket:
      type: object
      properties:
        id:
          type: string
        ticker:
          type: string
        title:
          type: string
        provider:
          type: string
        price:
          type: number
        volume1h:
          type: number
        volume24h:
          type: number
        volume:
          type: number
        expiration:
          type: string
          format: date-time
        token_id:
          type: string
          nullable: true
        condition_id:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        isGrouped:
          type: boolean
          enum:
          - false
    DiscoverExpiringResponse:
      type: object
      required:
      - markets
      - count
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverExpiringMarket'
        count:
          type: integer
        timestamp:
          type: string
    DiscoverSubcategory:
      type: object
      required:
      - name
      - slug
      - count
      properties:
        name:
          type: string
        slug:
          type: string
        count:
          type: integer
          description: Approximate active-market count (topic ∩ subtopic ∩ active-ranked-set); may run
            slightly higher than the post-grouping total shown on the cards page.
    DiscoverSubcategoriesResponse:
      type: object
      required:
      - category
      - subcategories
      properties:
        category:
          type: string
        subcategories:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverSubcategory'
          description: Sorted by count descending; zero-count subtopics are omitted.
    DiscoverKalshiSportsEvent:
      type: object
      description: A cached Kalshi sports event object written by a background job. Only the fields
        this router reads are typed; the object may carry additional passthrough fields.
      properties:
        event_ticker:
          type: string
        total_volume:
          type: number
        market_count:
          type: integer
      additionalProperties: true
    DiscoverKalshiMatchedGame:
      type: object
      required:
      - away
      - home
      - league
      - kalshi_events
      - total_volume
      - total_markets
      properties:
        away:
          type: string
          description: Uppercased team code.
        home:
          type: string
          description: Uppercased team code.
        league:
          type: string
          description: Lowercased league code
          or empty string if not supplied.: null
        kalshi_events:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverKalshiSportsEvent'
          description: Sorted by total_volume descending.
        total_volume:
          type: number
        total_markets:
          type: integer
    DiscoverKalshiLiveResponse:
      type: object
      required:
      - events
      - matched
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverKalshiSportsEvent'
          description: All cached events (unfiltered).
        matched:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverKalshiMatchedGame'
          description: Empty unless `games` was supplied and matched at least one event.
    DiscoverTrendingMarket:
      type: object
      properties:
        id:
          type: string
        ticker:
          type: string
          description: Same value as id.
        title:
          type: string
        price:
          type: number
        volume:
          type: number
          description: Total volume (TrendingMarket.volume_total).
        volume1h:
          type: number
        provider:
          type: string
        outcomeLabel:
          type: string
          nullable: true
        event_id:
          type: string
          nullable: true
    DiscoverTrendingResponse:
      type: object
      required:
      - markets
      - count
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverTrendingMarket'
        count:
          type: integer
        timestamp:
          type: string
    ProvidersConfig:
      type: object
      description: Exchange-agnostic provider configuration.
      required:
      - id
      - display_name
      - icon_url
      - chain_id
      - chain_name
      - is_active
      - supported_order_types
      - supports_walk_the_book
      - supports_token_approval
      - has_multi_token_markets
      - auth_flow_type
      properties:
        id:
          type: string
          example: kalshi
          description: '''kalshi'' | ''polymarket'' | ''predictfun'' | ''hyperliquid'' | ...'
        display_name:
          type: string
          example: Kalshi
        icon_url:
          type: string
        chain_id:
          type: string
        chain_name:
          type: string
        is_active:
          type: boolean
        supported_order_types:
          type: array
          items:
            type: string
          example:
          - limit
          - market
        supports_walk_the_book:
          type: boolean
        supports_token_approval:
          type: boolean
        has_multi_token_markets:
          type: boolean
        auth_flow_type:
          type: string
          enum:
          - none
          - wallet_signature
          - api_key
        token_id_format:
          type: string
          enum:
          - opaque
          - clob_uint256
          default: opaque
        metadata_key_type:
          type: string
          enum:
          - marketId
          - conditionId
          default: marketId
        numeric_id:
          type: integer
          nullable: true
        primary_color:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
    ProvidersConfigsResponse:
      type: object
      required:
      - configs
      - meta
      properties:
        configs:
          type: array
          items:
            $ref: '#/components/schemas/ProvidersConfig'
        meta:
          type: object
          required:
          - count
          properties:
            count:
              type: integer
    ProvidersAPIAccessResponse:
      type: object
      required:
      - providers
      - meta
      properties:
        providers:
          type: array
          description: Active provider ids whose API-key access is enabled.
          items:
            type: string
          example:
          - hyperliquid
          - kalshi
          - polymarket
        meta:
          type: object
          required:
          - count
          properties:
            count:
              type: integer
              example: 3
    SportsTrendingMatchedResponse:
      type: object
      description: Top matched sports markets ranked by live Kalshi volume.
      required:
      - matches
      - count
      properties:
        matches:
          type: array
          items:
            $ref: '#/components/schemas/SportsTrendingMatch'
        count:
          type: integer
          description: Number of items in matches (after truncation to limit).
          example: 5
    SportsTrendingMatch:
      type: object
      required:
      - slug
      - title
      - image
      - icon
      - volume1h
      - polymarket
      - kalshi
      properties:
        slug:
          type: string
          description: Game slug, used as the matching-cache key.
          example: nba-lal-bos-2026-01-15
        title:
          type: string
          description: Human-readable game title, from the matching cache.
          example: Lakers vs Celtics
        image:
          type:
          - string
          - 'null'
          description: Always null — not populated by this endpoint.
        icon:
          type:
          - string
          - 'null'
          description: Always null — not populated by this endpoint.
        volume1h:
          type: number
          format: double
          description: Kalshi 1h (or total, as fallback) volume used for ranking, from the discover cache.
          example: 48213.5
        polymarket:
          $ref: '#/components/schemas/SportsTrendingMatchPolymarketSide'
        kalshi:
          $ref: '#/components/schemas/SportsTrendingMatchKalshiSide'
    SportsTrendingMatchPolymarketSide:
      type: object
      required:
      - marketId
      - tokenId
      - price
      properties:
        marketId:
          type: string
          description: Equal to the game slug (Polymarket has no separate market id in this cache entry).
          example: nba-lal-bos-2026-01-15
        tokenId:
          type:
          - string
          - 'null'
          description: Polymarket CLOB token id, from the matching cache.
          example: 10897234...
        price:
          type:
          - number
          - 'null'
          format: double
          example: 0.62
    SportsTrendingMatchKalshiSide:
      type: object
      required:
      - marketId
      - eventTicker
      - price
      properties:
        marketId:
          type:
          - string
          - 'null'
          example: KXNBAGAME-26JAN15LALBOS-LAL
        eventTicker:
          type:
          - string
          - 'null'
          example: KXNBAGAME-26JAN15LALBOS
        price:
          type:
          - number
          - 'null'
          format: double
          example: 0.6
    SportsMatchingMarketsResponse:
      type: object
      description: Map keyed by requested game slug. Slugs with no cache entry, or an unparseable cache
        value, are omitted from this object entirely.
      additionalProperties:
        $ref: '#/components/schemas/SportsMatchingMarketEntry'
      example:
        nba-lal-bos-2026-01-15:
          title: Lakers vs Celtics
          tokenId: 10897234...
          providers:
          - provider: polymarket
            marketId: nba-lal-bos-2026-01-15
            price: 0.62
          - provider: kalshi
            marketId: KXNBAGAME-26JAN15LALBOS-LAL
            price: 0.6
            eventTicker: KXNBAGAME-26JAN15LALBOS
          - provider: predictfun
            marketId: "39142"
            price: 0.61
    SportsMatchingMarketEntry:
      type: object
      description: Raw contents of a cached cross-venue sports market match entry.
      required:
      - providers
      properties:
        title:
          type: string
          example: Lakers vs Celtics
        tokenId:
          type: string
          description: Polymarket CLOB token id for the primary market side.
          example: 10897234...
        providers:
          type: array
          items:
            $ref: '#/components/schemas/SportsMatchingProvider'
    SportsMatchingProvider:
      type: object
      required:
      - provider
      - marketId
      - price
      properties:
        provider:
          type: string
          enum:
          - polymarket
          - kalshi
          - predictfun
        marketId:
          type: string
          example: KXNBAGAME-26JAN15LALBOS-LAL
        price:
          type: number
          format: double
          nullable: true
          description: 0-1 scale probability/price.
          example: 0.6
        eventTicker:
          type: string
          nullable: true
          description: Kalshi event ticker. Present only on kalshi provider entries.
          example: KXNBAGAME-26JAN15LALBOS
        volume:
          type: number
          format: double
          nullable: true
        teamPrices:
          type: array
          description: Per-team YES markets for providers that split 3-way moneylines.
          items:
            type: object
            required:
            - name
            - marketId
            - price
            properties:
              name:
                type: string
              marketId:
                type: string
              price:
                type: number
                format: double
                nullable: true
        drawMarketId:
          type: string
          nullable: true
        drawPrice:
          type: number
          format: double
          nullable: true
        homeMarketId:
          type: string
          nullable: true
        homePrice:
          type: number
          format: double
          nullable: true
        awayMarketId:
          type: string
          nullable: true
        awayPrice:
          type: number
          format: double
          nullable: true
    SportsKalshiFiltersResponse:
      type: object
      description: Verbatim pass-through of the Kalshi filters_by_sport upstream payload.
      required:
      - filters_by_sports
      - sport_ordering
      properties:
        filters_by_sports:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SportsKalshiFilterSport'
          example:
            Basketball:
              competitions:
                NBA:
                  scopes:
                  - Game
                  - Series
                  - Season
              scopes:
              - Game
              - Series
              - Season
        sport_ordering:
          type: array
          items:
            type: string
          example:
          - All sports
          - Basketball
          - Football
          - Baseball
    SportsKalshiFilterSport:
      type: object
      required:
      - competitions
      - scopes
      properties:
        competitions:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SportsKalshiFilterCompetition'
        scopes:
          type: array
          items:
            type: string
    SportsKalshiFilterCompetition:
      type: object
      required:
      - scopes
      properties:
        scopes:
          type: array
          items:
            type: string
          example:
          - Game
          - Series
          - Season
    SportsLiveEventsResponse:
      type: object
      required:
      - events
      - matchingMarkets
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/SportsLiveEvent'
        matchingMarkets:
          type: object
          description: Map keyed by matched slug, one entry per event that resolved a matchingSlug.
          additionalProperties:
            $ref: '#/components/schemas/SportsMatchingMarketEntry'
    SportsLiveEvent:
      type: object
      required:
      - gameId
      - sport
      - sportFamily
      - homeTeam
      - awayTeam
      - homeScore
      - awayScore
      - status
      - period
      - elapsed
      - live
      - ended
      - slug
      - matchingSlug
      - ts
      - urgencyScore
      - stale
      - primaryMarket
      - marketsCount
      - timing
      properties:
        timing:
          $ref: '#/components/schemas/SportsTiming'
        gameId:
          type: string
          example: '12345678'
        sport:
          type: string
          description: Lowercased league abbreviation.
          example: nba
        sportFamily:
          type: string
          description: Canonical category id from the sports catalog.
          example: basketball
        homeTeam:
          type: string
          example: Boston Celtics
        awayTeam:
          type: string
          example: Los Angeles Lakers
        homeScore:
          type: integer
          description: Series score if the game has one, else the game score.
          example: 88
        awayScore:
          type: integer
          example: 91
        status:
          type: string
          example: live
        period:
          type:
          - string
          - 'null'
          example: Q4
        elapsed:
          description: Opaque provider clock string; direction, period resets, and overtime semantics are not verified.
          nullable: true
          example: 02:14
        live:
          type: boolean
        ended:
          type: boolean
        slug:
          type:
          - string
          - 'null'
          example: nba-lal-bos-2026-01-15
        matchingSlug:
          type:
          - string
          - 'null'
          description: Slug used as the key into matchingMarkets.
          example: nba-lal-bos-2026-01-15
        ts:
          type: string
          format: date-time
          description: Game-state observation timestamp; source observation time when available, otherwise time received by Kairos.
          example: '2026-07-22T23:14:05Z'
        urgencyScore:
          type: integer
          description: Candidate-ranking heuristic for fresh games explicitly in play. Zero for stale,
            stopped, ended, unknown-status, and schedule-only games. Not a remaining-time signal.
          example: 160
        stale:
          type: boolean
          description: True for state older than 120 seconds, invalid or future timestamps,
            timestamps without a timezone, or absence of a live score feed. Evaluated at timing.evaluatedAt.
        primaryMarket:
          oneOf:
          - $ref: '#/components/schemas/SportsNormalizedMarket'
          - type: 'null'
        marketsCount:
          type: integer
          example: 6
        markets:
          type: array
          description: Present only when include_markets=true.
          items:
            $ref: '#/components/schemas/SportsNormalizedMarket'
    SportsTiming:
      type: object
      description: Conservative timing evidence. Polymarket elapsed is not normalized to remaining time.
        Predict.fun schedule windows do not establish live score freshness. Consumers must age
        stateAgeSeconds from evaluatedAt because responses can be cached.
      required:
      - source
      - status
      - clock
      - clockDirection
      - clockRunning
      - periodRemainingSeconds
      - gameRemainingSeconds
      - overtime
      - stateAgeSeconds
      - evaluatedAt
      - staleAfterSeconds
      - stale
      - usableForLateGame
      - unavailableReason
      properties:
        source:
          type: string
          enum: [polymarket, predictfun]
        status:
          type: string
          enum: [scheduled, live, break, overtime, shootout, suspended, delayed, postponed, cancelled, final, unknown]
        clock:
          type: [string, 'null']
        clockDirection:
          type: string
          enum: [unknown]
        clockRunning:
          type: [boolean, 'null']
          description: Currently null; running state is not established.
        periodRemainingSeconds:
          type: [number, 'null']
          description: Currently null; do not derive this from elapsed without verified clock semantics.
        gameRemainingSeconds:
          type: [number, 'null']
          description: Currently null; overtime and eventual completion cannot be inferred from regulation time.
        overtime:
          type: [boolean, 'null']
          description: True for explicit OT or extra-time evidence, false for regulation final FT,
            otherwise null. Completion after overtime is not active overtime.
        stateAgeSeconds:
          type: [number, 'null']
          minimum: 0
        evaluatedAt:
          type: string
          format: date-time
        staleAfterSeconds:
          type: integer
          const: 120
        stale:
          type: boolean
        usableForLateGame:
          type: boolean
          const: false
          description: No authoritative countdown is currently available from these sources.
        unavailableReason:
          type: string
          enum: [unknown_clock_semantics, missing_clock, game_not_in_play, stale_state, invalid_timestamp, no_live_score_feed]
    SportsNormalizedMarket:
      type: object
      description: Upstream market normalized into a common shape used across the live-events, event-markets,
        and upcoming-events endpoints.
      required:
      - id
      - question
      - slug
      - conditionId
      - tokenId
      - outcomePrices
      - outcomes
      - volumeNum
      - liquidityNum
      - acceptingOrders
      - sportsMarketType
      - groupItemTitle
      - bestBid
      - bestAsk
      - spread
      - provider
      - image
      - icon
      properties:
        id:
          type: string
          example: '587234'
        question:
          type: string
          example: Will the Celtics win?
        slug:
          type: string
          example: nba-lal-bos-2026-01-15-bos
        conditionId:
          type: string
          example: 0xabc123...
        tokenId:
          type: string
          description: First clobTokenId for the market.
          example: 10897234...
        outcomePrices:
          type: array
          items:
            type: number
            format: double
          example:
          - 0.6
          - 0.4
        outcomes:
          type: array
          items:
            type: string
          example:
          - 'Yes'
          - 'No'
        volumeNum:
          type: number
          format: double
          example: 128432.1
        liquidityNum:
          type: number
          format: double
          example: 42311.0
        acceptingOrders:
          type: boolean
        sportsMarketType:
          type:
          - string
          - 'null'
          example: moneyline
        groupItemTitle:
          type:
          - string
          - 'null'
          example: Celtics
        gameStartTime:
          type:
          - string
          - 'null'
          format: date-time
          description: Scheduled fixture kickoff used for cross-provider live-game matching.
        bestBid:
          type:
          - number
          - 'null'
          format: double
          example: 0.59
        bestAsk:
          type:
          - number
          - 'null'
          format: double
          example: 0.61
        spread:
          type:
          - number
          - 'null'
          format: double
          example: 0.02
        provider:
          type: string
          example: polymarket
        image:
          type:
          - string
          - 'null'
        icon:
          type:
          - string
          - 'null'
        outcomeMarketIds:
          type: array
          description: Only present on 3-way (soccer moneyline) composite markets. Length 3, [home, draw,
            away].
          items:
            type:
            - string
            - 'null'
          example:
          - '587234'
          - '587235'
          - '587236'
        outcomeTokenIds:
          type: array
          description: Per-outcome token IDs in the same order as outcomes, when available.
          items:
            type:
            - string
            - 'null'
        outcomeConditionIds:
          type: array
          description: Only present on 3-way composite markets.
          items:
            type:
            - string
            - 'null'
    SportsEventMarketsResponse:
      type: object
      required:
      - gameId
      - markets
      properties:
        gameId:
          type: string
          description: Resolved gameId, or the raw game_id/slug query value ("unknown" if neither resolved).
          example: '12345678'
        markets:
          type: array
          items:
            $ref: '#/components/schemas/SportsNormalizedMarket'
    SportsKalshiLiveGamesResponse:
      type: object
      required:
      - games
      properties:
        games:
          type: array
          items:
            $ref: '#/components/schemas/SportsKalshiLiveGame'
    SportsKalshiLiveGame:
      type: object
      required:
      - milestoneId
      - type
      - family
      - league
      - sport
      - title
      - startDate
      - homeTeamId
      - awayTeamId
      - home
      - away
      - live
      - moneyline
      - spread
      - total
      - featuredImageUrl
      - totalVolume
      properties:
        milestoneId:
          type: string
          example: MILESTONE-987
        type:
          type: string
          enum:
          - hockey_tournament
          - basketball_game
          - baseball_game
          - football_game
          - soccer_tournament_multi_leg
          - esports_match
        family:
          type: string
          enum:
          - team
          - soccer
          - esports
        league:
          type: string
          example: NBA
        sport:
          type: string
          example: basketball
        title:
          type: string
          example: Lakers at Celtics
        startDate:
          type: string
          format: date-time
        homeTeamId:
          type: string
        awayTeamId:
          type: string
        home:
          $ref: '#/components/schemas/SportsKalshiTeamBlock'
        away:
          $ref: '#/components/schemas/SportsKalshiTeamBlock'
        draw:
          oneOf:
          - $ref: '#/components/schemas/SportsKalshiTeamBlock'
          - type: 'null'
          description: Only meaningful for family=soccer.
        drawTicker:
          type:
          - string
          - 'null'
          description: Only meaningful for family=soccer.
        drawPrice:
          type:
          - number
          - 'null'
          format: double
          description: Only meaningful for family=soccer.
        live:
          $ref: '#/components/schemas/SportsKalshiLiveData'
        moneyline:
          $ref: '#/components/schemas/SportsKalshiMoneyline'
        spread:
          oneOf:
          - $ref: '#/components/schemas/SportsKalshiLinesGroup'
          - type: 'null'
        total:
          oneOf:
          - $ref: '#/components/schemas/SportsKalshiLinesGroup'
          - type: 'null'
        featuredImageUrl:
          type:
          - string
          - 'null'
        totalVolume:
          type: number
          format: double
          description: Sum of moneyline market volumes.
          example: 84213.0
    SportsKalshiTeamBlock:
      type: object
      required:
      - name
      - logoUrl
      - color
      properties:
        name:
          type: string
          example: Boston Celtics
        logoUrl:
          type:
          - string
          - 'null'
        color:
          type:
          - string
          - 'null'
          example: '#007A33'
    SportsKalshiLiveData:
      type: object
      description: 'Live game-state block. Always includes homePoints, awayPoints, lastPlay, status. The
        remaining fields vary by milestone type: soccer_tournament_multi_leg adds statusText/half/time;
        esports_match adds format/currentMap; hockey_tournament adds period/periodRemaining; basketball_game
        adds period/periodType/periodRemaining/possession; baseball_game adds inning/inningHalf/balls/strikes/outs/bases;
        football_game adds quarter/clock/down/yardsToFirst/yardline/possessionTeamId.'
      additionalProperties: true
      properties:
        homePoints:
          type: integer
        awayPoints:
          type: integer
        lastPlay:
          type: string
        status:
          type: string
      example:
        homePoints: 88
        awayPoints: 91
        lastPlay: 3-pointer made
        status: in_progress
        period: 4
        periodType: quarter
        periodRemaining: 02:14
        possession: away
    SportsKalshiMoneyline:
      type: object
      required:
      - eventTicker
      - seriesTicker
      - homeTicker
      - awayTicker
      - homePrice
      - awayPrice
      - homeYesAsk
      - awayYesAsk
      properties:
        eventTicker:
          type: string
          example: KXNBAGAME-26JAN15LALBOS
        seriesTicker:
          type: string
          example: KXNBAGAME
        homeTicker:
          type: string
          example: KXNBAGAME-26JAN15LALBOS-BOS
        awayTicker:
          type: string
          example: KXNBAGAME-26JAN15LALBOS-LAL
        homePrice:
          type:
          - number
          - 'null'
          format: double
          description: yes_ask, falling back to last_price.
          example: 0.6
        awayPrice:
          type:
          - number
          - 'null'
          format: double
          example: 0.41
        homeYesAsk:
          type:
          - number
          - 'null'
          format: double
        awayYesAsk:
          type:
          - number
          - 'null'
          format: double
    SportsKalshiLinesGroup:
      type: object
      required:
      - eventTicker
      - seriesTicker
      - markets
      properties:
        eventTicker:
          type: string
        seriesTicker:
          type: string
        markets:
          type: array
          items:
            $ref: '#/components/schemas/SportsKalshiLineMarket'
    SportsKalshiLineMarket:
      type: object
      required:
      - ticker
      - yesSubTitle
      - noSubTitle
      - lastPrice
      - yesAsk
      - noAsk
      - volume
      properties:
        ticker:
          type: string
          example: KXNBAGAME-26JAN15LALBOS-T220.5
        yesSubTitle:
          type: string
          example: Over 220.5
        noSubTitle:
          type: string
          example: Under 220.5
        lastPrice:
          type:
          - number
          - 'null'
          format: double
        yesAsk:
          type:
          - number
          - 'null'
          format: double
        noAsk:
          type:
          - number
          - 'null'
          format: double
        volume:
          type:
          - number
          - 'null'
          format: double
    SportsPolyKalshiPairingsResponse:
      type: object
      required:
      - pairings
      properties:
        pairings:
          type: array
          items:
            $ref: '#/components/schemas/SportsPolyKalshiPairing'
    SportsPolyKalshiPairing:
      type: object
      required:
      - polyGameId
      - polySport
      - kalshiEventTicker
      - kalshiLeague
      - kalshiMilestoneId
      - kalshiMarkets
      properties:
        polyGameId:
          type: string
          example: '10078222'
        polySport:
          type: string
          example: mlb
        kalshiEventTicker:
          type: string
          example: KXMLBGAME-26JUN041410SFMIL
        kalshiLeague:
          type: string
          example: MLB
        kalshiMilestoneId:
          type: string
          example: MILESTONE-4521
        kalshiMarkets:
          type: array
          items:
            $ref: '#/components/schemas/SportsKalshiFlattenedMarket'
    SportsKalshiFlattenedMarket:
      type: object
      required:
      - ticker
      - title
      - yes_sub_title
      - price
      - volume
      - image
      - eventTicker
      properties:
        ticker:
          type: string
          example: KXMLBGAME-26JUN041410SFMIL-SF
        title:
          type: string
          example: Giants at Brewers
        yes_sub_title:
          type: string
          example: Giants win
        price:
          type: number
          format: double
          description: Rescaled to a 0-100 range (not the 0-1 scale used elsewhere in this API), matching
            a legacy response shape.
          example: 46.5
        volume:
          type: number
          format: double
          example: 12045.0
        image:
          type:
          - string
          - 'null'
        eventTicker:
          type: string
          example: KXMLBGAME-26JUN041410SFMIL
    SportsCatalogResponse:
      type: object
      required:
      - categories
      properties:
        categories:
          type: array
          items:
            $ref: '#/components/schemas/SportsCatalogCategory'
    SportsCatalogCategory:
      type: object
      required:
      - id
      - label
      - leagues
      properties:
        id:
          type: string
          example: basketball
        label:
          type: string
          example: Basketball
        leagues:
          type: array
          items:
            $ref: '#/components/schemas/SportsCatalogLeague'
    SportsCatalogLeague:
      type: object
      required:
      - slug
      - label
      - tagId
      - seriesId
      properties:
        slug:
          type: string
          example: nba
        label:
          type: string
          example: NBA
        tagId:
          type: integer
          description: Hardcoded constant shared by every league entry.
          example: 100639
        seriesId:
          type:
          - integer
          - 'null'
          description: Resolved Polymarket series id, or null if unresolved.
          example: 123
    SportsUpcomingEventsResponse:
      type: object
      required:
      - events
      - startingEvents
      - matchingMarkets
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/SportsUpcomingEvent'
        startingEvents:
          type: array
          description: Fixtures whose scheduled start was within the last ten minutes but which
            have not yet appeared in the live feed. These are schedule-derived, not confirmed live.
          items:
            $ref: '#/components/schemas/SportsUpcomingEvent'
        matchingMarkets:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SportsUpcomingMatchingEntry'
    SportsUpcomingEvent:
      type: object
      required:
      - eventId
      - title
      - startTime
      - sport
      - sportFamily
      - league
      - teamA
      - teamB
      - image
      - primaryMarket
      - marketsCount
      - eventSlug
      - drawPrice
      - teamBPrice
      properties:
        eventId:
          type: string
          example: '10078222'
        title:
          type: string
          example: Lakers at Celtics
        startTime:
          type: string
          format: date-time
          description: Normalized ISO8601 gameStartTime (endDate fallback for events missing it).
        sport:
          type: string
          description: Broad sport family used for display and grouping.
          example: soccer
        sportFamily:
          type: string
          description: Canonical category id from the sports catalog; equal to sport on this route.
          example: soccer
        league:
          type: string
          description: Sports-catalog league slug.
          example: uel
        teamA:
          type:
          - string
          - 'null'
          example: Los Angeles Lakers
        teamB:
          type:
          - string
          - 'null'
          example: Boston Celtics
        image:
          type:
          - string
          - 'null'
        primaryMarket:
          oneOf:
          - $ref: '#/components/schemas/SportsUpcomingMarket'
          - type: 'null'
        marketsCount:
          type: integer
          example: 4
        eventSlug:
          type:
          - string
          - 'null'
          description: Venue fixture slug accepted by the cross-venue game-markets catalog.
          example: nba-lal-bos-2026-01-15
        matchingSlug:
          type:
          - string
          - 'null'
          description: Alternate market slug under which cross-venue matches may be published.
        startingEligible:
          type: boolean
          description: True when a known kickoff lets the fixture enter the starting grace window.
        awaitingLive:
          type: boolean
          description: True only in startingEvents; it does not confirm that the game is live.
        drawPrice:
          type:
          - number
          - 'null'
          format: double
          description: Present for soccer 3-way events.
        teamBPrice:
          type:
          - number
          - 'null'
          format: double
          description: Present for soccer 3-way events.
    SportsUpcomingMarket:
      type: object
      required:
      - id
      - provider
      - question
      - slug
      - conditionId
      - tokenId
      - outcomePrices
      - outcomes
      - volumeNum
      - liquidityNum
      - acceptingOrders
      - sportsMarketType
      - gameId
      properties:
        id:
          type: string
        provider:
          type: string
          enum:
          - polymarket
          - kalshi
          - predictfun
        question:
          type: string
        slug:
          type: string
        conditionId:
          type: string
        tokenId:
          type: string
        outcomePrices:
          type: array
          items:
            type: number
            format: double
        outcomes:
          type: array
          items:
            type: string
        volumeNum:
          type: number
          format: double
        liquidityNum:
          type: number
          format: double
        acceptingOrders:
          type: boolean
        sportsMarketType:
          type:
          - string
          - 'null'
        gameId:
          description: Raw upstream game id, type varies by provider.
          nullable: true
        outcomeMarketIds:
          type: array
          description: Only present on 3-way (soccer) composite markets. [home, draw, away].
          items:
            type:
            - string
            - 'null'
        outcomeTokenIds:
          type: array
          description: Only present on 3-way composite markets.
          items:
            type:
            - string
            - 'null'
        outcomeConditionIds:
          type: array
          description: Only present on 3-way composite markets.
          items:
            type:
            - string
            - 'null'
    SportsUpcomingMatchingEntry:
      type: object
      required:
      - providers
      properties:
        providers:
          type: array
          items:
            $ref: '#/components/schemas/SportsUpcomingMatchingProvider'
    SportsUpcomingMatchingProvider:
      type: object
      required:
      - provider
      - price
      - marketId
      - eventTicker
      - volume
      properties:
        provider:
          type: string
          enum:
          - polymarket
          - kalshi
          - predictfun
          - hyperliquid
        price:
          type:
          - number
          - 'null'
          format: double
        marketId:
          type: string
        eventTicker:
          type:
          - string
          - 'null'
        volume:
          type:
          - number
          - 'null'
          format: double
        source:
          type: string
          description: Present as `market_matcher` when cross-venue identity matching supplied the entry.
    SportsFuturesResponse:
      type: object
      required:
      - futures
      properties:
        futures:
          type: array
          items:
            $ref: '#/components/schemas/SportsFuturesEvent'
        total:
          type: integer
          description: Futures matching the filters across every page. Present only when `limit` is set.
        nextOffset:
          type: integer
          nullable: true
          description: '`offset` for the next page, or null on the last. Present only when `limit` is set.'
    SportsFuturesEvent:
      type: object
      required:
      - eventId
      - title
      - image
      - sport
      - sportFamily
      - league
      - provider
      - volume
      - outcomes
      - marketsCount
      properties:
        eventId:
          type: string
          example: '10088234'
        title:
          type: string
          example: Super Bowl LX Winner
        image:
          type:
          - string
          - 'null'
        sport:
          type: string
          description: Sports-catalog category id, or "other" if not classified into a known sport.
          example: football
        sportFamily:
          type: string
          description: Canonical category id from the sports catalog; equal to sport on this route.
          example: football
        league:
          type:
          - string
          - 'null'
          example: nfl
        provider:
          type: string
          enum:
          - polymarket
          - predictfun
        volume:
          type: number
          format: double
          description: Sum of contender market volumes. Always 0 for predictfun legs (no volume field
            upstream).
          example: 842311.0
        outcomes:
          type: array
          description: Capped at 30 outcomes per event, highest-priced first with unpriced outcomes sunk
            to the bottom.
          items:
            $ref: '#/components/schemas/SportsFuturesOutcome'
        marketsCount:
          type: integer
          description: Equal to len(outcomes) after capping.
          example: 32
    SportsFuturesOutcome:
      type: object
      required:
      - label
      - price
      - marketId
      - tokenId
      - conditionId
      properties:
        label:
          type: string
          example: Kansas City Chiefs
        price:
          type:
          - number
          - 'null'
          format: double
          example: 0.18
        marketId:
          type: string
          example: '10088235'
        tokenId:
          type:
          - string
          - 'null'
        conditionId:
          type:
          - string
          - 'null'
    SportsTournamentBracketResponse:
      type: object
      required:
      - leftRounds
      - rightRounds
      - final
      - winner
      properties:
        leftRounds:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketRound'
        rightRounds:
          type: array
          description: Empty when format=left-to-right.
          items:
            $ref: '#/components/schemas/SportsBracketRound'
        final:
          oneOf:
          - $ref: '#/components/schemas/SportsBracketMatch'
          - type: object
            description: Placeholder used when neither finalist is known yet.
            properties:
              id:
                type: string
                example: final-tbd
              teams:
                type: array
                items:
                  type: 'null'
              status:
                type: string
                example: scheduled
              subtitle:
                type: string
                example: Final
        winner:
          type: 'null'
          description: Always null — this endpoint never resolves a tournament champion.
        thirdPlace:
          oneOf:
          - $ref: '#/components/schemas/SportsBracketMatch'
          - type: 'null'
          description: Present only for competitions that have a third-place fixture.
        groups:
          type: array
          description: Present only when format=groups-then-knockout.
          items:
            $ref: '#/components/schemas/SportsBracketGroup'
    SportsBracketRound:
      type: object
      required:
      - name
      - shortName
      - matches
      properties:
        name:
          type: string
          example: Quarter-finals
        shortName:
          type: string
          example: QF
        matches:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketMatch'
    SportsBracketMatch:
      type: object
      required:
      - id
      - teams
      - status
      - subtitle
      - resolvesAfterExtraTime
      properties:
        id:
          type: string
          example: ucl-qf-1
        teams:
          type: array
          minItems: 2
          maxItems: 2
          items:
            oneOf:
            - $ref: '#/components/schemas/SportsBracketTeam'
            - type: 'null'
        status:
          type: string
          enum:
          - scheduled
          - live
          - completed
        subtitle:
          type: string
          description: Formatted fixture date, or "Date1 / Date2" for two-leg ties.
          example: Apr 9, 2026
        resolvesAfterExtraTime:
          type: boolean
        kickoffTime:
          type: string
          format: date-time
        matchday:
          type: integer
          example: 5
        scores:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: integer
          example:
          - 2
          - 1
        winnerIdx:
          type: integer
          enum:
          - 0
          - 1
        aggregateScores:
          type: array
          description: Present for two-leg ties.
          minItems: 2
          maxItems: 2
          items:
            type: integer
          example:
          - 3
          - 2
        providerPrices:
          $ref: '#/components/schemas/SportsBracketProviderPrices'
    SportsBracketTeam:
      type: object
      required:
      - name
      - shortName
      - logo
      properties:
        name:
          type: string
          example: Real Madrid
        shortName:
          type: string
          example: RMA
        logo:
          type:
          - string
          - 'null'
    SportsBracketProviderPrices:
      type: object
      description: Only providers with a resolvable market for this tie are present.
      properties:
        polymarket:
          $ref: '#/components/schemas/SportsBracketProviderPricePoly'
        predictfun:
          $ref: '#/components/schemas/SportsBracketProviderPriceGeneric'
        kalshi:
          $ref: '#/components/schemas/SportsBracketProviderPriceKalshi'
    SportsBracketProviderPricePoly:
      type: object
      required:
      - slug
      - marketId
      - title
      - titles
      - prices
      - drawPrice
      - outcomeMarketIds
      properties:
        slug:
          type: string
        marketId:
          type:
          - string
          - 'null'
        title:
          type: string
        titles:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: string
        prices:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type:
            - number
            - 'null'
            format: double
        drawPrice:
          type:
          - number
          - 'null'
          format: double
        outcomeMarketIds:
          type: array
          minItems: 3
          maxItems: 3
          items:
            type:
            - string
            - 'null'
        spreadLines:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketSpreadLine'
        totalLines:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketTotalLine'
        comboEligibility:
          type: object
          description: Present only when the combo eligibility store has data for this tie.
          additionalProperties:
            $ref: '#/components/schemas/SportsComboEligibilityEntry'
    SportsBracketSpreadLine:
      type: object
      properties:
        line:
          type:
          - number
          - 'null'
          format: double
        team0Price:
          type:
          - number
          - 'null'
          format: double
        team1Price:
          type:
          - number
          - 'null'
          format: double
        team0MarketId:
          type:
          - string
          - 'null'
        team1MarketId:
          type:
          - string
          - 'null'
    SportsBracketTotalLine:
      type: object
      properties:
        line:
          type:
          - number
          - 'null'
          format: double
        overPrice:
          type:
          - number
          - 'null'
          format: double
        underPrice:
          type:
          - number
          - 'null'
          format: double
        overMarketId:
          type:
          - string
          - 'null'
        underMarketId:
          type:
          - string
          - 'null'
    SportsBracketProviderPriceGeneric:
      type: object
      required:
      - slug
      - marketId
      - title
      - titles
      - prices
      - drawPrice
      - outcomeMarketIds
      properties:
        slug:
          type: string
        marketId:
          type:
          - string
          - 'null'
        title:
          type: string
        titles:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: string
        prices:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type:
            - number
            - 'null'
            format: double
        drawPrice:
          type:
          - number
          - 'null'
          format: double
        outcomeMarketIds:
          type: array
          minItems: 3
          maxItems: 3
          items:
            type:
            - string
            - 'null'
    SportsBracketProviderPriceKalshi:
      type: object
      required:
      - eventTicker
      - marketId
      - title
      - titles
      - prices
      - drawPrice
      - outcomeMarketIds
      properties:
        eventTicker:
          type: string
        marketId:
          type:
          - string
          - 'null'
        title:
          type: string
        titles:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: string
        prices:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type:
            - number
            - 'null'
            format: double
        drawPrice:
          type:
          - number
          - 'null'
          format: double
        outcomeMarketIds:
          type: array
          minItems: 3
          maxItems: 3
          items:
            type:
            - string
            - 'null'
    SportsComboEligibilityEntry:
      type: object
      required:
      - yesPositionId
      - noPositionId
      properties:
        yesPositionId:
          type: string
        noPositionId:
          type: string
    SportsBracketGroup:
      type: object
      required:
      - name
      - shortName
      - table
      - matches
      properties:
        name:
          type: string
          example: Group A
        shortName:
          type: string
          example: A
        table:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketStanding'
        matches:
          type: array
          items:
            $ref: '#/components/schemas/SportsBracketMatch'
    SportsBracketStanding:
      type: object
      required:
      - team
      - played
      - won
      - draw
      - lost
      - goalsFor
      - goalsAgainst
      - points
      - goalDifference
      properties:
        team:
          $ref: '#/components/schemas/SportsBracketTeam'
        played:
          type: integer
        won:
          type: integer
        draw:
          type: integer
        lost:
          type: integer
        goalsFor:
          type: integer
        goalsAgainst:
          type: integer
        points:
          type: integer
        goalDifference:
          type: integer
    SportsGameMarketsResponse:
      type: object
      required:
      - slug
      - teamA
      - teamB
      - teamALogo
      - teamBLogo
      - sport
      - sections
      properties:
        slug:
          type: string
          example: nba-lal-bos-2026-01-15
        teamA:
          type:
          - string
          - 'null'
          example: Los Angeles Lakers
        teamB:
          type:
          - string
          - 'null'
          example: Boston Celtics
        teamALogo:
          type:
          - string
          - 'null'
        teamBLogo:
          type:
          - string
          - 'null'
        sport:
          type:
          - string
          - 'null'
          example: nba
        sections:
          $ref: '#/components/schemas/SportsGameMarketsSections'
    SportsGameMarketsSections:
      type: object
      required:
      - gameLines
      - halves
      - playerProps
      - moreMarkets
      - exactScore
      - corners
      properties:
        gameLines:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
        halves:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
        playerProps:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
        moreMarkets:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
        exactScore:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
        corners:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketFamily'
    SportsGameMarketFamily:
      type: object
      description: A market family from one venue. layout=pills carries a flat legs list; layout=ladder
        carries spread/total rungs grouped by line value. Polymarket families may be combo-eligible;
        Predict.fun families never are.
      required:
      - key
      - title
      - section
      - layout
      - provider
      - mergeKey
      - volume
      properties:
        key:
          type: string
          example: moneyline
        title:
          type: string
          example: Moneyline
        section:
          type: string
          enum:
          - gameLines
          - halves
          - playerProps
          - moreMarkets
          - exactScore
          - corners
        layout:
          type: string
          enum:
          - pills
          - ladder
        provider:
          type: string
          enum:
          - polymarket
          - predictfun
          - kalshi
          - hyperliquid
          description: Venue for every market in this family.
        mergeKey:
          type: string
          description: Opaque server-assigned identity for equivalent families across venues. Group pill
            families by this value rather than inferring identity from titles.
          example: moneyline
        volume:
          type: number
          format: double
        kind:
          type: string
          enum:
          - spread
          - total
          description: Only present when layout=ladder.
        legs:
          type: array
          description: Only present when layout=pills.
          items:
            $ref: '#/components/schemas/SportsGameMarketLeg'
        rungs:
          type: array
          description: Only present when layout=ladder.
          items:
            $ref: '#/components/schemas/SportsGameMarketRung'
    SportsGameMarketRung:
      type: object
      required:
      - line
      - legs
      properties:
        line:
          type:
          - number
          - 'null'
          format: double
          example: 220.5
        legs:
          type: array
          items:
            $ref: '#/components/schemas/SportsGameMarketLeg'
    SportsGameMarketLeg:
      type: object
      required:
      - label
      - marketId
      - price
      - side
      - outcomeKey
      properties:
        label:
          type: string
          example: Celtics -4.5
        marketId:
          type: string
          example: '587234'
        price:
          type: number
          format: double
          minimum: 0
          exclusiveMaximum: 1
          description: Probability-scale price. Predict.fun may return 0 when an open market has no usable
            quote; 0 is unavailable pricing, not an executable price.
          example: 0.52
        side:
          type: string
          enum:
          - 'yes'
          - 'no'
          description: Contract side represented by this leg. Select the matching position ID for combos;
            a no-side leg must use noPositionId.
        outcomeKey:
          type: string
          description: Opaque server-assigned identity for equivalent outcome rows across venues.
          example: team:boston celtics
        comboEligible:
          type: boolean
          description: Present only when eligibility is known. Predict.fun legs always set false; an absent
            field means unknown, not false.
        yesPositionId:
          type:
          - string
          - 'null'
          description: Present when eligibility is known. Use only for side=yes.
        noPositionId:
          type:
          - string
          - 'null'
          description: Present when eligibility is known. Use only for side=no.
    SportsComboMarketsResponse:
      type: object
      required:
      - markets
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/SportsComboMarket'
    SportsComboMarket:
      type: object
      required:
      - id
      - conditionId
      - yesPositionId
      - noPositionId
      - slug
      - title
      - tags
      properties:
        id:
          type: string
          description: Polymarket market id.
          example: '587234'
        conditionId:
          type: string
          example: 0xabc123...
        yesPositionId:
          type: string
          example: 10897234...
        noPositionId:
          type: string
          example: 10897235...
        slug:
          type: string
          example: nba-lal-bos-2026-01-15-bos
        title:
          type: string
          example: Celtics -4.5
        tags:
          type: array
          items:
            type: string
          example:
          - nba
          - spread
    SportsMetadataResponse:
      type: object
      required:
      - teams
      - leagues
      properties:
        teams:
          type: array
          items:
            $ref: '#/components/schemas/SportsTeam'
        leagues:
          type: array
          items:
            $ref: '#/components/schemas/SportsLeague'
    SportsTeam:
      type: object
      required:
      - id
      - name
      - league
      - logoUrl
      - abbreviation
      - alias
      - color
      properties:
        id:
          type: string
          description: Internal row id (numeric, serialized as string).
          example: '142'
        name:
          type: string
          example: Boston Celtics
        league:
          type: string
          example: NBA
        logoUrl:
          type:
          - string
          - 'null'
        abbreviation:
          type:
          - string
          - 'null'
          example: BOS
        alias:
          type:
          - string
          - 'null'
          example: Celtics
        color:
          type:
          - string
          - 'null'
          example: '#007A33'
    SportsLeague:
      type: object
      required:
      - id
      - sport
      - imageUrl
      properties:
        id:
          type: string
          example: '8'
        sport:
          type: string
          example: basketball
        imageUrl:
          type:
          - string
          - 'null'
    SportsMatchedMarketsResponse:
      type: object
      required:
      - pairs
      - count
      - limit
      - offset
      - has_more
      properties:
        pairs:
          type: array
          items:
            $ref: '#/components/schemas/SportsMatchedMarketPair'
        count:
          type: integer
          description: Number of pairs on this page after live-status post-filtering (may be less than
            limit).
          example: 42
        limit:
          type: integer
          example: 50
        offset:
          type: integer
          example: 0
        has_more:
          type: boolean
          description: Whether the underlying page (before live-status post-filtering) was full —
            not a strict function of count.
        next_cursor:
          type:
          - string
          - 'null'
          description: Opaque cursor for the next keyset page. Present when cursor mode is requested.
        catalog_version:
          type: string
          description: Full-set fingerprint shared by every page in one cursor walk.
        total:
          type: integer
          description: Only present when include_total=true. Total matching-pair count under the pre-live-filter
            query filters.
          example: 1284
    SportsMatchedMarketPair:
      type: object
      required:
      - a
      - b
      - similarity
      - updated_at
      properties:
        a:
          $ref: '#/components/schemas/SportsMatchedMarketSide'
        b:
          $ref: '#/components/schemas/SportsMatchedMarketSide'
        similarity:
          type: number
          format: double
          minimum: 0.82
          maximum: 1.0
          example: 0.94
        updated_at:
          type: string
          format: date-time
    SportsMatchedMarketSide:
      type: object
      required:
      - provider_id
      - provider
      - market_id
      - title
      - ticker
      - image
      - icon
      - expires_at
      - category
      properties:
        provider_id:
          type: integer
          example: 2
        provider:
          type: string
          example: polymarket
        market_id:
          type: string
          example: '587234'
        title:
          type:
          - string
          - 'null'
          description: Null if metadata for this side's market isn't available.
          example: Will the Celtics win?
        ticker:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
        icon:
          type:
          - string
          - 'null'
        expires_at:
          type:
          - string
          - 'null'
          format: date-time
        category:
          type:
          - string
          - 'null'
          description: Canonical title-cased category, or null when the market is uncategorised.
    SportsEnrichedMatchedMarketsResponse:
      allOf:
      - $ref: '#/components/schemas/SportsMatchedMarketsResponse'
      - type: object
        required:
        - pairs
        properties:
          pairs:
            type: array
            items:
              $ref: '#/components/schemas/SportsEnrichedMatchedMarketPair'
    SportsEnrichedMatchedMarketPair:
      allOf:
      - $ref: '#/components/schemas/SportsMatchedMarketPair'
      - type: object
        required:
        - a
        - b
        properties:
          a:
            $ref: '#/components/schemas/SportsEnrichedMatchedMarketSide'
          b:
            $ref: '#/components/schemas/SportsEnrichedMatchedMarketSide'
    SportsEnrichedMatchedMarketSide:
      allOf:
      - $ref: '#/components/schemas/SportsMatchedMarketSide'
      - type: object
        required:
        - details
        - pricing
        properties:
          details:
            description: Indexed market identifiers and outcomes, or null when no market row resolves.
            oneOf:
            - $ref: '#/components/schemas/MarketsDetail'
            - type: 'null'
          pricing:
            description: Current venue reference price, or null when unsupported or unavailable.
            oneOf:
            - $ref: '#/components/schemas/MarketsPrice'
            - type: 'null'
    PerpetualVenue:
      type: string
      description: Canonical perpetual venue identifier.
      enum:
      - hyperliquid
      - polymarket_perps
      - kalshi_margin
    PerpetualVenueCapabilities:
      type: object
      additionalProperties: false
      required:
      - venue
      - environment
      - market_data_service
      - instruments
      - book
      - trades
      - candles
      - funding
      - market_state
      - notes
      properties:
        venue:
          $ref: '#/components/schemas/PerpetualVenue'
        environment:
          type: string
          description: Venue-native environment name; do not infer it from Kairos staging.
        market_data_service:
          type: string
          const: agora
          description: Legacy compatibility identifier; live REST snapshots are owned by the Market Data API, not this metadata endpoint.
        instruments:
          type: boolean
        book:
          type: boolean
        trades:
          type: boolean
        candles:
          type: boolean
        funding:
          type: boolean
        market_state:
          type: boolean
        notes:
          type: array
          items:
            type: string
    HyperliquidPublicMetadata:
      type: object
      additionalProperties: false
      required:
      - kind
      - listing_namespace
      - margin_table_id
      - venue_margin_mode
      properties:
        kind:
          type: string
          const: hyperliquid
        listing_namespace:
          type: string
          const: validator_main_dex
        margin_table_id:
          type: integer
        venue_margin_mode:
          type:
          - string
          - 'null'
    PolymarketRiskTier:
      type: object
      additionalProperties: false
      required:
      - lower_bound
      - max_leverage
      properties:
        lower_bound:
          type: string
          description: Exact decimal-string lower bound for the tier.
        max_leverage:
          type: integer
    PolymarketPublicMetadata:
      type: object
      additionalProperties: false
      required:
      - kind
      - category
      - funding_interval
      - price_decimals
      - risk_tiers
      properties:
        kind:
          type: string
          const: polymarket_perps
        category:
          type: string
        funding_interval:
          type: string
        price_decimals:
          type: integer
        risk_tiers:
          type: array
          items:
            $ref: '#/components/schemas/PolymarketRiskTier'
    KalshiMarketSchedule:
      type: object
      additionalProperties: false
      required:
      - is_open
      - next_close_ts
      - next_open_ts
      properties:
        is_open:
          type: boolean
        next_close_ts:
          type:
          - integer
          - 'null'
          description: Venue Unix timestamp, or null when no next close is published.
        next_open_ts:
          type:
          - integer
          - 'null'
          description: Venue Unix timestamp, or null when no next open is published.
    KalshiSampledLeverage:
      type: object
      additionalProperties: false
      required:
      - semantics
      - leverage
      - sample_notional_usd
      properties:
        semantics:
          type: string
          const: sampled_estimate
        leverage:
          type: string
          description: Exact decimal-string leverage estimate.
        sample_notional_usd:
          type:
          - string
          - 'null'
          description: Exact decimal-string sample notional, or null.
    KalshiPublicMetadata:
      type: object
      additionalProperties: false
      required:
      - kind
      - title
      - fractional_trading_enabled
      - sampled_leverage
      - sampled_leverage_curve
      - schedule
      properties:
        kind:
          type: string
          const: kalshi_margin
        title:
          type: string
        fractional_trading_enabled:
          type: boolean
        sampled_leverage:
          oneOf:
          - $ref: '#/components/schemas/KalshiSampledLeverage'
          - type: 'null'
        sampled_leverage_curve:
          type: array
          items:
            $ref: '#/components/schemas/KalshiSampledLeverage'
        schedule:
          oneOf:
          - $ref: '#/components/schemas/KalshiMarketSchedule'
          - type: 'null'
    PublicInstrumentMetadata:
      oneOf:
      - $ref: '#/components/schemas/HyperliquidPublicMetadata'
      - $ref: '#/components/schemas/PolymarketPublicMetadata'
      - $ref: '#/components/schemas/KalshiPublicMetadata'
      discriminator:
        propertyName: kind
        mapping:
          hyperliquid: '#/components/schemas/HyperliquidPublicMetadata'
          polymarket_perps: '#/components/schemas/PolymarketPublicMetadata'
          kalshi_margin: '#/components/schemas/KalshiPublicMetadata'
    PerpetualInstrument:
      type: object
      additionalProperties: false
      required:
      - instrument_id
      - venue
      - integration_id
      - environment
      - venue_instrument_id
      - display_symbol
      - base_asset_id
      - quote_asset_id
      - collateral_asset_id
      - settlement_asset_id
      - native_quantity_unit
      - contract_multiplier
      - status
      - isolated_only
      - max_leverage
      - price_increment
      - size_increment
      - metadata
      properties:
        instrument_id:
          type: string
          description: |
            Stable Kairos instrument identity. Hyperliquid standard assets use
            `hl-mainnet-{base}-usdt`; HYPE and PURR use the USDC exception.
          examples:
          - hl-mainnet-btc-usdt
          - hl-mainnet-hype-usdc
          - hl-mainnet-purr-usdc
        venue:
          $ref: '#/components/schemas/PerpetualVenue'
        integration_id:
          type: string
          description: Product-specific routing and credential boundary.
        environment:
          type: string
        venue_instrument_id:
          type: string
          description: Exact identifier accepted by the venue and Market Data API snapshot endpoint.
        display_symbol:
          type: string
          examples:
          - BTC-USDT
          - HYPE-USDC
          - PURR-USDC
        base_asset_id:
          type: string
        quote_asset_id:
          type: string
          description: Hyperliquid uses USDT except for HYPE and PURR, which use USDC.
          examples:
          - USDT
          - USDC
        collateral_asset_id:
          type: string
        settlement_asset_id:
          type:
          - string
          - 'null'
        native_quantity_unit:
          type: string
          enum:
          - base_asset
          - contracts
        contract_multiplier:
          type:
          - string
          - 'null'
          description: Exact decimal string, or null when no authoritative conversion exists.
        status:
          type: string
          enum:
          - active
          - inactive
          - closed
          - delisted
          - unknown
        isolated_only:
          type:
          - boolean
          - 'null'
          description: Null when the venue does not publish an authoritative mode.
        max_leverage:
          type:
          - string
          - 'null'
          description: Exact decimal string when published by the venue.
        price_increment:
          type:
          - string
          - 'null'
          description: Exact venue price increment; null when not established.
        size_increment:
          type:
          - string
          - 'null'
          description: Exact venue quantity increment; null when not established.
        metadata:
          $ref: '#/components/schemas/PublicInstrumentMetadata'
    DiscoverTrendingWsMarket:
      type: object
      description: Trending market reduced to what a live-price subscriber needs.
      properties:
        id:
          type: string
        symbol:
          type:
          - string
          - 'null'
        title:
          type: string
        price:
          type: number
        volume24h:
          type: number
        provider:
          type: string
    DiscoverTrendingWsResponse:
      type: object
      required:
      - markets
      - count
      - timestamp
      properties:
        markets:
          type: array
          items:
            $ref: '#/components/schemas/DiscoverTrendingWsMarket'
        count:
          type: integer
        timestamp:
          type: string
    MarketClusterMember:
      type: object
      description: One market inside a cluster, after the venue allowlist and stale-status filter.
      required:
      - provider_id
      - provider
      - market_id
      properties:
        provider_id:
          type: integer
          description: Numeric provider id as stored.
        provider:
          type: string
          description: Provider name resolved from provider_id.
          example: polymarket
        market_id:
          type: string
        title:
          type:
          - string
          - 'null'
        ticker:
          type:
          - string
          - 'null'
        image:
          type:
          - string
          - 'null'
        icon:
          type:
          - string
          - 'null'
        expires_at:
          type:
          - string
          - 'null'
          description: ISO-8601 expiry, null when the venue publishes none.
    MarketCluster:
      type: object
      required:
      - cluster_id
      - floor
      - truncated
      - size
      - members
      properties:
        cluster_id:
          type: string
        floor:
          type: string
          description: Tier floor this membership was resolved at.
          enum:
          - exact
          - semantic
        truncated:
          type: boolean
          description: True when the cluster holds more members than the 32 returned.
        size:
          type: integer
          description: Cluster size as stored — counts members this response filtered out.
        members:
          type: array
          maxItems: 32
          items:
            $ref: '#/components/schemas/MarketClusterMember'
    MarketClustersResponse:
      type: object
      required:
      - clusters
      - count
      - floor
      properties:
        clusters:
          type: object
          description: |
            Keyed by the requested `<provider_id>:<market_id>` reference. A reference with no cluster, or whose cluster has fewer than two live members after filtering, is absent — the map is never padded with empties.
          additionalProperties:
            $ref: '#/components/schemas/MarketCluster'
        count:
          type: integer
          description: Number of entries in `clusters`.
        floor:
          type: string
          enum:
          - exact
          - semantic
    TxoddsTeamRef:
      type: object
      required:
      - code
      - name
      properties:
        code:
          type: string
        name:
          type: string
    TxoddsWinProb:
      type: object
      required:
      - home
      - draw
      - away
      properties:
        home:
          type: number
        draw:
          type: number
        away:
          type: number
    TxoddsFixtureListItem:
      type: object
      required:
      - fixtureId
      - competitionId
      - competition
      - home
      - away
      - kickoff
      properties:
        fixtureId:
          type: integer
        competitionId:
          type: integer
        competition:
          type: string
        home:
          $ref: '#/components/schemas/TxoddsTeamRef'
        away:
          $ref: '#/components/schemas/TxoddsTeamRef'
        kickoff:
          type: integer
          description: Kick-off, epoch milliseconds.
        winProb:
          oneOf:
          - $ref: '#/components/schemas/TxoddsWinProb'
          - type: 'null'
        status:
          type: string
          default: scheduled
          description: Derived from feed phase plus wall clock at cache time, so it can be up to one
            cache TTL stale.
        homeScore:
          type: integer
          default: 0
        awayScore:
          type: integer
          default: 0
        sport:
          type: string
          default: soccer
    TxoddsFixtureList:
      type: object
      required:
      - items
      - limit
      - offset
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TxoddsFixtureListItem'
        limit:
          type: integer
          description: Echo of the requested page size.
        offset:
          type: integer
    TxoddsScoreStats:
      type: object
      description: Latest score state. Soccer-shaped; gridiron fixtures carry their counters in `football`.
      required:
      - gameState
      - homeGoals
      - awayGoals
      - homeYellow
      - awayYellow
      - homeRed
      - awayRed
      - homeCorners
      - awayCorners
      - possessionHome
      - possessionAway
      - shotsHome
      - shotsAway
      - shotsOnTargetHome
      - shotsOnTargetAway
      - updateCount
      properties:
        gameState:
          type: string
        homeGoals:
          type: integer
        awayGoals:
          type: integer
        homeYellow:
          type: integer
        awayYellow:
          type: integer
        homeRed:
          type: integer
        awayRed:
          type: integer
        homeCorners:
          type: integer
        awayCorners:
          type: integer
        possessionHome:
          type:
          - number
          - 'null'
        possessionAway:
          type:
          - number
          - 'null'
        shotsHome:
          type:
          - integer
          - 'null'
        shotsAway:
          type:
          - integer
          - 'null'
        shotsOnTargetHome:
          type:
          - integer
          - 'null'
        shotsOnTargetAway:
          type:
          - integer
          - 'null'
        updateCount:
          type: integer
        currentHolder:
          type:
          - string
          - 'null'
        possessionThreat:
          type:
          - string
          - 'null'
    TxoddsTimelineEvent:
      type: object
      required:
      - ts
      - seq
      - minute
      - action
      - gameState
      - homeGoals
      - awayGoals
      - side
      - data
      properties:
        ts:
          type: integer
          description: Event time, epoch milliseconds.
        seq:
          type: integer
        minute:
          type:
          - integer
          - 'null'
        action:
          type: string
        gameState:
          type: string
        homeGoals:
          type: integer
        awayGoals:
          type: integer
        side:
          type:
          - string
          - 'null'
        data:
          type: string
        detail:
          type:
          - string
          - 'null'
    TxoddsMomentumPoint:
      type: object
      required:
      - minute
      - home
      - away
      properties:
        minute:
          type: integer
        home:
          type: integer
        away:
          type: integer
    TxoddsConversion:
      type: object
      required:
      - homeShots
      - awayShots
      - homeConversion
      - awayConversion
      properties:
        homeShots:
          type:
          - integer
          - 'null'
        awayShots:
          type:
          - integer
          - 'null'
        homeConversion:
          type:
          - number
          - 'null'
        awayConversion:
          type:
          - number
          - 'null'
    TxoddsTurnover:
      type: object
      required:
      - switches
      - perMinute
      properties:
        switches:
          type: integer
        perMinute:
          type:
          - number
          - 'null'
    TxoddsMetrics:
      type: object
      description: Possession-derived metrics. Empty shapes for gridiron fixtures, which emit no
        possession events, and for any part that failed while the rest of the detail loaded.
      required:
      - avgPossessionHome
      - avgPossessionAway
      - momentum
      - conversion
      - turnover
      properties:
        avgPossessionHome:
          type:
          - number
          - 'null'
        avgPossessionAway:
          type:
          - number
          - 'null'
        momentum:
          type: array
          items:
            $ref: '#/components/schemas/TxoddsMomentumPoint'
        conversion:
          $ref: '#/components/schemas/TxoddsConversion'
        turnover:
          $ref: '#/components/schemas/TxoddsTurnover'
    TxoddsFootballDown:
      type: object
      required:
      - number
      - yardsToGo
      - possession
      properties:
        number:
          type: integer
        yardsToGo:
          type: integer
        yardsToEndzone:
          type:
          - integer
          - 'null'
        possession:
          type: string
    TxoddsFootballPeriodScore:
      type: object
      required:
      - period
      - home
      - away
      properties:
        period:
          type: string
        home:
          type: integer
        away:
          type: integer
    TxoddsFootballState:
      type: object
      description: US-football situational state. Present only when `sport` is `usfootball`.
      required:
      - homePoints
      - awayPoints
      - touchdowns
      - fieldGoals
      - periodScores
      properties:
        homePoints:
          type: integer
        awayPoints:
          type: integer
        touchdowns:
          type: array
          items:
            type: integer
        fieldGoals:
          type: array
          items:
            type: integer
        onePointConversions:
          type: array
          items:
            type: integer
        twoPointConversions:
          type: array
          items:
            type: integer
        safeties:
          type: array
          items:
            type: integer
        periodScores:
          type: array
          items:
            $ref: '#/components/schemas/TxoddsFootballPeriodScore'
        down:
          oneOf:
          - $ref: '#/components/schemas/TxoddsFootballDown'
          - type: 'null'
        clockSeconds:
          type:
          - integer
          - 'null'
        clockRunning:
          type: boolean
          default: false
        redZone:
          type: boolean
          default: false
        phase:
          type: string
          default: ''
    TxoddsFixtureTiming:
      properties:
        fixtureId:
          title: Fixtureid
          type: integer
        competitionId:
          title: Competitionid
          type: integer
        sport:
          title: Sport
          type: string
        source:
          default: txodds
          title: Source
          type: string
        phase:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Phase
        status:
          default: unknown
          title: Status
          type: string
        observedAt:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Observedat
        evaluatedAt:
          title: Evaluatedat
          type: string
        stateAgeSeconds:
          anyOf:
          - type: number
          - type: 'null'
          default: null
          title: Stateageseconds
        staleAfterSeconds:
          default: 15
          title: Staleafterseconds
          type: integer
        stale:
          default: true
          title: Stale
          type: boolean
        clockSeconds:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Clockseconds
        clockRunning:
          anyOf:
          - type: boolean
          - type: 'null'
          default: null
          title: Clockrunning
        clockDirection:
          default: unknown
          title: Clockdirection
          type: string
        periodRemainingSeconds:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Periodremainingseconds
        regulationRemainingSeconds:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Regulationremainingseconds
        gameRemainingSeconds:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Gameremainingseconds
        elapsedMatchSeconds:
          anyOf:
          - type: integer
          - type: 'null'
          default: null
          title: Elapsedmatchseconds
        lateGameBasis:
          anyOf:
          - type: string
          - type: 'null'
          default: null
          title: Lategamebasis
        usableForLateGame:
          default: false
          title: Usableforlategame
          type: boolean
        unavailableReason:
          anyOf:
          - type: string
          - type: 'null'
          default: missing_score_observation
          title: Unavailablereason
      required:
      - fixtureId
      - competitionId
      - sport
      - evaluatedAt
      title: FixtureTiming
      type: object
      description: Source-observed fixture timing. Eligibility describes supported clock
        evidence, not whether a game is late or a market is safe to trade. Soccer exposes
        the nominal second-half clock, excluding any unknown added time; NFL exposes regulation
        clock time. No field predicts the final whistle or extrapolates between observations.
    TxoddsFixtureDetail:
      type: object
      description: Everything the match detail view renders, from the score event store.
      required:
      - fixtureId
      - competitionId
      - competition
      - home
      - away
      - kickoff
      - status
      - score
      - timeline
      - metrics
      properties:
        fixtureId:
          type: integer
        competitionId:
          type: integer
        competition:
          type: string
        home:
          $ref: '#/components/schemas/TxoddsTeamRef'
        away:
          $ref: '#/components/schemas/TxoddsTeamRef'
        kickoff:
          type: integer
        status:
          type: string
        sport:
          type: string
          default: soccer
        football:
          oneOf:
          - $ref: '#/components/schemas/TxoddsFootballState'
          - type: 'null'
        score:
          $ref: '#/components/schemas/TxoddsScoreStats'
        timeline:
          type: array
          items:
            $ref: '#/components/schemas/TxoddsTimelineEvent'
        metrics:
          $ref: '#/components/schemas/TxoddsMetrics'
    TxoddsWinProbSample:
      type: object
      required:
      - ts
      - minute
      - home
      - draw
      - away
      properties:
        ts:
          type: integer
        minute:
          type: integer
        home:
          type: number
        draw:
          type: number
        away:
          type: number
    TxoddsMarketRead:
      type: object
      required:
      - expectedGoals
      - supremacyHome
      - projHome
      - projAway
      properties:
        expectedGoals:
          type:
          - number
          - 'null'
        supremacyHome:
          type:
          - number
          - 'null'
        projHome:
          type:
          - number
          - 'null'
        projAway:
          type:
          - number
          - 'null'
    TxoddsFixtureTimeseries:
      type: object
      description: Odds-derived curves, split out from the detail because they read a table orders of
        magnitude larger than the score store.
      required:
      - fixtureId
      - winProbHistory
      - marketRead
      properties:
        fixtureId:
          type: integer
        winProbHistory:
          type: array
          items:
            $ref: '#/components/schemas/TxoddsWinProbSample'
        marketRead:
          $ref: '#/components/schemas/TxoddsMarketRead'
    ArbBetsStrategy:
      type: object
      description: The two-leg bet the vendor scored as best for this opportunity.
      properties:
        combination:
          type: string
        platform_1:
          type: string
        side_1:
          type: string
          enum:
          - Yes
          - No
        price_1:
          type: number
        bet_amount_1:
          type: number
        payout_1:
          type: number
        platform_2:
          type: string
        side_2:
          type: string
          enum:
          - Yes
          - No
        price_2:
          type: number
        bet_amount_2:
          type: number
        payout_2:
          type: number
        total_prob:
          type: number
          description: Combined implied probability. Below 1 is what makes the pair an arb.
        gross_profit:
          type: number
        net_profit:
          type: number
        roi_percent:
          type: number
        fees:
          type: number
    ArbBetsOpportunity:
      type: object
      properties:
        kalshi_id:
          type: string
        poly_clob_token_ids:
          type: array
          items:
            type: string
        market_name_a:
          type: string
        market_name_b:
          type: string
        platform_a:
          type: string
        platform_b:
          type: string
        url_a:
          type: string
        url_b:
          type: string
        volume_a:
          type: number
        volume_b:
          type: number
        best_arbitrage:
          $ref: '#/components/schemas/ArbBetsStrategy'
    ArbBetsResponse:
      type: object
      description: |
        Vendor payload, passed through unchanged. Fields are documented as observed — the vendor owns this shape and can change it without a Kairos deploy.
      properties:
        success:
          type: boolean
        generated_at:
          type: string
        investment_amount:
          type: number
        min_profit_filter:
          type: number
        total_markets_analyzed:
          type: integer
        total_arbitrages_found:
          type: integer
        markets_skipped:
          type: integer
        skip_reasons:
          type: object
          additionalProperties:
            type: integer
        profitable_count:
          type: integer
        profitable_arbitrages:
          type: array
          items:
            $ref: '#/components/schemas/ArbBetsOpportunity'
    MarketLinkVenue:
      type: object
      description: One venue's listing of a linked contract, in that venue's own keys.
      required:
      - provider
      - marketId
      - streamKey
      - tokenIdYes
      - tokenIdNo
      - executable
      properties:
        provider:
          type: string
          enum:
          - polymarket
          - predictfun
          - kalshi
          - hyperliquid
        marketId:
          type: string
          description: Executor-convention market id (Polymarket condition id, Predict.fun numeric id, Kalshi ticker, Hyperliquid outcome id).
        streamKey:
          type: string
          description: Stream-side id (Polymarket's numeric Gamma id; the market id elsewhere).
        marketIdNo:
          type: string
          description: The venue market that books the link's NO side where it is not `marketId` (Kalshi lists a game as one ticker per team). Present only on such legs.
        streamKeyNo:
          type: string
        tokenIdYes:
          type: string
          nullable: true
          description: The token the union book reads for the link's YES side on this venue.
        tokenIdNo:
          type: string
          nullable: true
        executable:
          type: boolean
          description: Whether `GET /orders/route-fees` reports this venue routable for the link; false for a display-only leg.
    MarketLinkRow:
      type: object
      description: One cross-venue link as the catalog publishes it.
      required:
      - id
      - title
      - image
      - league
      - sport
      - startTime
      - venues
      - syntheticIdYes
      - syntheticIdNo
      - executableVenues
      - rank
      - sideLabels
      - primary
      properties:
        id:
          type: string
          format: uuid
          description: The `MarketLink` id.
        title:
          type: string
          description: The event title from Discover's row for the primary venue, else the link's own title.
        image:
          type: string
          nullable: true
        league:
          type: string
          nullable: true
          description: League named by Discover's sports chip (e.g. `NFL`); null where the chip is the sport itself or the link is not sports.
        sport:
          type: string
          nullable: true
          description: Sport family (e.g. `Football`, `Soccer`).
        startTime:
          type: string
          format: date-time
          nullable: true
          description: Earliest leg expiration; venues close a game market at its scheduled start.
        slug:
          type: string
          description: The venue's game slug when Discover carries one; absent otherwise.
        venues:
          type: array
          minItems: 2
          maxItems: 4
          items:
            $ref: '#/components/schemas/MarketLinkVenue'
        syntheticIdYes:
          type: string
          description: Id of the union book for the link's YES side (`synthetic:<id>` on the stream).
        syntheticIdNo:
          type: string
        executableVenues:
          type: array
          items:
            type: string
          description: Providers whose legs are `executable`, in leg order.
        rank:
          type: number
          description: The largest Discover hourly-volume score across legs; the featured ordering key, never a displayed volume.
        sideLabels:
          type: object
          required:
          - 'yes'
          - 'no'
          properties:
            'yes':
              type: string
            'no':
              type: string
        primary:
          type: object
          required:
          - provider
          - marketId
          properties:
            provider:
              type: string
            marketId:
              type: string
          description: The leg whose Discover row supplied the title, else the first executable leg.
    MarketLinksFeaturedResponse:
      type: object
      required:
      - links
      - nextCursor
      properties:
        links:
          type: array
          items:
            $ref: '#/components/schemas/MarketLinkRow'
        nextCursor:
          type: string
          nullable: true
          description: Pass as `cursor` for the next page; null on the last page.
    MarketLinksLookupResponse:
      type: object
      required:
      - links
      properties:
        links:
          type: object
          description: Keyed by `<provider>:<marketId>` as requested; markets without a link are omitted.
          additionalProperties:
            $ref: '#/components/schemas/MarketLinkRow'
  responses:
    DataUnauthorized:
      description: |
        No valid credential presented — missing/invalid API-key headers, or an invalid/expired session token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataError'
          example:
            detail: Not authenticated
    DataValidationError:
      description: Request failed FastAPI parameter validation.
      content:
        application/json:
          schema:
            type: object
            required:
            - detail
            properties:
              detail:
                type: array
                description: One entry per failed field, with location, message, and error type.
                items:
                  type: object
                  additionalProperties: true
    DataForbidden:
      description: |
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataError'
          example:
            detail: Invite required
    DataRateLimited:
      description: |
        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.
      headers:
        Retry-After:
          schema:
            type: integer
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: 'Rate limit exceeded: 100 per 1 minute'
    DataPayloadTooLarge:
      description: |
        Request body exceeded the 8 MiB service-wide cap, refused before the handler ran.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DataError'
          example:
            detail: Request body too large
paths:
  /perpetuals/venues:
    get:
      operationId: listPerpetualVenues
      summary: List perpetual venue capabilities
      description: |
        Returns the supported perpetual venue identifiers and their public-data
        capabilities. This is metadata owned by the Python Data API. It does not
        serve books, trades, candles, funding observations, or WebSocket data.

        This preview exists only in Kairos staging. When the preview flag is
        disabled, the route fails closed with 404. Live REST snapshots are served
        separately by the Market Data API; anonymous WebSocket transport is served by
        `websocket_cpp`.
      tags:
      - Perpetuals
      x-kairos-auth: public
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      security: []
      servers:
      - url: https://staging-data.kairos.trade
        description: Staging preview only
      parameters: []
      responses:
        '200':
          description: Canonical venue capability records.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PerpetualVenueCapabilities'
        '404':
          description: The staging preview is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Perpetual metadata is disabled
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /perpetuals/instruments:
    get:
      operationId: listPerpetualInstruments
      summary: Discover perpetual instruments
      description: |
        Returns strict canonical instrument metadata, optionally for one venue.
        Financial values are exact decimal strings or null; consumers must not
        invent a contract multiplier, tick, or leverage value when one is absent.
        Results are cached independently per venue for 60 seconds.

        This endpoint owns discovery and instrument rules only. Fetch live market
        state from the Market Data API's REST snapshot endpoint and real-time transport from
        `websocket_cpp`. This preview exists only in Kairos staging and returns
        404 when disabled.
      tags:
      - Perpetuals
      x-kairos-auth: public
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      security: []
      servers:
      - url: https://staging-data.kairos.trade
        description: Staging preview only
      parameters:
      - name: venue
        in: query
        required: false
        description: Restrict discovery to one canonical venue.
        schema:
          $ref: '#/components/schemas/PerpetualVenue'
      responses:
        '200':
          description: Canonical perpetual instrument records.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PerpetualInstrument'
        '404':
          description: The staging preview is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Perpetual metadata is disabled
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Venue metadata was unavailable or failed strict validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Perpetual venue metadata is unavailable or invalid
  /markets/active:
    get:
      operationId: listActiveMarkets
      summary: Enumerate active markets for a provider (MMC cursor page)
      description: |
        Returns one cursor-paginated page of the active-market snapshot for a single provider. If the upstream listing is unavailable or the lookup otherwise fails, the route responds `503 Service Unavailable` ("Active market snapshot is temporarily unavailable") rather than fabricating an empty page — this can also happen for an exchange whose listing isn't ready yet. The response echoes back the validated/lowercased `provider` and a `source` field. `next_cursor` is opaque — pass it back verbatim as `cursor` to fetch the next page; `has_more: false` / `next_cursor: null` marks the last page.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: provider
        in: query
        required: false
        schema:
          type: string
          minLength: 1
          maxLength: 64
          default: polymarket
        description: |
          Provider/exchange id. Must be an active, known provider or the request is rejected with 400.
        example: polymarket
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 250
          default: 100
        example: 100
      - name: cursor
        in: query
        required: false
        schema:
          type: string
          maxLength: 512
        description: Opaque pagination cursor from a previous page's `next_cursor`.
      responses:
        '200':
          description: One page of active markets for the provider.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsActivePageResponse'
        '400':
          description: Unknown/inactive provider.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Invalid provider: foobar'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: No active-market listing is ready for this provider yet, or the upstream
            lookup failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Active market snapshot is temporarily unavailable
  /markets/details:
    post:
      operationId: getMarketDetails
      summary: Fetch market details (name/category/status/ids)
      description: |
        Looks up market details for the given `(market_id, provider_id)` pairs. The `market_id` in the request is matched against `market_id` OR `condition_id` OR `token_id` — any identifier works. Markets with an empty/missing `market_id` in the request are silently dropped before querying. Unmatched markets are simply absent from the response — there is no `found: false` sentinel here (contrast with `/markets/metadata/batch`). Hard cap: 500 markets per request.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketsDetailsRequest'
            example:
              markets:
              - market_id: KXBTC15M-26JUL221600-00
                provider_id: 1
              - market_id: 0x1234abcd...ef
                provider_id: 2
      responses:
        '200':
          description: Matched market details (unmatched inputs are simply omitted).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsDetailsResponse'
        '400':
          description: Missing/empty `markets` array, or more than 500 markets requested.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '413':
          $ref: '#/components/responses/DataPayloadTooLarge'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Query failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Failed to fetch details
  /markets/batch-prices:
    post:
      operationId: batchFetchMarketPrices
      summary: Batch fetch current prices for multiple markets
      description: |
        Fetches current price/volume/liquidity for up to 300 `(market_id, provider_id)` pairs. Results are cached briefly. Every reference must contain a non-empty market identifier and a known integer `provider_id`; an invalid reference rejects the request. A provider with no batch-price support is skipped (its markets are simply absent from the response, not an error). Response is a flat object keyed by the requested `market_id` — no top-level wrapper.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketsBatchPricesRequest'
            example:
              markets:
              - market_id: KXBTC15M-26JUL221600-00
                provider_id: 1
              - market_id: 0x1234abcd...ef
                provider_id: 2
      responses:
        '200':
          description: Prices keyed by the requested `market_id`. Markets that could not be priced are
            simply absent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsBatchPricesResponse'
        '400':
          description: Missing/empty `markets` array, malformed market reference, unknown provider, or more than 300 markets requested.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '413':
          $ref: '#/components/responses/DataPayloadTooLarge'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Upstream provider fetch failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Batch fetch failed
  /markets/metadata:
    get:
      operationId: getMarketMetadata
      summary: Get full market metadata (rules, images, contract spec)
      description: |
        Returns the full metadata document for one market. Resolution order: (1) a fast cached-metadata path — for Polymarket, a cache hit derives status/category/images from the cached upstream payload; (2) on a miss, falls back to a live provider-API fetch (Kalshi ticker + event metadata for images; Polymarket; Opinion; predict.fun) — this covers markets too new to be indexed yet (e.g. short-lived 15-minute crypto markets); (3) if the cache hit is non-null but carries **empty** `resolution_rules` (both `primary` and `secondary` blank), an extra provider-API call backfills `resolution_rules` (and `description` if also empty) without discarding the rest of the cached document. Only the cached-metadata path populates `condition_id`/`event_id`/`contract.settlement_ts` — the live provider-API fallback leaves those `null`. Finally, for `polymarket`/`predictfun`, a best-effort liquidity-rewards enrichment mutates `extra` in place (never blocks or fails the response): Polymarket sets `extra.clobRewards` (`[{rewardsDailyRate}]` or `[]`), `extra.rewardsMaxSpread`, `extra.rewardsMinSize`; predict.fun sets `extra.rewards.current` (an active reward-window object, or `null`). 404 only if no source has the market at all.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: ticker
        in: query
        required: true
        schema:
          type: string
        description: Market ticker/ID.
        example: KXBTC15M-26JUL221600-00
      - name: provider
        in: query
        required: true
        schema:
          type: string
        description: kalshi or polymarket (also accepts any other active provider — e.g. predictfun,
          opinion — via the API fallback path).
        example: polymarket
      responses:
        '200':
          description: Full market metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsMetadataResponse'
        '400':
          description: Invalid/unknown provider, or a downstream `ValueError`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '404':
          description: Market not found in cached metadata or any provider API.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Market not found: KXFOO-99'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Metadata lookup failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Metadata fetch failed
  /markets/metadata/batch:
    post:
      operationId: getMarketMetadataBatch
      summary: Batch fetch market metadata for multiple contracts
      description: |
        Batch variant of `/markets/metadata`, capped at 100 contracts. Contracts whose provider is currently inactive are dropped before processing; if that empties the batch the response is `{}`. Looks up the rest via a cached-metadata fast path, with a fallback for cache misses and non-Polymarket providers. Unless `titles_only: true` is set, three further best-effort enrichment passes run (each independently swallows its own errors and never fails the request):
          1. **API fallback** — any contract still `found: false` is retried
             against the live provider API (same fallback `/markets/metadata`
             uses), for markets too new to be indexed.
          2. **Category inference** — markets with no `category` get one
             inferred from Kalshi crypto ticker patterns or crypto keywords
             in the title (bitcoin/ethereum/solana/xrp/"btc "/"eth "/"sol ").
          3. **Tag icon enrichment** — resolves market-to-platform-tag
             mappings and assigns the highest-precedence tag's icon as
             `tag_icon`; can also backfill `category` from the tag's slug
             (crypto/politics/sports/esports/finance/tech/world).
        Note: Polymarket order-book liquidity-rewards enrichment (used by the single-market `/markets/metadata`) is intentionally **not** run here — this endpoint feeds list views where the rewards badge doesn't render.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarketsBatchMetadataRequest'
            example:
              contracts:
              - ticker: KXBTC15M-26JUL221600-00
                provider: kalshi
              - ticker: 0x1234abcd...ef
                provider: polymarket
              titles_only: false
      responses:
        '200':
          description: 'Metadata items keyed by the requested ticker. Every requested ticker (whose provider
            wasn''t hidden) appears, with `found: false` for unresolved ones.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsBatchMetadataResponse'
        '400':
          description: Missing/empty `contracts` array, or more than 100 contracts requested.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '413':
          $ref: '#/components/responses/DataPayloadTooLarge'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Metadata lookup failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Batch fetch failed
  /markets/outcomes:
    get:
      operationId: getMarketOutcomes
      summary: Get all sibling outcomes for the event containing a market
      description: |
        Returns every outcome in the event that `market_id` belongs to (for multi-outcome / grouped markets), served from a cache of pre-computed event/market lookups. Any identifier (market_id, token_id, condition_id, symbol, slug) is mapped to its `event_id`; if that misses, falls back to the per-market entry's embedded `event_id`, then to deriving a Kalshi-style event ticker by stripping the last `-SEGMENT` off `market_id` (e.g. `KXPGATOUR-THGI26-SSTR` → `KXPGATOUR-THGI26`). If no event can be resolved at all and `provider=polymarket`, falls back to a live Polymarket lookup — guarding against a known upstream quirk that can silently return an unrelated default result instead of an empty list. Non-Polymarket providers get no such fallback — an unresolvable event returns the empty shape. `all_ids` (comma-separated, ≤200 ids, each ≤128 chars) additionally resolves `event_groups` — a map from every supplied id (and every sibling id discovered while resolving events) to its `event_id`, used by the frontend to dedupe sibling contracts across dropdowns. **Price scale note:** unlike most of this API, outcome prices here are returned as a **0–1 decimal probability**, not the platform's usual 0–100 cents scale.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: market_id
        in: query
        required: true
        schema:
          type: string
          maxLength: 128
        description: Market ID / ticker / token_id / slug. Non-empty after trimming, ≤128 chars.
        example: KXPGATOUR-THGI26-SSTR
      - name: provider
        in: query
        required: true
        schema:
          type: string
        description: Provider name; must be an active provider.
        example: kalshi
      - name: all_ids
        in: query
        required: false
        schema:
          type: string
        description: Comma-separated list of additional contract ids (≤200 items, each ≤128 chars) to
          resolve into `event_groups` for dropdown deduplication.
      responses:
        '200':
          description: Event outcomes for the market, or the empty shape if no event could be resolved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsOutcomesResponse'
        '400':
          description: Invalid/unknown provider, empty `market_id`, an oversized identifier, or too many
            `all_ids`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /markets/crypto:
    get:
      operationId: getCryptoMarkets
      summary: Get 5m/15m/1h crypto up-or-down prediction markets for a window
      description: |
        Returns the crypto up/down markets (BTC/ETH/SOL/XRP on Kalshi + Polymarket, plus DOGE/HYPE/BNB and predict.fun on some intervals) for one time window. `window_offset` shifts by whole windows of the chosen `interval` (0=current, -1=previous, +1=next, etc.). Responses are cached so UI "previous/next window" navigation doesn't repeatedly re-fetch from upstream. Past windows are immutable and cached longer; future/current windows are refreshed more often since they're still filling in. A window is only cached once it has enough data from both venues; past windows relax that requirement for Kalshi, since Kalshi removes settled contracts from its API. **Price scale note:** `price` is a raw **0–1 decimal** probability read/derived directly from each venue's order book — NOT the platform's usual 0–100 cents scale.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: interval
        in: query
        required: false
        schema:
          type: string
          enum:
          - 5m
          - 15m
          - 1h
          default: 15m
        example: 15m
      - name: window_offset
        in: query
        required: false
        schema:
          type: integer
          minimum: -10000
          maximum: 10000
          default: 0
        description: Number of windows from current (0=now, -1=previous, +1=next). Bounded so an absurd
          offset is rejected with 422 rather than overflowing the window arithmetic.
        example: 0
      responses:
        '200':
          description: Crypto markets for the requested window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsCryptoResponse'
        '400':
          description: Invalid `interval` (must be 5m, 15m, or 1h).
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Invalid interval: 30m. Must be one of: [''15m'', ''1h'', ''5m'']'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Upstream fetch (Kalshi/Polymarket/predict.fun) failed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Failed to fetch crypto markets
  /markets/crypto/oracle-history:
    get:
      operationId: getOraclePriceHistory
      summary: Get historical oracle (spot) prices for crypto chart pre-population
      description: |
        Returns per-symbol resolution-price history over `[end_ms - minutes*60*1000, end_ms]`
        (`end_ms` defaults to now; clamped to now if a future value is given). Each request
        names **one** oracle `source` so settlement feeds are never mixed. Symbols must be
        the wire ids for that source (bare `btc-usd` for Binance;
        `btc-usd-polymarket-chainlink`, `btc-usd-kalshi-cfb`, `btc-usd-hyperliquid-mark`,
        or a `-polymarket-twap30`/`-polymarket-twap60` suffix for venue series). When
        `source` is omitted, it is inferred from the wire symbols — mixed sources are
        rejected. Only Binance gaps may be backfilled from Binance REST; every other
        source returns an empty array for a missing span. Responses are cached briefly.
        The internal store can lag real time by a few seconds.
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: symbols
        in: query
        required: true
        schema:
          type: string
        description: |
          Comma-separated, case-insensitive wire symbols for a single source.
          Logical assets are btc-usd, eth-usd, sol-usd, xrp-usd, doge-usd, hype-usd,
          bnb-usd; venue sources append their suffix (for example
          `btc-usd-polymarket-chainlink`). Kalshi CF Benchmarks supports BTC, ETH, SOL, XRP, DOGE, HYPE, and BNB.
        example: btc-usd
      - name: source
        in: query
        required: false
        schema:
          type: string
          enum:
          - binance
          - hyperliquid_mark
          - kalshi_cfbenchmarks
          - polymarket_chainlink
          - polymarket_twap30
          - polymarket_twap60
        description: |
          Resolution feed identity. Omit only for legacy clients that encode the
          source in the wire symbol; new callers should pass it explicitly.
        example: binance
      - name: minutes
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1440
          default: 10
        description: Minutes of history to fetch (max 24h).
      - name: full_resolution
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: Return raw 1-second history without downsampling.
      - name: end_ms
        in: query
        required: false
        schema:
          type: integer
        description: End timestamp in epoch ms (clamped to server "now"). Omit for the live edge. Used
          for chunked/paginated backward loading.
      responses:
        '200':
          description: Price-history points per symbol, sorted ascending by timestamp.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsOracleHistoryResponse'
        '400':
          description: |
            No symbols given, unknown `source`, symbols that do not belong to the
            requested source, mixed-source inference failure, or a non-positive
            `end_ms`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /markets/crypto/ptb:
    get:
      operationId: getCryptoPriceToBeat
      summary: Get Price-to-Beat (PTB) values for crypto up/down contracts
      description: |
        Returns the oracle price captured at the start of each requested window
        ("the price to beat"), nested `{window: {symbol: {...}}}`. `windows` is
        comma-separated; each must be one of `1m, 5m, 15m, 1h, 4h, 1d`. Window
        starts align to clean ET boundaries. Only **window** PTB sources are
        accepted: `binance`, `kalshi_cfbenchmarks`, `polymarket_twap30`,
        `polymarket_twap60`. Contract-tick series (`polymarket_chainlink`,
        `hyperliquid_mark`) are not window benchmarks and are rejected. When
        `source` is omitted, the server picks Binance or a legacy TWAP series
        from the `oracle_twap_feed` flag. Binance may fall back to an external
        market-data source for missing symbols; venue series do not. Symbols
        with no resolvable price for a window are **omitted** (not `null`).
      tags:
      - Markets
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: windows
        in: query
        required: false
        schema:
          type: string
          default: 15m
        description: Comma-separated windows. Each must be one of 1m, 5m, 15m, 1h, 4h, 1d.
        example: 5m,15m
      - name: source
        in: query
        required: false
        schema:
          type: string
          enum:
          - binance
          - kalshi_cfbenchmarks
          - polymarket_twap30
          - polymarket_twap60
        description: |
          ET-aligned window source. New market clients should pass this
          explicitly. TWAP30 remains for legacy 1m overlays.
        example: kalshi_cfbenchmarks
      responses:
        '200':
          description: PTB values nested by window then symbol.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsPtbResponse'
        '400':
          description: No windows given, or an invalid window value.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Invalid window: 30m. Must be one of: [''15m'', ''1d'', ''1h'', ''1m'', ''4h'',
                      ''5m'']'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Oracle store/source lookup failed for one or more windows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Failed to fetch price-to-beat values
  /markets/equity/snapshot:
    get:
      operationId: getEquitySnapshot
      summary: Get last-known prices for equity/forex/commodity symbols
      description: |
        Returns last-known prices for a fixed allow-list of stock, ETF, forex, and commodity symbols (e.g. AAPL, SPY, EURUSD, XAUUSD), mapped internally to their upstream tickers. A request is served straight from cache only if every requested symbol is already cached. Otherwise, missing symbols are fetched and merged into the existing cache, and only the newly-fetched subset is returned with `cached: false` — symbols already warm in the cache are NOT re-included in that response body (the endpoint returns either the full cached hit set with `cached: true`, or just the freshly-fetched symbols with `cached: false`, never a merge of both in one response). Returns prices even when markets are closed (uses the last regular-session print) so the UI is never blank outside trading hours. **Auth differs from every other endpoint in this router**: this accepts a first-party session JWT (`Authorization: Bearer ...`) or an internal admin credential — it does **not** accept the `X-Client-Id`/`X-Api-Key`/ `X-Api-Secret` API-key headers that the rest of `/markets/*` accepts.
      tags:
      - Markets
      x-kairos-auth: session
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      security:
      - bearerAuth: []
      parameters:
      - name: symbols
        in: query
        required: true
        schema:
          type: string
        description: |
          Comma-separated symbols (case-insensitive, upper-cased server-side). Must be a subset of: AAPL, TSLA, MSFT, GOOGL, AMZN, META, NVDA, NFLX, PLTR, OPEN, RKLB, ABNB, COIN, HOOD, SPY, QQQ, EWY, VXX, EURUSD, GBPUSD, USDCAD, USDJPY, USDKRW, XAUUSD, XAGUSD, WTI, CC, NGD.
        example: AAPL,TSLA,EURUSD
      responses:
        '200':
          description: Last-known prices for the requested symbols.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsEquitySnapshotResponse'
        '400':
          description: No symbols given, or a symbol outside the supported allow-list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Invalid symbol: DOGEUSD. Must be one of: [...]'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /candles:
    get:
      operationId: getCandles
      summary: Fetch OHLCV candles for a single contract
      tags:
      - Candles
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: candles
      description: |
        Returns OHLCV candle bars for one `(provider, contract_id, outcome)` series over `[start, end)`, bucketed at `timeframe_seconds`.

        **Auth**: accepts an admin secret (`X-Admin-Secret`), an API key (`X-Client-Id` + `X-Api-Key` + `X-Api-Secret`), or a first-party JWT (`Authorization: Bearer` or `turnkey_session_jwt` cookie). No API-key scope is required for this endpoint — any valid credential can read candles.

        **Cache / data source**: responses may be served from cache unless `rebuild=true`, which always bypasses the cache and reconstructs candles from the underlying trade/candle data, then repopulates the cache. `rebuild=true` is for callers that explicitly want fresh data regardless of what's cached.

        **Validation** (all return 400): unknown `provider` (must exist in the live provider registry — currently includes `kalshi`, `polymarket`, `dome`, `opinion`); empty or >128-char `contract_id`; `timeframe_seconds` not one of the fixed allowed values; unparseable `start`/`end` (ISO 8601); or `end <= start`. An `outcome` index invalid for the given provider/contract (e.g. an out-of-range index on a binary market) also returns 400.
      parameters:
      - name: provider
        in: query
        required: true
        schema:
          type: string
        description: |
          Venue identifier, matched case-insensitively against the live provider registry (e.g. `kalshi`, `polymarket`). Unknown providers return 400.
        example: kalshi
      - name: contract_id
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
        description: Provider-specific contract/market/token identifier. Whitespace is trimmed; empty
          or >128 chars returns 400.
        example: KXPRESPOLAND-24-DT
      - name: timeframe_seconds
        in: query
        required: true
        schema:
          type: integer
          enum:
          - 1
          - 60
          - 300
          - 900
          - 3600
          - 14400
          - 86400
        description: |
          Candle bucket width in seconds. FastAPI first enforces `>= 1`; the handler then rejects any value not exactly one of `[1, 60, 300, 900, 3600, 14400, 86400]` (1s, 1m, 5m, 15m, 1h, 4h, 1d) with 400.
        example: 60
      - name: start
        in: query
        required: true
        schema:
          type: string
          format: date-time
        description: Window start, ISO 8601 (any offset; normalized to UTC internally). Unparseable values
          return 400.
        example: '2026-07-21T00:00:00Z'
      - name: end
        in: query
        required: true
        schema:
          type: string
          format: date-time
        description: Window end, ISO 8601. Must be strictly after `start` (400 otherwise).
        example: '2026-07-22T00:00:00Z'
      - name: outcome
        in: query
        required: false
        schema:
          type:
          - integer
          - 'null'
          minimum: 0
        description: |
          Zero-based outcome index for multi-outcome markets. Omitted/null is treated as `0` (the first/primary outcome). An index invalid for the contract's outcome count returns 400.
      - name: rebuild
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: When true, bypasses the cache and forces a fresh fetch/rebuild.
      responses:
        '200':
          description: OHLCV series for the requested window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandlesSeriesResponse'
        '400':
          description: |
            Invalid request — unknown provider, invalid/missing contract_id, disallowed timeframe_seconds, unparseable or out-of-order start/end, or an outcome index invalid for this contract.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'Invalid timeframe. Must be one of: [1, 60, 300, 900, 3600, 14400, 86400]'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Candle fetch failed (cache or ClickHouse read error).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /candles/batch:
    post:
      operationId: postCandlesBatch
      summary: Fetch candles for up to 200 contracts in one request
      tags:
      - Candles
      x-kairos-auth: api-key
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: candles
      description: |
        Batched form of `GET /candles`. Accepts up to 200 per-contract requests and returns results in the same order, index-tagged.

        **Auth**: same as `GET /candles` — `require_auth_or_admin_or_apikey`, no scope required.

        **Cache / data source**: each item is checked against the cache in parallel first; only uncached items are fetched fresh. Items with `rebuild: true` always skip the cache regardless of a hit.

        **Fail-fast, partial-failure semantics**: an empty series for an item is returned as `"candles": []` (not an error) — the client renders an empty chart and expects later bars via the live WebSocket feed; there is no server-side ingestion retry. If fetching fails for some items but others were already served from cache, those cached items are still returned; every failed item instead gets `"candles": [], "error": "<message>"` so the client can distinguish a genuine failure from a legitimately empty window. Only if **no** item could be served at all does the whole request fail with 500.

        **Validation** (all 400): body must contain a JSON array under `requests` (or the legacy alias `items`); the array must be non-empty and at most 200 items; each item must be an object with `provider`, `contract_id`, `timeframe_seconds`, `start`, and `end` present; the same per-field checks as `GET /candles` apply per item (unknown provider, invalid contract_id, disallowed timeframe, unparseable/out-of-order timestamps, negative or non-integer `outcome`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CandlesBatchRequest'
            example:
              requests:
              - provider: kalshi
                contract_id: KXPRESPOLAND-24-DT
                timeframe_seconds: 60
                start: '2026-07-21T00:00:00Z'
                end: '2026-07-22T00:00:00Z'
                outcome: 0
                rebuild: false
              - provider: polymarket
                contract_id: 0xabc123...token
                timeframe_seconds: 3600
                start: '2026-07-15T00:00:00Z'
                end: '2026-07-22T00:00:00Z'
      responses:
        '200':
          description: |
            Index-ordered results, one per request item. A failed item carries an `error` string and an empty `candles` array instead of a 5xx for the whole batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandlesBatchResponse'
        '400':
          description: |
            Malformed batch — `requests` missing/not an array/empty/over 200 items, or a specific item is malformed (missing required field, unknown provider, invalid contract_id, disallowed timeframe_seconds, unparseable/out-of-order start/end, non-integer or negative outcome).
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Max 200 requests
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '413':
          $ref: '#/components/responses/DataPayloadTooLarge'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Every request in the batch failed; nothing could be served from cache.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /trades/kalshi:
    get:
      operationId: getKalshiTrades
      summary: Proxy Kalshi's public trade-history API for one market
      tags:
      - Trades
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Thin proxy over Kalshi's `GET /trade-api/v2/markets/trades`, with a short-lived cache and Kairos-computed metrics appended.

        **Auth**: requires the `trade:read` scope. Admin-secret and first-party JWT callers are always allowed through with no scope check. API-key callers must have `trade:read` in their credential's `scopes` array, or this returns 403 (`API key missing required scope: trade:read`). API-key callers are additionally gated by the `kalshi` platform's API access being enabled — if it's disabled, this returns 403 regardless of scope.

        **Cache / data source**: responses are cached briefly to de-duplicate bursts of identical requests, then fall through to the live Kalshi API at `https://api.elections.kalshi.com/trade-api/v2/markets/trades`. A Kalshi HTTP error (non-2xx or network failure) returns 502.

        **Response shape**: the raw Kalshi API response object is returned unmodified except for an injected `metrics` field, computed over the *locally time-filtered* trades (filtering by `min_ts`/`max_ts` is done client-side against each trade's `created_time`, since Kalshi's API does not filter server-side beyond `min_ts`/`max_ts` passthrough). The metrics window is fixed at 24 hours regardless of the requested `min_ts`/`max_ts` span.
      parameters:
      - name: ticker
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
        description: Kalshi market ticker. Empty or >128 chars returns 400.
        example: KXPRESPOLAND-24-DT
      - name: min_ts
        in: query
        required: false
        schema:
          type: integer
          nullable: true
        description: Inclusive lower bound, Unix seconds, applied both to the upstream request and to
          client-side filtering by `created_time`.
      - name: max_ts
        in: query
        required: false
        schema:
          type: integer
          nullable: true
        description: Inclusive upper bound, Unix seconds, applied both to the upstream request and to
          client-side filtering by `created_time`.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 100
        description: Max trades returned, passed through to the upstream Kalshi API.
      - name: cursor
        in: query
        required: false
        schema:
          type: string
          maxLength: 512
          nullable: true
        description: Upstream pagination cursor from a previous response. Blank strings are treated as
          absent; >512 chars returns 400.
      responses:
        '200':
          description: Raw Kalshi trade-history payload plus a computed `metrics` object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradesKalshiResponse'
        '400':
          description: Missing/empty `ticker`, or `cursor` too long.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API-key credential lacks the `trade:read` scope, or API access to the `kalshi`
            platform is currently disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: 'API key missing required scope: trade:read'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Kalshi's upstream trade-history API returned an error or was unreachable.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Kalshi API error
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /trades/polymarket:
    get:
      operationId: getPolymarketTrades
      summary: Proxy Polymarket's public trade-history API for one market
      tags:
      - Trades
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Thin proxy over Polymarket's `GET https://data-api.polymarket.com/trades`, with a short-lived cache and Kairos-computed metrics appended.

        **Auth**: identical model to `/trades/kalshi` — requires the `trade:read` scope (admin/JWT bypass the scope check; API keys need `trade:read`), plus API access to the `polymarket` platform being enabled for API-key callers (403 if disabled).

        **Market resolution**: `market` may be a `0x`-prefixed condition ID (used as-is) or a market/token identifier that is resolved to a condition ID via a cached metadata lookup, then a direct database lookup, and finally a live Polymarket API lookup as a last resort. If resolution never yields a value starting with `0x`, the endpoint short-circuits and returns `{"trades": [], "metrics": {}}` **without** calling the upstream trades API.

        **Cache / data source**: responses are cached briefly to de-duplicate bursts of identical requests. On a miss, calls the Polymarket Data API with the resolved `condition_id` as `market`. A non-2xx/network error returns 502.

        **Response shape**: `trades` is the raw upstream trade array (client-side re-filtered by `after` against each trade's `timestamp` as a safety net). `metrics` is computed over the filtered trades with a metrics window fixed at 24 hours; it groups trade value by outcome identifier (`asset_id`/`outcome`/`token_id`) and keeps only the top 2 outcomes by volume as an approximate outcome_0/outcome_1 split — this is an approximation, not a guaranteed yes/no mapping.
      parameters:
      - name: market
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
        description: |
          Polymarket condition ID (`0x...`), market ID, or token ID. Non-`0x` values are resolved to a condition ID before querying upstream. Empty or >128 chars returns 400.
        example: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd'
      - name: after
        in: query
        required: false
        schema:
          type: integer
          nullable: true
        description: Inclusive lower bound, Unix seconds. Passed to upstream and re-applied client-side
          against each trade's `timestamp`.
      - name: before
        in: query
        required: false
        schema:
          type: integer
          nullable: true
        description: Inclusive upper bound, Unix seconds, passed to the upstream API.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 100
        description: Max trades returned, passed through to the upstream Polymarket API.
      responses:
        '200':
          description: |
            Raw Polymarket trades plus computed `metrics`. `metrics` is `{}` when the `market` value could not be resolved to a condition ID (no upstream call was made).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradesPolymarketResponse'
        '400':
          description: Missing/empty `market`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API-key credential lacks the `trade:read` scope, or API access to the `polymarket`
            platform is currently disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Polymarket's upstream trade-history API returned an error or was unreachable.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Polymarket API error
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /trades/history:
    get:
      operationId: getTradeHistory
      summary: Fetch normalized, Kairos-ingested trade history for a contract
      tags:
      - Trades
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Returns normalized trade history for a single `(provider, contract_id)` from Kairos's own ingested trade store. Unlike `/trades/kalshi` and `/trades/polymarket`, this is not a live upstream proxy — it serves from data Kairos has already ingested, so coverage depends on how much history has been backfilled/streamed for that contract.

        **Auth**: requires the `trade:read` scope (admin/JWT bypass the scope check; API keys need `trade:read`), plus API access to the requested provider being enabled for API-key callers — 403 if disabled.

        **Data source / staleness**: reads ingested trade rows for the window `[before - window_seconds, before]` (or ending "now" if `before` is omitted). If `trigger_ingest=true` and the data is missing or stale, an ingestion job is kicked off for that contract and reported via the `indexing` flag in the response — the returned trades may still be incomplete for that call.
      parameters:
      - name: provider
        in: query
        required: true
        schema:
          type: string
        description: Venue identifier, matched case-insensitively against the live provider registry.
        example: kalshi
      - name: contract_id
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
        description: Provider-specific contract identifier. Empty or >128 chars returns 400.
      - name: window_seconds
        in: query
        required: false
        schema:
          type: integer
          minimum: 3600
          maximum: 86400
          default: 86400
        description: Lookback window in seconds, ending at `before` (or now). Clamped to [3600, 86400]
          (1 hour to 24 hours).
      - name: before
        in: query
        required: false
        schema:
          type: integer
          nullable: true
        description: Unix-seconds cursor — return trades at or before this timestamp. Omit to end the
          window at "now".
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 500
          nullable: true
        description: Max trades returned. If omitted, the service applies its own internal default.
      - name: trigger_ingest
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: If true, kicks off ingestion when data for this window is stale or missing, rather
          than just returning what's already stored.
      responses:
        '200':
          description: Normalized trade rows for the window plus pagination/coverage metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradesHistoryResponse'
        '400':
          description: Invalid trade history request (unknown provider, invalid contract_id, or a request
            the service itself rejected as malformed).
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Invalid trade history request
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API-key credential lacks the `trade:read` scope, or API access to the requested
            provider is currently disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Trade history fetch failed on the backend.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Trade history fetch failed
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /trades/metrics:
    get:
      operationId: getTradeMetrics
      summary: Fetch volume and outcome-pressure metrics for a contract
      tags:
      - Trades
      x-kairos-auth: api-key
      x-kairos-scope: trade:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Returns aggregate trade metrics (volume, per-outcome volume split, trade count) for a single `(provider, contract_id)` over a lookback window, computed from Kairos's own ingested trade store — same data source as `/trades/history`, not a live upstream proxy. There is no buy/sell-side breakdown: metrics are split by outcome (`outcome_0`/`outcome_1`) rather than trade direction, since trade direction cannot be reliably determined from exchange data for all providers.

        **Auth**: requires the `trade:read` scope (admin/JWT bypass the scope check; API keys need `trade:read`), plus API access to the requested provider being enabled for API-key callers — 403 if disabled.

        **Staleness**: if `trigger_ingest=true` and ingestion coverage for the window is low, the service triggers a backfill/ingestion job and reports it via the `indexing` flag on the returned metrics object.
      parameters:
      - name: provider
        in: query
        required: true
        schema:
          type: string
        description: Venue identifier, matched case-insensitively against the live provider registry.
        example: kalshi
      - name: contract_id
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 128
        description: Provider-specific contract identifier. Empty or >128 chars returns 400.
      - name: window_seconds
        in: query
        required: false
        schema:
          type: integer
          minimum: 3600
          maximum: 86400
          default: 86400
        description: Lookback window in seconds, ending "now". Clamped to [3600, 86400] (1 hour to 24
          hours).
      - name: trigger_ingest
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: If true, triggers ingestion when coverage for this window is low, rather than computing
          metrics from partial/stale data alone.
      responses:
        '200':
          description: Aggregate volume/pressure metrics for the window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradesMetricsResponse'
        '400':
          description: Invalid trade metrics request (unknown provider, invalid contract_id, or a request
            the service itself rejected as malformed).
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Invalid trade metrics request
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API-key credential lacks the `trade:read` scope, or API access to the requested
            provider is currently disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Trade metrics fetch failed on the backend.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Trade metrics fetch failed
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /trader-stats/pnl-history/{wallet_address}:
    get:
      operationId: getTraderPnlHistory
      summary: Get a trader's realized-PnL time series
      tags:
      - Trader Analytics
      x-kairos-auth: public
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      security: []
      description: |
        Returns a time-series of realized PnL data points for charting a trader's PnL progression over the requested window, bucketed by `time_range`. The wallet address is validated as either an Ethereum (0x...) address (Polymarket/Opinion/predictfun) or a Solana base58 address (Kalshi) and normalized (lower-cased for EVM) before lookup; if it matches neither format the request is rejected with 400. `provider` is optional — if omitted, the provider is auto-detected from the address format; pass it explicitly to disambiguate EVM venues other than Polymarket. Provider values are validated against the live set of active providers rather than a fixed enum, so an inactive/unknown id is rejected with 400. Responses are cacheable at the edge and briefly cached server-side. **Public endpoint — no authentication required.** Rate-limited per client IP (X-Forwarded-For, falling back to the socket peer) at 30 requests/minute, independent of the global 100/minute default.
      parameters:
      - name: wallet_address
        in: path
        required: true
        schema:
          type: string
        description: |
          Ethereum (0x-prefixed, 42 chars) or Solana (base58) wallet address. Normalized (EVM lower-cased) before use. Returns 400 if it matches neither format.
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: time_range
        in: query
        required: false
        schema:
          type: string
          enum:
          - 1D
          - 1W
          - 1M
          - ALL
          default: ALL
        description: Time window for the PnL series.
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: |
          Provider override (e.g. polymarket, kalshi, opinion, predictfun). Validated against the live active-provider registry. Auto-detected from address format when omitted.
        example: polymarket
      responses:
        '200':
          description: PnL time series for the wallet and time range.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderPnlHistoryResponse'
        '400':
          description: Malformed wallet address or unrecognized provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching PnL history.
  /trader-stats/summary/{wallet_address}:
    get:
      operationId: getTraderSummary
      summary: Get a trader's performance stats without the position list
      tags:
      - Trader Analytics
      x-kairos-auth: public
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      security: []
      description: |
        Returns the stats a trader profile's header and stats panel show (PnL, ROI, win rate, volume, position counts, positions_value) without the position list, so they render before the positions page. The figures match `performance` on `/trader-stats/positions`. `performance` is null for a venue whose stats come only from the full positions build; read them from `/trader-stats/positions` there. **Public endpoint — no authentication required** (trader data is public). Rate-limited per client IP. Responses are cacheable at the edge.
      parameters:
      - name: wallet_address
        in: path
        required: true
        schema:
          type: string
        description: Ethereum or Solana wallet address.
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: include_redeemable
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: Count resolved positions still waiting for redemption.
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: Provider override (polymarket, kalshi, opinion, predictfun). Auto-detected if omitted.
        example: predictfun
      responses:
        '200':
          description: Performance stats over the wallet's full book.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderSummaryResponse'
        '400':
          description: Malformed wallet address or unrecognized provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching trader summary.
  /trader-stats/analysis/{wallet_address}:
    get:
      operationId: getTraderAnalysis
      summary: Get a trader's PnL windows, win/loss, ROI distribution and daily PnL calendar
      tags:
      - Trader Analytics
      x-kairos-auth: public
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      security: []
      description: |
        Returns the trader profile's analysis panel: realized PnL and buy count over the 1D, 7D, 30D and ALL windows (whole UTC days ending today; 1D is today only), lifetime winning/losing closed positions and win rate, the distribution of closed positions by ROI (realized PnL over the cost bought for the position), and daily realized PnL for the last 90 UTC days that have activity. Only venues whose trades flow through the lot allocator (Predict.fun) are supported; for any other venue `supported` is false and no figures are returned. **Public endpoint — no authentication required** (trader data is public). Rate-limited per client IP. Cached per wallet for up to a minute.
      parameters:
      - name: wallet_address
        in: path
        required: true
        schema:
          type: string
        description: Ethereum or Solana wallet address.
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: Provider override (polymarket, kalshi, opinion, predictfun). Auto-detected if omitted.
        example: predictfun
      responses:
        '200':
          description: The analysis panel, or `supported` false for a venue without allocator data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderAnalysisResponse'
        '400':
          description: Malformed wallet address or unrecognized provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching trader analysis.
  /trader-stats/positions/{wallet_address}:
    get:
      operationId: getTraderPositions
      summary: Get a trader's open/closed positions and derived performance stats
      tags:
      - Trader Analytics
      x-kairos-auth: public
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      security: []
      description: |
        Returns a trader's positions plus performance metrics computed from a full-inventory scan — served independently of the unified profile so the Positions tab and stats panel can load on their own fetch. Positions are filtered by `status` (open/closed/all), sorted by current value (largest first), and paginated via `limit`/`offset`; `has_more` indicates another page exists in the filtered set. Market names/icons are resolved only for the returned page, so wallets holding a very large number of positions don't blow past internal query limits. `performance` (win rate, total PnL, volume, positions_value, etc.) is always computed over the wallet's FULL inventory, not just the returned page. `include_redeemable` additionally includes resolved positions still awaiting on-chain redemption. **Public endpoint — no authentication required** (trader data is public). Rate-limited per client IP at 30 requests/minute. Responses are cacheable at the edge.
      parameters:
      - name: wallet_address
        in: path
        required: true
        schema:
          type: string
        description: Ethereum or Solana wallet address (validated/normalized as above).
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: include_redeemable
        in: query
        required: false
        schema:
          type: boolean
          default: false
        description: Include resolved positions still waiting for redemption.
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: Provider override (polymarket, kalshi, opinion, predictfun). Auto-detected if omitted.
        example: polymarket
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 200
        description: Max positions to return per page (sorted by value, largest first).
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          default: 0
        description: Number of positions to skip (pagination).
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - all
          - open
          - closed
          default: all
        description: Filter the returned page by position status.
      responses:
        '200':
          description: Positions page plus full-inventory performance stats.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderPositionsResponse'
        '400':
          description: Malformed wallet address or unrecognized provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching trader positions.
  /trader-stats/trades/{wallet_address}:
    get:
      operationId: getTraderTrades
      summary: Get a trader's recent trade/fill history
      tags:
      - Trader Analytics
      x-kairos-auth: public
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      security: []
      description: |
        Returns a paginated page of a wallet's fills, served independently of the full profile as the fast path for the History tab. Reads only trade rows (plus market-title enrichment) — it deliberately skips the heavier positions/inventory and PnL-summary computations used by the full profile build. Each trade carries the realized PnL booked by that fill (populated for SELLs; BUYs report null/0). **Public endpoint — no authentication required** (trader data is public). Rate-limited per client IP at 60 requests/minute. Responses are cacheable at the edge.
      parameters:
      - name: wallet_address
        in: path
        required: true
        schema:
          type: string
        description: Ethereum or Solana wallet address (validated/normalized as above).
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: trade_limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 50
        description: Max trades to fetch per page.
      - name: trade_offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          default: 0
        description: Offset for trade pagination.
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: Provider override (polymarket, kalshi, opinion, predictfun). Auto-detected if omitted.
        example: polymarket
      responses:
        '200':
          description: Page of the wallet's trade history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderRecentTradesResponse'
        '400':
          description: Malformed wallet address or unrecognized provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching trader trades.
  /top-holders:
    get:
      operationId: getTopHolders
      summary: Get top token holders for one or more markets
      tags:
      - Trader Analytics
      x-kairos-auth: api-key
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      description: |
        Fetches the largest holders of each outcome token for the given market IDs from the provider-specific upstream source, including Polymarket's data-api and Predict.fun's public GraphQL API. `market` accepts a comma-separated list (up to 50 IDs, each capped at 128 chars); Predict.fun requires numeric market IDs. An empty or missing value is rejected with 400. Results are grouped per token/outcome and include public profile fields (username, verification badge, avatar) alongside the held `amount`. This is a live upstream read, not served from a local cache; a downstream 4xx/5xx from the provider surfaces as 404 (market/token not found) or 502 (provider error). **Auth**: accepts an admin secret (`X-Admin-Secret`), an API key (`X-Client-Id` + `X-Api-Key` + `X-Api-Secret`), or a first-party JWT (Authorization header or `turnkey_session_jwt` cookie). No API-key scope is required — any valid credential can read holders. Rate-limited by the `heavy` group at 10 requests/minute, the same budget the `/trader-stats` endpoints carry, because every call is a live upstream provider read.
      parameters:
      - name: market
        in: query
        required: true
        schema:
          type: string
        description: Comma-separated list of market/condition IDs (max 50 items, 128 chars each).
        example: 0xabc123,0xdef456
      - name: provider
        in: query
        required: false
        schema:
          type: string
          default: polymarket
        description: Provider name, validated against the active provider registry. Examples include polymarket and predictfun.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 20
          default: 20
        description: Maximum number of holders to return per token.
      - name: minBalance
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          maximum: 999999
          default: 1
        description: Minimum token balance a holder must have to be included.
      responses:
        '200':
          description: Top holders per requested token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderTopHoldersResponse'
        '400':
          description: |
            Missing/empty `market`, too many market IDs, an oversized market ID, an invalid `provider`, or an invalid request accepted by the provider client.
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '404':
          description: Market or token not found by the upstream provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Upstream provider error fetching top holders.
  /search-traders:
    get:
      operationId: searchTraders
      summary: Search for a trader's public profile by wallet address
      tags:
      - Trader Analytics
      x-kairos-auth: api-key
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      description: |
        Fetches public profile information (display name, pseudonym, bio, avatar, X/Twitter handle, verification badge, associated user IDs) for a wallet address from the upstream provider. Provider is auto-detected from address format when omitted — Ethereum (0x...) addresses default to Polymarket and Solana addresses default to Kalshi; auto-detection maps **every** 0x address to Polymarket, so other EVM venues (Opinion, predictfun, ...) must pass `provider` explicitly. `provider`, when given, is validated against the central provider registry shared with `/trader-stats`; any registered provider is accepted, and non-Polymarket venues currently receive a synthetic (service-generated) profile rather than a live upstream fetch. This is a live provider read, not served from cached data. **Auth**: accepts an admin secret (`X-Admin-Secret`), an API key (`X-Client-Id` + `X-Api-Key` + `X-Api-Secret`), or a first-party JWT (Authorization header or `turnkey_session_jwt` cookie). No API-key scope is required — any valid credential can read a public profile. Rate-limited by the `heavy` group at 10 requests/minute, matching `/trader-stats`, because every call is a live upstream provider read.
      parameters:
      - name: address
        in: query
        required: true
        schema:
          type: string
        description: Wallet address to search for (any non-empty string; trimmed).
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: provider
        in: query
        required: false
        schema:
          type: string
        description: |
          Provider override. Auto-detected from address format (0x -> polymarket, Solana -> kalshi) if omitted.
        example: polymarket
      responses:
        '200':
          description: Trader search result. `profile` is null if no profile was found; `error` carries
            a provider-supplied message in that case.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TraderSearchResponse'
        '400':
          description: Missing/blank `address` or an invalid `provider`.
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Upstream provider error searching for the trader.
  /pnl/providers:
    get:
      operationId: listPnlProviders
      summary: List available PnL providers
      tags:
      - PnL
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Returns the set of providers the merged-PnL pipeline knows about, each with a human-readable description. For API-key callers, the set is additionally filtered to providers whose API access is currently enabled — JWT-user and admin callers are exempt from this check. No endpoint-specific rate limit; the global default of 100 requests/minute applies.
      parameters: []
      responses:
        '200':
          description: List of available PnL providers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PnlProviderInfo'
              example:
              - name: polymarket
                description: Polymarket prediction market PnL
              - name: hyperliquid
                description: hyperliquid PnL provider
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API key is missing the `position:read` scope, or the platform's API access is disabled
            via feature flag.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /pnl/{user_id}:
    get:
      operationId: getUserPnl
      summary: Get merged PnL data for a user across one or more wallets/providers
      tags:
      - PnL
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Fetches and merges PnL records/summary across the wallets supplied for this user via the general-purpose PnL path — not the real-time pipeline used by `/pnl/hover` and `/pnl/wallet-totals`. At least one of `polymarket_wallet`, `kalshi_wallet`, or `hyperliquid_wallet` is required — the request is rejected with 400 if none are supplied. `polymarket_wallet` and `hyperliquid_wallet` must be valid Ethereum addresses (normalized/lower-cased); `kalshi_wallet` is treated as an opaque user-id string (Kalshi is a CEX with internal, non-address identifiers). `provider` (repeatable query param, up to 16 items) filters the merged result down to specific provider(s); when omitted, all providers implied by the supplied wallets are used. Kalshi and Hyperliquid PnL are served only through this aggregate endpoint; neither is available from `/pnl/hover` or `/pnl/wallet-totals`. Hyperliquid open-position and unrealized PnL use HIP-4 balances and mark prices, while realized PnL and fee fields are currently zero. The caller-supplied `{user_id}` MUST match the authenticated principal's own id (JWT `sub`, or `user_id` for API-key auth) — mismatches are rejected with 403; this endpoint can only ever return the caller's own PnL. `limit`/`offset` paginate the merged record list; internally up to `min(limit + offset, 1000)` records per provider are fetched, so `total`/`has_more` in the response describe the page relative to that ceiling, not an authoritative full-history count. For API-key callers, each requested provider must additionally have its API access enabled; JWT/admin callers are exempt.
      parameters:
      - name: user_id
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
        description: Kairos internal user id. Must match the authenticated caller's own id.
        example: usr_9f3c2a1b
      - name: polymarket_wallet
        in: query
        required: false
        schema:
          type: string
          maxLength: 128
        description: Polymarket (Ethereum) wallet address.
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: kalshi_wallet
        in: query
        required: false
        schema:
          type: string
          maxLength: 128
        description: Kalshi user ID (opaque internal identifier, not a wallet address).
      - name: hyperliquid_wallet
        in: query
        required: false
        schema:
          type: string
          maxLength: 128
        description: Hyperliquid EVM address (HIP-4 positions key off this same deposit/trade address).
        example: '0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c'
      - name: provider
        in: query
        required: false
        style: form
        explode: true
        schema:
          type: array
          maxItems: 16
          items:
            type: string
        description: Filter merged PnL to these provider(s). Repeatable query param.
        example:
        - polymarket
        - hyperliquid
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 500
        description: Records per page.
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
        description: Records to skip.
      responses:
        '200':
          description: Merged PnL summary and records for the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PnlUserPnlResponse'
        '400':
          description: |
            Missing/oversized `user_id`, an invalid wallet address, an invalid `provider` filter, or no wallet address supplied at all.
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: The `user_id` does not match the authenticated caller, the API key is missing the
            `position:read` scope, or platform API access is disabled.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /pnl/hover/{provider_id}/{wallet}/{contract_id}:
    get:
      operationId: getWalletMarketPnl
      summary: Live per-market PnL for a (wallet, market) pair
      tags:
      - PnL
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Live "hover" PnL for a single wallet on a single market, sourced from a fast real-time pipeline — quick regardless of how far back the wallet's earliest trade goes. Returns one row per token held on the contract (e.g. YES + NO for a binary market) plus aggregate totals across those tokens. Only providers on this pipeline are supported: `polymarket`, `opinion`, `predictfun`; `kalshi` and `hyperliquid` are rejected with 400 and must use `/pnl/{user_id}`. When the underlying pipeline is disabled (e.g. while a backfill is ramping), the endpoint returns a well-formed zero/empty payload (`tokens: []`, all totals `0.0`) instead of erroring, matching the "no data" state the frontend already renders gracefully. For API-key callers, the requested provider must additionally have its API access enabled.
      parameters:
      - name: provider_id
        in: path
        required: true
        schema:
          type: string
          maxLength: 32
          enum:
          - polymarket
          - opinion
          - predictfun
        description: MV-pipeline provider id. `kalshi`, `hyperliquid`, and any other registered-but-non-MV provider are
          rejected with 400.
        example: polymarket
      - name: wallet
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
        description: Wallet address (opaque string; trimmed, length-checked — not format-validated at
          this layer).
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      - name: contract_id
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
        description: Market/condition (contract) id.
        example: 0xabc123condition
      responses:
        '200':
          description: |
            Per-token PnL rows and aggregate totals for the wallet on this market. Returns an empty/zeroed payload (not a 404) when the underlying PnL pipeline is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PnlWalletMarketPnlResponse'
        '400':
          description: Missing/oversized `wallet`/`contract_id`, or `provider_id` is not a valid MV-pipeline
            provider (e.g. kalshi or hyperliquid).
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API key missing `position:read` scope, or platform API access disabled for this
            provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching hover PnL.
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /pnl/wallet-totals/{provider_id}/{wallet}:
    get:
      operationId: getWalletTotals
      summary: Wallet-wide PnL totals across all markets
      tags:
      - PnL
      x-kairos-auth: api-key
      x-kairos-scope: position:read
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      description: |
        Wallet-wide PnL totals aggregated across every market the wallet has touched, refreshed hourly — a fast point lookup rather than a live scan. For real-time per-market freshness on a single active market, use `/pnl/hover/{provider_id}/{wallet}/{contract_id}` instead. Same provider restriction as `/pnl/hover`: only `polymarket`, `opinion`, `predictfun` are accepted; `kalshi` and `hyperliquid` are rejected with 400 and must use `/pnl/{user_id}`. When the underlying pipeline is disabled, returns a well-formed zero payload (`market_count: 0`, `token_count: 0`, all dollar totals `0.0`, `snapshot_ts` set to the current time) instead of erroring. For API-key callers, the requested provider must additionally have its API access enabled.
      parameters:
      - name: provider_id
        in: path
        required: true
        schema:
          type: string
          maxLength: 32
          enum:
          - polymarket
          - opinion
          - predictfun
        description: MV-pipeline provider id. `kalshi` and `hyperliquid` are rejected with 400.
        example: polymarket
      - name: wallet
        in: path
        required: true
        schema:
          type: string
          maxLength: 128
        description: Wallet address (opaque string; trimmed, length-checked — not format-validated at
          this layer).
        example: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b'
      responses:
        '200':
          description: |
            Wallet-wide PnL totals as of the last hourly snapshot. Returns a zeroed payload (not a 404) when the underlying PnL pipeline is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PnlWalletTotalsResponse'
        '400':
          description: Missing/oversized `wallet`, or `provider_id` is not a valid MV-pipeline provider
            (e.g. kalshi or hyperliquid).
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          description: API key missing `position:read` scope, or platform API access disabled for this
            provider.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Unexpected error fetching wallet totals.
        '503':
          description: Per-venue API-access flag could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/markets:
    get:
      operationId: searchMarkets
      summary: Search markets by text query
      description: Unified market search. Results are post-filtered to drop rows whose provider
        is currently inactive.
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: search
      parameters:
      - name: q
        in: query
        required: true
        description: Search query text.
        schema:
          type: string
          minLength: 1
          maxLength: 200
        example: trump 2028
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
      - name: provider
        in: query
        description: Filter by provider id (e.g. kalshi, polymarket, predictfun, opinion, dome). Validated
          dynamically against active providers; 400 if unrecognized. `provider_id` is accepted as a legacy
          alias — if both are given they must match or the request 400s.
        schema:
          type: string
        example: polymarket
      - name: provider_id
        in: query
        description: Legacy alias for `provider`. See `provider`.
        schema:
          type: string
      - name: include_expired
        in: query
        schema:
          type: boolean
          default: false
      - name: statuses
        in: query
        description: Repeatable market-status filter (e.g. open, closed, settled). Passed through unvalidated.
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: categories
        in: query
        description: Repeatable category filter. Passed through unvalidated.
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: tags
        in: query
        description: Repeatable platform-tag-slug filter (query param name is `tags`; internally `platform_tags`).
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      responses:
        '200':
          description: Search results, visibility-filtered by active provider.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchMarketsResponse'
        '400':
          description: Empty/whitespace-only `q`, or invalid `provider`/`provider_id` (unknown id, or
            mismatch between the two).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchBadRequestError'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Search backend query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/markets-and-events:
    get:
      operationId: searchMarketsAndEvents
      summary: Search both markets and events with combined relevance scoring
      description: 'Runs a market search and/or event search depending on `type`, tags event rows
        with `"type": "event"`, merges both result sets, sorts by `relevance_score` descending, and
        truncates to `limit`. Same provider-visibility post-filter as `/search/markets`.'
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: search
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 200
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
      - name: provider
        in: query
        schema:
          type: string
      - name: provider_id
        in: query
        description: Legacy alias for `provider`.
        schema:
          type: string
      - name: type
        in: query
        description: Which result set(s) to include.
        schema:
          type: string
          enum:
          - market
          - event
          - both
          default: both
      - name: include_expired
        in: query
        schema:
          type: boolean
          default: false
      - name: statuses
        in: query
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: categories
        in: query
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: tags
        in: query
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      responses:
        '200':
          description: Combined market + event results, sorted by relevance descending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchMarketsAndEventsResponse'
        '400':
          description: Empty `q`, invalid provider, or `type` not one of market/event/both.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchBadRequestError'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Search backend query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/simple:
    get:
      operationId: searchSimple
      summary: Fast simple search for navbar/autocomplete overlay
      description: Primary search endpoint behind the web navbar overlay. Supports sort/offset/pagination
        and optional server-side event grouping (`group_results`), then applies provider-visibility
        filtering. When `group_results=true`, best-effort enrichment (never fails the request) adds
        `classifiedGroups`/`absorbedMarketIds` (interactive ladder groups) and `correlations` (a cross-venue
        similar-market rail, similarity above a threshold, capped at 4 counterparts per result market).
        When `group_results=false`, only `correlations` is attached.
      tags:
      - Search
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: search
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
          minLength: 1
          maxLength: 200
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 20
      - name: offset
        in: query
        schema:
          type: integer
          minimum: 0
          default: 0
      - name: provider
        in: query
        schema:
          type: string
      - name: provider_id
        in: query
        description: Legacy alias for `provider`.
        schema:
          type: string
      - name: tags
        in: query
        description: Repeatable platform-tag-slug filter.
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: statuses
        in: query
        schema:
          type: array
          items:
            type: string
        style: form
        explode: true
      - name: sort_by
        in: query
        schema:
          type: string
          enum:
          - relevance
          - volume_24h
          - newest
          - ending_soon
          default: relevance
      - name: sort_order
        in: query
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      - name: include_total
        in: query
        description: Include a best-effort `meta.total` (falls back to the filtered count if any rows
          were dropped by visibility filtering).
        schema:
          type: boolean
          default: false
      - name: include_expired
        in: query
        schema:
          type: boolean
          default: false
      - name: group_results
        in: query
        description: Group markets by event server-side, producing `groups`/`singles` in the response.
        schema:
          type: boolean
          default: true
      responses:
        '200':
          description: Simple-search result envelope, optionally grouped and enriched with classified
            ladders + cross-venue correlations.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchSimpleResponse'
        '400':
          description: Empty `q`, invalid provider, or invalid `sort_by`/`sort_order`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchBadRequestError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Search backend query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/resolve-url:
    get:
      operationId: searchResolveUrl
      summary: Resolve a pasted Polymarket/Kalshi/predict.fun market or event URL
      description: Parses `url` against known host patterns — polymarket.com `/event/<slug>/<market-slug>`
        (market) or `/event/<slug>` (event) or legacy `/market/<slug>`; kalshi.com ticker/event_id/market_id
        path segments; predict.fun equivalents. Unrecognized hosts or unmatched paths return an empty
        `/search/simple`-shaped body (200) so the frontend can fall back to a normal text search rather
        than erroring. On a match, resolves the market/event (for an event URL, every outcome market in
        that event) and returns the same `{results, groups?, singles?, meta}` shape as `/search/simple`,
        enriched with `correlations` only (ladder classification is skipped — a resolved URL is already
        a single market/event, not a broad set to collapse).
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: search
      parameters:
      - name: url
        in: query
        required: true
        description: Raw pasted URL (scheme optional — `https://` is prepended if missing) or arbitrary
          text.
        schema:
          type: string
          minLength: 1
          maxLength: 1000
        example: https://polymarket.com/event/will-trump-win-2028/yes
      responses:
        '200':
          description: Resolved market/event in `/search/simple` shape, or an empty body if the URL wasn't
            recognized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchSimpleResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: URL resolution failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/suggest:
    get:
      operationId: searchSuggest
      summary: Autocomplete suggestions for search-as-you-type
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: search
      description: Returns autocomplete suggestions. Not provider-visibility filtered.
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
          minLength: 2
          maxLength: 100
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 10
      - name: provider
        in: query
        description: Filter suggestions by provider id.
        schema:
          type: string
      responses:
        '200':
          description: Autocomplete suggestions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchSuggestResponse'
        '400':
          description: Empty `q` or invalid provider.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchBadRequestError'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Suggestion query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/screener:
    get:
      operationId: searchScreener
      summary: Find markets by what their book and tape are doing
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 30/minute
      x-kairos-bucket: screener
      description: '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.'
      parameters:
      - name: min_spread
        in: query
        description: Cents. Only two-sided books can satisfy a spread bound.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: max_spread
        in: query
        description: Cents.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: min_price
        in: query
        description: Cents, on the matched outcome's mid.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: max_price
        in: query
        description: Cents, on the matched outcome's mid.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: min_volume_24h
        in: query
        description: Dollars traded in the rolling 24h window.
        schema:
          type: number
          minimum: 0
      - name: min_volume_1h
        in: query
        description: Dollars traded in the rolling 1h window.
        schema:
          type: number
          minimum: 0
      - name: min_liquidity
        in: query
        description: Dollars resting across every outcome, both sides.
        schema:
          type: number
          minimum: 0
      - name: max_liquidity
        in: query
        description: Dollars resting across every outcome, both sides. Pair with a volume floor to find
          thin books carrying heavy flow.
        schema:
          type: number
          minimum: 0
      - name: min_outcome_liquidity
        in: query
        description: Dollars resting on the matched outcome alone, both sides.
        schema:
          type: number
          minimum: 0
      - name: min_open_interest
        in: query
        description: Contracts outstanding.
        schema:
          type: number
          minimum: 0
      - name: two_sided
        in: query
        description: Only markets quoting a bid and an ask. A settled market has no quotes at all, which
          would otherwise read as infinitely thin.
        schema:
          type: boolean
          default: false
      - name: max_book_age_minutes
        in: query
        description: 'Drop markets whose top of book has not been published within this many minutes.
          Settled and delisted markets stop being streamed, so their frozen book would otherwise screen
          as a live quote — on a live catalogue that is more than half the records. `0` removes the bound
          and returns them.


          The timestamp is the streamer''s publish clock, so this detects "nothing is publishing this
          market any more", not "the venue''s book went quiet": a wedged venue connection re-publishing
          a cached book still stamps now.'
        schema:
          type: number
          minimum: 0
          default: 10
      - name: provider
        in: query
        description: Repeatable. Restricts the screen to these venues.
        schema:
          type: array
          items:
            type: string
      - name: sort
        in: query
        schema:
          type: string
          default: volume_24h
          enum:
          - volume_24h
          - volume_1h
          - spread_desc
          - spread_asc
          - liquidity
          - open_interest
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 500
          default: 50
      - name: offset
        in: query
        schema:
          type: integer
          minimum: 0
          maximum: 10000
          default: 0
      responses:
        '200':
          description: A page of matching markets, most relevant to the chosen sort first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenerResponse'
        '400':
          description: No metric bound given, an inverted range, an unknown provider or sort, or a query
            the vitals service refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchBadRequestError'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Screener query failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /search/screener/presets:
    get:
      operationId: searchScreenerPresets
      summary: One-click screens the screener offers
      tags:
      - Search
      x-kairos-auth: api-key
      x-kairos-rate-limit: 30/minute
      x-kairos-bucket: screener
      description: Named parameter sets for common screens, so a client does not hardcode thresholds.
        Each preset's `params` are query parameters for `/search/screener`. Also lists the accepted `sort`
        values.
      responses:
        '200':
          description: Available presets and sorts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenerPresetsResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /api/markets/discover/v2:
    get:
      operationId: discoverMarketsV2
      summary: Paginated, filtered, event-grouped market list for the Discover page
      description: 'Returns a paginated, filtered, sorted, event-grouped market list for the Discover
        page, with server-side filtering (provider, category incl. alias remapping, free-text search,
        price/volume bounds, explicit market-id list, platform-tag/subcategory intersection, expiration
        window) and sorting. Most filter combinations are cacheable; free-text or bounded (price/volume)
        requests always run fresh and are not cached. Tag/subcategory filters that resolve to zero markets
        short-circuit to an empty payload; tag-filtered requests that would otherwise come back empty
        also get a best-effort fallback lookup so populated subtopics never render empty.'
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: offset
        in: query
        description: Bounded by the candidate window the feed is assembled from; `total` is derived
          from that same window, so real pagination never reaches the cap.
        schema:
          type: integer
          minimum: 0
          maximum: 5000
          default: 0
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
      - name: sort_by
        in: query
        schema:
          type: string
          enum:
          - volume
          - volume_24h
          - volume_1h
          - price
          - newest
          - liquidity
          - rewards
          default: volume_1h
      - name: sort_order
        in: query
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      - name: provider
        in: query
        description: Provider id or `all`. Validated dynamically against currently active providers.
        schema:
          type: string
        example: kalshi
      - name: category
        in: query
        description: Topic category. Display aliases `elections`, `geopolitics`, `economy`, `climate &
          science` are remapped server-side to `politics`, `world`, `finance`, `weather` respectively
          before filtering.
        schema:
          type: string
          enum:
          - all
          - politics
          - crypto
          - sports
          - esports
          - finance
          - tech
          - world
          - weather
          - elections
          - geopolitics
          - culture
          - economy
          - climate & science
      - name: search
        in: query
        description: Free-text filter. Disables response caching for this request.
        schema:
          type: string
          maxLength: 128
      - name: min_price
        in: query
        description: Inclusive lower bound, in cents (0-100). Disables response caching.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: max_price
        in: query
        description: Inclusive upper bound, in cents (0-100). Disables response caching.
        schema:
          type: number
          minimum: 0
          maximum: 100
      - name: min_volume_1h
        in: query
        description: Minimum hourly volume, matched against volume1hRank so a market whose
          hour nobody reports is judged on its day rather than dropped. Disables response caching.
        schema:
          type: number
          minimum: 0
      - name: market_ids
        in: query
        description: Comma-separated market ids (max 500 items, 128 chars each). Disables response caching.
        schema:
          type: string
        example: KXPRES-28,0x1234abcd
      - name: tag_ids
        in: query
        description: Comma-separated platform-tag UUIDs, AND-intersected against `market_ids`/`tag_slug`
          (max 50). Disables response caching.
        schema:
          type: string
      - name: tag_slug
        in: query
        description: Single platform-tag slug (alternative to `tag_ids`).
        schema:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{0,63}$
      - name: subcategory
        in: query
        description: Subcategory (subtopic) tag slug, intersected with any other filters. Also accepts
          the tournament sentinel `__tournament:<uuid>` emitted by the web bracket UI, which short-circuits
          to an empty markets payload rather than 400ing.
        schema:
          type: string
      - name: expiration_after
        in: query
        description: ISO-8601 datetime lower bound (inclusive) on market expiration.
        schema:
          type: string
          format: date-time
      - name: expiration_before
        in: query
        description: ISO-8601 datetime upper bound (exclusive) on market expiration.
        schema:
          type: string
          format: date-time
      - name: zipper
        in: query
        description: Interleave/zip results across providers instead of a flat sort.
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: Paginated market list (event-grouped where applicable).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverV2Response'
        '400':
          description: Invalid sort_by/sort_order/category/provider, invalid tag_slug/subcategory slug,
            invalid expiration ISO date, or oversized market_ids/tag_ids list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverBadRequestError'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Discover assembly failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
        '503':
          description: Discover data store is not ready.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/discover/v2/search-index:
    get:
      operationId: discoverSearchIndex
      summary: Compact full market list for client-side instant search
      description: Returns every valid market (grouped by event) in a compact shape for the client
        to index locally. Responses are cached to keep this fast after the first build.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters: []
      responses:
        '200':
          description: Full compact market index.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverSearchIndexResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Search-index rebuild failed on a cache miss.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/discover/v2/ticker:
    get:
      operationId: discoverTicker
      summary: Top markets for the discover page ticker bar
      description: Returns the top markets for the discover page ticker bar. Responses are cached
        briefly.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 60
          default: 30
      responses:
        '200':
          description: Ticker-bar market list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverTickerResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Ticker rebuild failed on a cache miss.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/discover/v2/breaking:
    get:
      operationId: discoverBreaking
      summary: Market pulse — latest trending movers for the Pulse rail
      description: Interleaves biggest 24h price movers, top 1h-volume, and top 24h-volume markets
        (deduped). Responses are cached briefly.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 25
      responses:
        '200':
          description: Breaking/trending market list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverBreakingResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Pulse rebuild failed on a cache miss.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/discover/v2/expiring:
    get:
      operationId: discoverExpiring
      summary: Markets expiring soonest, with actionable prices
      description: Returns markets expiring soonest, with actionable prices. Responses are cached
        briefly.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 20
          default: 7
      responses:
        '200':
          description: Soonest-expiring market list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverExpiringResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Expiring-markets rebuild failed on a cache miss.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/discover/v2/subcategories:
    get:
      operationId: discoverSubcategories
      summary: Available subcategories (subtopics) for a topic category, with live market counts
      description: Resolves the topic tag's child subtopic tags together with live market counts,
        drops zero-count subtopics, and sorts by count descending. Responses are cached, including
        empty results.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: category
        in: query
        required: true
        description: Topic tag slug (e.g. crypto, sports, politics).
        schema:
          type: string
      responses:
        '200':
          description: Subcategories with live market counts, sorted descending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverSubcategoriesResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: Subcategory index is missing or unreadable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/sports/kalshi-live:
    get:
      operationId: discoverKalshiLiveSports
      summary: Kalshi sports events matched against live Polymarket games
      description: Returns cached Kalshi sports events and, if `games` is supplied, matches each `AWAY:HOME[:league]`
        triple's team codes (uppercased) against each event's ticker suffix (segment after the first
        `-`), grouping and sorting matches by total volume descending.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: games
        in: query
        description: Comma-separated list of `AWAY:HOME[:league]` team-code triples, e.g. `ATL:CLE:nba,DET:MIN:mlb`.
          When omitted, returns all cached events with an empty `matched` list.
        schema:
          type: string
        example: ATL:CLE:nba,DET:MIN:mlb
      responses:
        '200':
          description: Cached Kalshi sports events, optionally matched to requested games.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverKalshiLiveResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /api/markets/trending:
    get:
      operationId: discoverTrendingMarkets
      summary: Trending markets by 1h volume
      description: Returns trending markets ranked by 1-hour volume.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 50
          default: 10
      - name: provider
        in: query
        description: Provider id filter, or omit/`all` for every provider.
        schema:
          type: string
      responses:
        '200':
          description: Trending markets ranked by 1h volume.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverTrendingResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Trending lookup failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /api/markets/trending/ws:
    get:
      operationId: discoverTrendingMarketIds
      summary: Trending market ids for a live-price subscription
      description: Same ranking as `/api/markets/trending`, reduced to the fields a WebSocket
        subscriber needs to open a live-price stream (id, symbol, price, 24h volume). Not cached —
        every call reads the trending index directly.
      tags:
      - Discover
      x-kairos-auth: api-key
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: discover
      parameters:
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 300
          default: 20
      - name: provider
        in: query
        description: Provider id filter, or omit for every provider.
        schema:
          type: string
      responses:
        '200':
          description: Trending market ids ranked by 1h volume.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverTrendingWsResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: Trending lookup failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /providers/configs:
    get:
      operationId: getProviderConfigs
      summary: List all active provider configurations
      description: Returns the minimal exchange-agnostic config needed by the UI for every currently
        active provider. Public endpoint — no auth headers are checked. No endpoint-specific rate limit;
        only the global default (100/minute) applies.
      tags:
      - Providers
      x-kairos-auth: public
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      security: []
      parameters: []
      responses:
        '200':
          description: All active provider configs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvidersConfigsResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /providers/api-access:
    get:
      operationId: getProviderAPIAccess
      summary: List providers available to API-key callers
      description: Returns active providers whose API-access toggle is enabled. Public endpoint — no
        auth required. Operator reasons and other admin-only flag metadata are never returned. No
        endpoint-specific rate limit; only the global default (100/minute) applies.
      tags:
      - Providers
      x-kairos-auth: public
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      security: []
      parameters: []
      responses:
        '200':
          description: Providers currently available through API-key authentication.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvidersAPIAccessResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: API-access flags could not be read or validated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /providers/configs/{provider_id}:
    get:
      operationId: getProviderConfig
      summary: Get a single provider configuration by id
      description: Looks up one provider by id (case-insensitive, trimmed). Public endpoint — no
        auth required. No endpoint-specific rate limit; only the global default (100/minute) applies.
      tags:
      - Providers
      x-kairos-auth: public
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      security: []
      parameters:
      - name: provider_id
        in: path
        required: true
        schema:
          type: string
        example: hyperliquid
      responses:
        '200':
          description: The provider's configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvidersConfig'
              example:
                id: hyperliquid
                display_name: Hyperliquid
                icon_url: https://imagedelivery.net/Hias1rvxalFDFzbgXzaBJg/cfe0d4ac-9597-415b-c186-a375bda5be00/public
                chain_id: hyperliquid
                chain_name: Hyperliquid
                is_active: true
                supported_order_types: [limit, market]
                supports_walk_the_book: false
                supports_token_approval: false
                has_multi_token_markets: true
                auth_flow_type: wallet_signature
                token_id_format: opaque
                metadata_key_type: marketId
                numeric_id: 9
                primary_color: '#97FCE4'
        '404':
          description: No provider with this id is loaded/active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvidersNotFoundError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/trending-matched:
    get:
      operationId: getSportsTrendingMatched
      summary: Top live/upcoming sports markets matched across Polymarket and Kalshi
      description: Finds games that have BOTH a Polymarket and a Kalshi entry in the cross-venue
        sports match cache, drops any entry whose embedded date is older than yesterday (UTC), enriches
        each surviving Kalshi side with a live volume figure (`volume_1h` falling back to `volume_total`),
        sorts descending by that volume, and truncates to `limit`. Title and `tokenId` come from the
        cached match record itself, not from a live provider call. Responses are cacheable at the edge.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: limit
        in: query
        required: false
        description: Maximum number of matched markets to return, sorted by Kalshi volume descending.
        schema:
          type: integer
          minimum: 1
          maximum: 20
          default: 5
        example: 5
      responses:
        '200':
          description: 'Trending cross-provider matched markets. Returns `{"matches": [], "count": 0}`
            immediately if the matching cache is empty.'
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=30, s-maxage=60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsTrendingMatchedResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/matching-markets:
    get:
      operationId: getSportsMatchingMarkets
      summary: Cross-platform matching-market records for a set of market slugs
      description: Returns the cross-venue match record for each requested market slug, keyed by
        slug. Provider entries can include Polymarket, Kalshi, Predict.fun, and Hyperliquid. Slugs
        with no cross-venue match are silently omitted from the response object (not returned as
        null). When `live=true`, the Kalshi side of every
        matched entry is refreshed in-place with a live price (in cents / 100, 4 decimal places); a
        correction is applied when a stale price appears to be on the wrong side of a binary flip.
        Responses are cacheable, briefly when `live=true`, longer otherwise.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: slugs
        in: query
        required: true
        description: Comma-separated list of market or matching slugs to look up. Include every slug
          carried by the discovery card because event slugs and per-outcome market slugs are distinct.
          Empty/blank entries are dropped.
        schema:
          type: string
          minLength: 1
        example: nba-lal-bos-2026-01-15,nhl-tor-mtl-2026-01-15
      - name: live
        in: query
        required: false
        description: Pass the literal string "true" to refresh Kalshi prices before returning. Any
          other value, including omission, is treated as false.
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        example: 'true'
      responses:
        '200':
          description: Map of slug to matching-market record. Returns `{}` if `slugs` parses to no non-empty
            entries.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=30, s-maxage=60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsMatchingMarketsResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/kalshi-filters:
    get:
      operationId: getSportsKalshiFilters
      summary: Kalshi Sport -> Competition -> Scope taxonomy
      description: 'Returns the upstream Kalshi taxonomy payload verbatim (no normalization). Used
        to drive Kalshi sport/competition/scope filter UI.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: Raw Kalshi filters-by-sport taxonomy.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=21600
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsKalshiFiltersResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/live-events:
    get:
      operationId: getSportsLiveEvents
      summary: Active sports games with urgency scores and cross-provider prices
      description: 'Returns active game IDs plus, when the provider indexing gate has `predictfun`
        enabled, open Predict.fun fixtures inside their scheduled start/end window. The gate is
        re-checked on every request (including cache hits); a gate lookup failure excludes Predict.fun
        (fail closed) without dropping Polymarket/Kalshi rows. Hydrates current state, drops ended
        games idle for more than a few minutes, and computes an `urgencyScore`/`stale` flag per game.
        Enriches each event with cached primary-market data, falling back to a live provider lookup
        when the cache is cold. Resolves each event''s matching Polymarket/Kalshi slug and attaches the
        corresponding cross-venue match record into `matchingMarkets`. If no matched entry anywhere
        carries a Kalshi side, synthesizes one from a team-name-matching fallback lookup. Results are
        sorted by most-recent update, then by `urgencyScore` descending. Served from a shared server
        cache rebuilt about every 5 seconds while the endpoint is in use and never more than 60 seconds
        old; the `Age` header gives the seconds since the body was built.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: include_markets
        in: query
        required: false
        description: When true, includes the full markets list (not just the primary market) for every
          event.
        schema:
          type: boolean
          default: false
        example: false
      responses:
        '200':
          description: Active games ordered by recency then urgency, plus their matched cross-provider
            markets.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=10, s-maxage=10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsLiveEventsResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/event-markets:
    get:
      operationId: getSportsEventMarkets
      summary: All markets for a single sports event
      description: Resolves markets for one game. If `game_id` is given, tries a cached lookup first,
        falling back to a live Polymarket lookup on a miss. If only `slug` is given, first resolves
        the event's `gameId` via a live Polymarket lookup, then performs the same event-markets lookup.
        Returns an empty markets list if neither parameter resolves to an event. At least one of `game_id`
        or `slug` should be supplied; both are optional and unvalidated strings.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: game_id
        in: query
        required: false
        description: Upstream sports game ID to look up directly.
        schema:
          type: string
        example: '12345678'
      - name: slug
        in: query
        required: false
        description: Polymarket market slug used to discover the event's gameId when game_id is not known.
        schema:
          type: string
        example: nba-lal-bos-2026-01-15-lal
      responses:
        '200':
          description: Markets for the resolved event. `gameId` echoes the resolved id, or falls back
            to the raw `game_id`/`slug` query value ("unknown" if neither was supplied) when nothing could
            be resolved.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=10, s-maxage=10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsEventMarketsResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/kalshi-live-games:
    get:
      operationId: getSportsKalshiLiveGames
      summary: Currently-live Kalshi sports games (NHL/NBA/MLB/NFL+UFL/soccer/esports)
      description: 'Returns every Kalshi milestone currently live for the supported milestone types
        (hockey_tournament, basketball_game, baseball_game, football_game, soccer_tournament_multi_leg,
        esports_match), built into moneyline/spread/total market blocks from Kalshi''s trading API.
        "Live" means started recently or starting soon, per Kalshi''s live-data status. Responses
        are cacheable.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: Currently live Kalshi sports games with team, score/clock, and market data.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=240
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsKalshiLiveGamesResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/poly-kalshi-pairings:
    get:
      operationId: getSportsPolyKalshiPairings
      summary: Live game pairings between Polymarket and Kalshi
      description: 'Cross-references currently-live Polymarket games against currently-live Kalshi
        games, grouped by a Kalshi-league -> Polymarket-sport crosswalk and matched via a multi-step
        heuristic (ticker-code match, team-name/alias substring match, derived-code prefix match, letter-subset
        fallback). **Price scale note:** Kalshi market prices in the `kalshiMarkets` array are rescaled
        to a 0-100 range (not the 0-1 scale used elsewhere in this API) to match a legacy response
        shape. Falls back to a recent last-known-good snapshot if the live Kalshi data is unavailable.
        Responses are cacheable.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: Matched Polymarket <-> Kalshi live game pairs.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsPolyKalshiPairingsResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/catalog:
    get:
      operationId: getSportsCatalog
      summary: Enriched catalog of sport categories and leagues
      description: Returns the curated sports taxonomy, including UEFA Europa League (`uel`) under
        soccer, with the provider series and tag identifiers clients need to request league events.
        Responses are cacheable.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: Sport categories, each with its leagues and resolved Polymarket seriesId.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=3600, s-maxage=3600
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsCatalogResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/upcoming-events:
    get:
      operationId: getSportsUpcomingEvents
      summary: Upcoming (not-yet-live) sports events within a time window
      description: Returns fixtures in the requested league set whose scheduled start falls within
        `windowHours`, together with primary markets, combo eligibility where available, and
        cross-venue matches for Polymarket, Kalshi, Predict.fun, and Hyperliquid. Provider-only
        fixtures may be returned as standalone events. Served from a shared server cache rebuilt about
        every minute; a copy is never more than two hours old for the default 720h/200 and 168h/12
        windows, which are kept warm, or ten minutes old for any other window; the `Age` header gives the seconds since the
        body was built. `startFrom`/`startTo` return one slice of the window by start time, for
        loading a schedule a few days at a time. Returns 502 when no provider can supply data and
        504 when a cold build runs out of time.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: windowHours
        in: query
        required: false
        description: Look-ahead window, in hours, for upcoming game start times.
        schema:
          type: integer
          minimum: 1
          maximum: 720
          default: 24
        example: 24
      - name: category
        in: query
        required: false
        description: Sports-catalog category id (e.g. "basketball"). Expands to every league slug in that
          category and takes priority over league/series.
        schema:
          type: string
        example: basketball
      - name: league
        in: query
        required: false
        description: Single league slug filter (e.g. "nba"). Used only if category is not supplied or
          does not resolve.
        schema:
          type: string
        example: nba
      - name: series
        in: query
        required: false
        description: Comma-separated league slugs. Used only if neither category nor league resolves.
        schema:
          type: string
        example: nba,nhl
      - name: limitPerSeries
        in: query
        required: false
        description: Maximum number of events fetched per resolved series.
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 12
        example: 12
      - name: startFrom
        in: query
        required: false
        description: Only events starting at or after this instant (ISO 8601 with a UTC offset).
          `startingEvents` are included only in the slice that covers the current time, and
          `matchingMarkets` only holds entries for the events returned.
        schema:
          type: string
          format: date-time
        example: '2026-09-25T04:00:00Z'
      - name: startTo
        in: query
        required: false
        description: Only events starting before this instant (ISO 8601 with a UTC offset). Must be
          after `startFrom`.
        schema:
          type: string
          format: date-time
        example: '2026-09-28T04:00:00Z'
      responses:
        '200':
          description: 'Upcoming events within the window, plus matched cross-provider markets. Returns
            `{"events": [], "startingEvents": [], "matchingMarkets": {}}` on a genuine no-data result.'
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=30, s-maxage=60
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsUpcomingEventsResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: All upstream event-source fetches failed while building a cold cache entry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: All upstream fetches failed
  /sports/futures:
    get:
      operationId: getSportsFutures
      summary: Season/tournament futures markets across sports
      description: Returns futures-style (non-per-game) sports markets for providers Polymarket and
        Predict.fun (open/active status only). Per-match/per-game rows are excluded; each event is
        classified into a (category, league) pair from a large token/phrase table, falling back to
        an "other" bucket, and titles that look like props/game lines/drafts or, for the "other"
        bucket only, non-sport mis-tags are dropped. Each event is capped at 30 outcomes (highest-priced
        first, unpriced sink to the bottom); the "other" bucket is additionally capped at 150 events
        (richest-first); recognized-sport events are uncapped. Results are sorted by market count
        descending. Responses are served from a cache kept warm in the background. With no parameters
        the whole list is returned; `mode`, `category`, `providers` and `q` filter it, and `limit`
        switches to pages carrying `total` and `nextOffset`.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: mode
        in: query
        required: false
        description: '`sports` leaves out esports and the "other" bucket unless `category` names one;
          `esports` keeps only esports futures and filters `category` by title.'
        schema:
          type: string
          enum:
          - sports
          - esports
      - name: category
        in: query
        required: false
        description: '`all`, a catalog category id (e.g. `basketball`, `other`), or with `mode=esports`
          an esports title slug or `__esports_other` for titles outside the catalog.'
        schema:
          type: string
          maxLength: 64
          default: all
      - name: providers
        in: query
        required: false
        description: Comma-separated provider names to keep (e.g. `polymarket,predictfun`).
        schema:
          type: string
          maxLength: 200
      - name: q
        in: query
        required: false
        description: Space-separated terms that must all appear in the event title or a contender label.
        schema:
          type: string
          maxLength: 200
      - name: offset
        in: query
        required: false
        description: Position in the filtered list to start from; use the previous page's `nextOffset`.
        schema:
          type: integer
          minimum: 0
          default: 0
      - name: limit
        in: query
        required: false
        description: Page size. When set, the response carries `total` and `nextOffset`.
        schema:
          type: integer
          minimum: 1
          maximum: 100
      responses:
        '200':
          description: Futures/outright markets grouped by event, each with its priced outcomes.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=60, s-maxage=300
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsFuturesResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/tournament-bracket:
    get:
      operationId: getSportsTournamentBracket
      summary: Tournament bracket with live cross-provider prices per tie
      description: 'Builds a tournament bracket for `league`. Bracket structure and results are sourced
        from third-party sports-data providers depending on the league. Each tie/match is enriched
        with live prices from Polymarket, Kalshi (authenticated API), and Predict.fun where a
        corresponding market can be matched. `format=symmetric` (default)
        returns a left/right bracket converging on `final`, with an optional `thirdPlace` match and, for
        `format=groups-then-knockout`, a `groups` stage with standings tables. `format=left-to-right`
        instead returns all rounds in `leftRounds` with an empty `rightRounds`. `winner` is always null
        — this endpoint never resolves a champion. Responses are briefly cached.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 10/minute
      x-kairos-bucket: heavy
      parameters:
      - name: league
        in: query
        required: true
        description: League slug, e.g. "ucl" (UEFA Champions League). Must resolve via a known
          league table.
        schema:
          type: string
        example: ucl
      - name: format
        in: query
        required: false
        description: Bracket layout. "symmetric" converges left/right rounds on a final; "left-to-right"
          is a single linear round list; "groups-then-knockout" adds a group stage.
        schema:
          type: string
          enum:
          - symmetric
          - left-to-right
          - groups-then-knockout
          default: symmetric
        example: symmetric
      responses:
        '200':
          description: Tournament bracket structure with live provider prices attached per tie.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=60, stale-while-revalidate=3600
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsTournamentBracketResponse'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/game-markets:
    get:
      operationId: getSportsGameMarkets
      summary: Cross-venue market families for a single game, grouped by section
      description: Returns one game's markets grouped into sections such as game lines, halves,
        player props, exact score, and corners. Cross-venue families can include Polymarket, Kalshi,
        Predict.fun, and Hyperliquid. Each family is either a pill of named outcomes or a ladder of
        ordered Over/Under rungs. Tradeable legs carry provider identifiers and pricing; equivalent
        families and outcomes share opaque `mergeKey` and `outcomeKey` values. Combo-capable legs
        include their eligibility and position identifiers. Responses are briefly cached.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: sports
      parameters:
      - name: slug
        in: query
        required: false
        description: Game/event slug. Supply exactly one of `slug` or `game_id`.
        schema:
          type: string
          minLength: 1
          maxLength: 200
        example: nba-lal-bos-2026-01-15
      - name: game_id
        in: query
        required: false
        description: Provider game id. Supply exactly one of `game_id` or `slug`; this is preferred
          when the discovery response carries one.
        schema:
          type: string
          minLength: 1
          maxLength: 64
          pattern: ^[A-Za-z0-9_-]+$
        example: '12345678'
      responses:
        '200':
          description: Cross-venue markets grouped into sections of pill/ladder
            market families.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=30
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsGameMarketsResponse'
        '400':
          description: Neither identifier was supplied, or both were supplied.
          content:
            application/json:
              schema:
                type: object
                required:
                - error
                properties:
                  error:
                    type: string
                    example: exactly one of game_id or slug is required
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/combo-markets:
    get:
      operationId: getSportsComboMarkets
      summary: Catalog of combo-eligible Polymarket markets
      description: Returns the catalog of markets eligible for parlay/combo construction, filtered to
        Polymarket only (combos are Polymarket-only). Responses are briefly cached, with a self-healing
        guard that discards and rebuilds a cache entry left over from a pre-migration response shape.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      responses:
        '200':
          description: Combo-eligible market catalog.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=300
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsComboMarketsResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /sports/metadata:
    get:
      operationId: getSportsMetadata
      summary: Synced sports teams and leagues reference data
      description: Returns every synced team and league for frontend lookups (logos, abbreviations, aliases,
        colors), ordered by name/sport. Served from a shared server cache refreshed every 15 minutes and
        never more than 24 hours old; the `Age` header gives the seconds since the body was built.
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: All synced teams and leagues.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=3600, s-maxage=3600
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsMetadataResponse'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /matched-markets/enriched:
    get:
      operationId: getEnrichedMatchedMarkets
      summary: Matched markets with identifiers, outcomes, and current reference prices
      description: 'Returns the same verified, live-filtered pair catalog as `/matched-markets`,
        with `details` and `pricing` attached to both sides. Unsupported markets, missing quotes,
        and individual venue failures produce `pricing: null` without changing pair membership or
        pagination. Prices use each venue''s API scale and are reference prices, not executable bids
        or asks. The page is capped at 150 pairs to bound request cost. Responses are not cacheable.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: sports
      parameters:
      - name: limit
        in: query
        required: false
        description: Maximum number of enriched pairs to return.
        schema:
          type: integer
          minimum: 1
          maximum: 150
          default: 50
        example: 50
      - name: offset
        in: query
        required: false
        description: Legacy pagination offset. Do not combine with cursor.
        schema:
          type: integer
          minimum: 0
          default: 0
        example: 0
      - name: cursor
        in: query
        required: false
        description: Opaque keyset cursor. Send an empty value on the first page, then pass each
          response next_cursor. A 409 requires restarting the walk.
        schema:
          type: string
          maxLength: 16384
        example: ''
      - name: provider
        in: query
        required: false
        description: Filter to pairs where either side belongs to this provider.
        schema:
          type: string
        example: polymarket
      - name: min_similarity
        in: query
        required: false
        description: Minimum similarity, clamped server-side to at least 0.82.
        schema:
          type: number
          format: double
          minimum: 0.0
          maximum: 1.0
          default: 0.82
        example: 0.85
      - name: sort_by
        in: query
        required: false
        description: Offset-mode sort column. Cursor mode orders by canonical pair identity.
        schema:
          type: string
          enum:
          - similarity
          - updated_at
          default: similarity
      - name: include_total
        in: query
        required: false
        description: Include the total matching-pair count under pre-live filters.
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: A page of verified pairs with details and best-effort reference pricing.
          headers:
            Cache-Control:
              schema:
                type: string
                example: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsEnrichedMatchedMarketsResponse'
        '400':
          description: Unknown provider, invalid cursor/sort, or cursor combined with a non-zero offset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '409':
          description: The catalog changed during a cursor walk; restart with an empty cursor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: Matched-market or enrichment data could not be loaded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
  /matched-markets:
    get:
      operationId: getMatchedMarkets
      summary: Paginated catalog of verified cross-venue matched markets
      description: 'Returns embedding-verified cross-venue market matches (verified matches only),
        joined against market metadata for display (title, ticker, images, expiry, status).
        `min_similarity` is clamped server-side to never go below a minimum threshold regardless of
        what the caller passes. Pairs are dropped if either correlation side has expired, if either side
        references a provider that is unknown or not currently indexing-enabled, or if either side''s resolved market status is
        settled/closed/resolved; a side with no metadata available (unmirrored rather than de-indexed)
        is kept with null display fields. `has_more` reflects whether the underlying page (before the
        live-status post-filter) was full, so it is not a strict function of the returned
        `count`/`pairs` length. `total` is only computed (filters applied before the live-status
        post-filter) when `include_total=true`. This is treated as a primary, non-additive endpoint:
        any backend failure returns 503 rather than a degraded/empty result.'
      tags:
      - Sports
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: sports
      parameters:
      - name: limit
        in: query
        required: false
        description: Maximum number of pairs to return.
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 50
        example: 50
      - name: offset
        in: query
        required: false
        description: Legacy pagination offset. Do not combine with cursor.
        schema:
          type: integer
          minimum: 0
          default: 0
        example: 0
      - name: cursor
        in: query
        required: false
        description: Opaque keyset cursor. Send an empty value on the first page, then pass each
          response next_cursor. Cursor mode orders by immutable canonical pair identity so catalogue
          mutations cannot shift page boundaries, and binds every page to one full-set fingerprint.
        schema:
          type: string
          maxLength: 16384
        example: ''
      - name: provider
        in: query
        required: false
        description: Filter to pairs where either side belongs to this provider. Must resolve to a known
          provider name (case-insensitive) or the request fails with 400.
        schema:
          type: string
        example: polymarket
      - name: min_similarity
        in: query
        required: false
        description: Minimum embedding similarity score. Always clamped up server-side to a minimum
          threshold even if a lower value is supplied.
        schema:
          type: number
          format: double
          minimum: 0.0
          maximum: 1.0
          default: 0.82
        example: 0.85
      - name: sort_by
        in: query
        required: false
        description: Sort column. Must be "similarity" or "updated_at"; any other value fails with 400.
        schema:
          type: string
          enum:
          - similarity
          - updated_at
          default: similarity
        example: similarity
      - name: include_total
        in: query
        required: false
        description: When true, runs an additional COUNT query and includes the total matching-pair count
          in the response.
        schema:
          type: boolean
          default: false
        example: false
      responses:
        '200':
          description: A page of verified cross-venue matched market pairs.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SportsMatchedMarketsResponse'
        '400':
          description: Unknown provider name, or sort_by is not one of the supported columns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Unknown provider 'foo'
        '409':
          description: The correlation catalogue changed during a cursor walk. Discard accumulated
            pages and restart with an empty cursor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: The correlation store could not be queried.
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                    example: Correlation store unavailable
  /arb-bets/:
    get:
      operationId: getArbOpportunities
      summary: Cross-venue arbitrage opportunities from the Arb Bets feed
      description: |
        Server-side proxy to the Arb Bets vendor feed, called with a Kairos-held key so the credential never reaches a client. Fixed vendor parameters: $100 investment, 1.0% minimum profit. The body is the vendor's payload passed through unchanged — Kairos does not normalize it, and its shape is owned by the vendor. Note the **trailing slash**: the route is registered at `/arb-bets/`, and `/arb-bets` redirects.
      tags:
      - Markets
      x-kairos-auth: session
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      responses:
        '200':
          description: Vendor payload, passed through.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ArbBetsResponse'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '500':
          description: No Arb Bets key is configured on this deployment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Arb Bets API key not configured
        '503':
          description: Kairos' vendor credential was rejected upstream.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Arb Bets is down right now. Please check back later.
        default:
          description: Any other upstream failure is passed through with the vendor's status code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: 'Arb Bets API error: 502'
  /market-links/featured:
    get:
      operationId: getFeaturedMarketLinks
      summary: Ranked cross-venue links with a union book
      description: |
        The cross-venue "matches" catalog: every approved global market link (one real-world contract listed on 2–4 venues) for which order execution asserts a union order book per side. Rows are ranked by the largest Discover hourly-volume score across their legs and paged by an opaque `cursor`. Served straight from the catalog cron's Redis keys, rebuilt every 60 s; empty until the first cycle after a deploy. Each leg's `executable` mirrors what `GET /orders/route-fees` on the execution API reports for that venue, so a row says which venues an order can be routed to and which are display-only.

        Cached with `Cache-Control: public, max-age=15, s-maxage=30`.
      tags:
      - Markets
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: limit
        in: query
        description: Rows per page.
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
      - name: cursor
        in: query
        description: The previous page's `nextCursor`; omit for the first page.
        schema:
          type: string
          maxLength: 16
      responses:
        '200':
          description: One page of links in rank order.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=15, s-maxage=30
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketLinksFeaturedResponse'
        '400':
          description: A cursor this endpoint did not issue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: The catalog store could not be read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /market-links/lookup:
    get:
      operationId: lookupMarketLinks
      summary: The cross-venue link holding each venue market
      description: |
        Batched reverse lookup from venue markets to the catalog row that holds them. A market is matched by its executor market id, its stream key (the numeric Gamma id for Polymarket), or — for a Kalshi two-ticker game — the ticker that books the link's NO side. References without a published link are omitted from the response rather than returned empty.

        Cached with `Cache-Control: public, max-age=15, s-maxage=30`.
      tags:
      - Markets
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 200/minute
      x-kairos-bucket: market_data
      parameters:
      - name: markets
        in: query
        required: true
        description: Comma-separated `<provider>:<marketId>` references, max 100 distinct. Only the first colon separates the two parts, so market ids may contain colons; the provider is case-insensitive.
        schema:
          type: string
          minLength: 1
          maxLength: 8192
        example: polymarket:0x8a9f8be5bef9bc8d36c7895707a4f2561d49de9f5467b6675ba206196e8bd2e2,kalshi:KXNFLGAME-26OCT01PITCLE-CLE
      responses:
        '200':
          description: The link for each referenced market that has one, keyed by the reference as given (provider lower-cased).
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=15, s-maxage=30
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketLinksLookupResponse'
        '400':
          description: No parseable `<provider>:<marketId>` reference, or more than 100 of them.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: The catalog store could not be read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
  /market-clusters:
    get:
      operationId: getMarketClusters
      summary: Cross-venue cluster membership for a set of markets
      description: |
        Resolves each `<provider_id>:<market_id>` reference to its equivalence cluster at the requested tier floor and returns the cluster's other live members. Members listed on a venue whose indexing is switched off, or whose status is settled/closed/resolved, are excluded; a reference whose cluster keeps fewer than two live members after that filter is omitted from the response entirely. Membership is capped at 32 per cluster, with `truncated` and the stored `size` stating what was left out.
      tags:
      - Markets
      x-kairos-auth: public
      security: []
      x-kairos-rate-limit: 60/minute
      x-kairos-bucket: sports
      parameters:
      - name: markets
        in: query
        required: true
        description: Comma-separated `<provider_id>:<market_id>` references, max 200. Only the first
          colon separates the two parts, so market ids may themselves contain colons. Unparseable and
          duplicate references are dropped silently.
        schema:
          type: string
        example: 1:0x1234abcd,2:KXNBA-26JAN15-LAL
      - name: floor
        in: query
        description: Tier floor to resolve membership at.
        schema:
          type: string
          enum:
          - exact
          - semantic
          default: exact
      responses:
        '200':
          description: Cluster membership keyed by the requested reference.
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=10
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketClustersResponse'
        '400':
          description: Unknown `floor`, no parseable `<provider_id>:<market_id>` reference, or more
            than 200 references.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '503':
          description: The cluster store could not be queried.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: Cluster store unavailable
  /txodds/fixtures:
    get:
      operationId: listTxoddsFixtures
      summary: List sports fixtures with live status and win probabilities
      description: |
        Fixture board backed by the TxODDS score event store. Kick-off bounds are snapped to the minute — `from_ms` rounds down and `to_ms` rounds up, so the window may extend up to 59s past what was asked for. Per-row status is computed at cache time from feed phase plus wall clock, so a live-status flip can lag by up to one cache TTL. Concurrent misses on the same page share a single build.
      tags:
      - Sports
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: competition_id
        in: query
        description: Restrict to one competition.
        schema:
          type:
          - integer
          - 'null'
      - name: sport
        in: query
        schema:
          type:
          - string
          - 'null'
          enum:
          - soccer
          - usfootball
      - name: from_ms
        in: query
        description: Kick-off lower bound, epoch milliseconds. Rounded down to the minute.
        schema:
          type:
          - integer
          - 'null'
      - name: to_ms
        in: query
        description: Kick-off upper bound, epoch milliseconds. Rounded up to the minute. When both
          bounds are given the span may not exceed 400 days.
        schema:
          type:
          - integer
          - 'null'
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
      - name: offset
        in: query
        description: Bounded so deep paging cannot mint unlimited cache keys.
        schema:
          type: integer
          minimum: 0
          maximum: 5000
          default: 0
      responses:
        '200':
          description: One page of fixtures.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TxoddsFixtureList'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '422':
          description: |
            Parameter validation failed — unknown `sport`, out-of-range `limit`/`offset`, `from_ms` greater than `to_ms`, or a window wider than 400 days. The inverted/oversized-window cases return the plain `{"detail": "..."}` envelope, not the field-list envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: from_ms must not exceed to_ms
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /txodds/fixtures/{fixture_id}:
    get:
      operationId: getTxoddsFixture
      summary: Score, timeline, and possession metrics for one fixture
      description: |
        Full detail for a single fixture: scoreline, event timeline, and possession-derived metrics. Independent parts are read concurrently and degrade individually — a part that fails is returned in its empty shape rather than failing the response, and the degraded payload is cached for a shorter time than a clean one. Possession metrics are soccer-only; `usfootball` fixtures carry their situational state in `football` instead.
      tags:
      - Sports
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: fixture_id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          exclusiveMaximum: 9223372036854775808
        example: 4821993
      responses:
        '200':
          description: Fixture detail, possibly degraded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TxoddsFixtureDetail'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '404':
          description: No such fixture. Misses are cached, so a repeated probe for an unknown id does
            not reach the store.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
              example:
                detail: fixture 4821993 not found
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
  /txodds/fixtures/{fixture_id}/timing:
    get:
      operationId: getTxoddsFixtureTiming
      summary: Source-observed soccer and NFL fixture timing
      description: Fresh source clock evidence for one TxODDS fixture. No response caching
        or client countdown extrapolation. Missing, legacy, stale, interrupted, unsupported,
        or malformed evidence returns usableForLateGame=false with an explicit reason.
        Requires a verified fixture-to-market mapping before use by a trading strategy.
      tags:
      - Sports
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: fixture_id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          exclusiveMaximum: 9223372036854775808
      responses:
        '200':
          description: Timing evidence, including explicit unavailability.
          headers:
            Cache-Control:
              schema:
                type: string
              description: no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TxoddsFixtureTiming'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '404':
          description: Fixture not found.
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
        '502':
          description: Invalid source observation.
        '500':
          description: Timing source unavailable; no eligible fallback is returned.
  /txodds/fixtures/{fixture_id}/timeseries:
    get:
      operationId: getTxoddsFixtureTimeseries
      summary: Win-probability history and market read for one fixture
      description: |
        The odds-derived curves for a fixture, split out from the detail because they read a table orders of magnitude larger than the score store. Both parts degrade independently: a failed win-probability read returns an empty history, a failed market read returns null projections, and a degraded payload gets a shorter cache TTL. Otherwise the TTL follows kick-off and the last sample.
      tags:
      - Sports
      x-kairos-auth: api-key
      x-kairos-rate-limit: 100/minute
      x-kairos-bucket: default
      parameters:
      - name: fixture_id
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
          exclusiveMaximum: 9223372036854775808
        example: 4821993
      responses:
        '200':
          description: Win-probability samples and the current market read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TxoddsFixtureTimeseries'
        '401':
          $ref: '#/components/responses/DataUnauthorized'
        '403':
          $ref: '#/components/responses/DataForbidden'
        '404':
          description: No such fixture.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataError'
        '422':
          $ref: '#/components/responses/DataValidationError'
        '429':
          $ref: '#/components/responses/DataRateLimited'
