openapi: 3.0.3
info:
  title: Predictor SDK API
  description: >
    Public consumer API only. Internal dashboard billing routes and provider
    webhook handlers are intentionally excluded from this spec.


    Authenticated responses expose `X-RateLimit-Limit`,
    `X-RateLimit-Remaining` and `X-RateLimit-Reset`; reset is a Unix
    timestamp in seconds. API-key `429` responses also include `Retry-After`
    in seconds; an upstream provider `429` may omit it. Unauthenticated
    routes carry none of them — `GET /v1/plans`, and the three router-level
    errors described below — because the limiter is only consulted once a
    key has been read.


    **Pace a client from `X-RateLimit-Limit` and `429`, not from
    `X-RateLimit-Remaining`.** The limiter's budget recovers continuously as
    earlier requests age out rather than refilling at a window boundary, so
    `X-RateLimit-Remaining` is advisory: between two strictly sequential
    responses inside one `X-RateLimit-Reset` window it may repeat, skip
    values, or increase, and it over-states the budget while you are
    spending it. Measured live 2026-08-26 on a 1000/min key — 42 increases
    across 120 sequential requests, largest +8, over-stating the true
    remaining budget by up to 44 by the end of a burst. A local limiter that
    works counts its own requests over a rolling 60 seconds against
    `X-RateLimit-Limit` (which equals the `rate_limit_per_min` published for
    your plan on `GET /v1/plans`) and backs off when a `429` arrives,
    honouring `Retry-After`. `X-RateLimit-Reset` bounds how long that `429`
    can last; it is not the instant a fresh allowance begins. This is stated
    at the API level rather than per operation because it is true of every
    authenticated route here, and the header descriptions carry the detail.


    Every error, on every route, is an `ErrorResponse`: a stable `code` to
    branch on, a `message` for people, `status_code`, and, when the error
    concerns a parameter, `param` (with `errors` listing every problem when
    there are several). Three errors come from the router rather than from
    an endpoint, so they are documented here instead of under an operation.
    A path that matches no route answers `404` with code
    `route_not_found`. A path that exists but not under the method you
    sent answers `405` with code `method_not_allowed` and an `Allow` header
    naming the methods it does accept, repeated in `message`. A request
    carrying a query parameter the endpoint does not read answers `400`
    with code `unknown_parameter`, naming the rejected key and listing the
    ones that endpoint accepts; one that sends a parameter twice (other
    than the repeatable `event_id` and `source_id` of `GET
    /v1/matching-markets/sports/lookup`), or a filter or
    identifier with an empty value, answers `400` with code
    `invalid_parameter`, as does a query string that is not `&`-separated,
    percent-encoded `key=value` pairs (a `;` separator, or a `%` not
    followed by two hex digits). None requires authentication — the router answers
    before the API key is read. They are stated at the API level because
    an unrouted path has no path item, a rejected method has no operation
    object, and the parameter checks are true of every route here:
    declaring `405` on an operation below would document a status that
    operation never returns.


    **Every response carries an `X-Request-Id` header**: successes, every
    error, the three router-level ones above, `401` and `429`, with or
    without an API key. It is a short opaque string that names that one
    request on our side, so quote it when you report a problem. Treat it as
    opaque: do not parse it or rely on its length or prefix. The API
    generates a new one for every request and ignores any `X-Request-Id` you
    send, so it is not a way to tag your own requests. It is stated at the
    API level rather than per operation because it is true of every route
    here, and the `RequestId` header component carries the detail.


    **Every query parameter you send must be one the endpoint declares.**
    An unrecognized key is rejected rather than ignored, on every route,
    because a silently dropped parameter returns a full unfiltered response
    that looks filtered — a caller who guesses `?platform=` on `GET
    /v1/markets` would otherwise receive the whole catalog believing it was
    narrowed. The parameters each endpoint reads are the ones listed under
    it below, and nothing else; a generated SDK client cannot send anything
    else, so this only affects hand-built requests. A parameter this API
    renamed answers `400` naming its replacement: `platform` became
    `provider` on `GET /v1/markets/{market_id}` and `GET
    /v1/events/{event_id}`, and `pagination_key` became `cursor` on `GET
    /v1/crypto-prices/binance`. Provider webhook callbacks are the one
    exception, and they are not part of this spec.
  version: 0.1.4
servers:
  - url: https://api.predictorsdk.com
    description: Production
paths:
  /v1/plans:
    get:
      summary: List public API plans
      description: >
        Returns the machine-readable public billing catalog used by API
        consumers and pricing surfaces. This endpoint is intentionally
        unauthenticated. Stripe price IDs and all other provisioning secrets
        are excluded from the response.
      operationId: getPlans
      security: []
      responses:
        "200":
          description: Public plan catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlansResponse"
              examples:
                default:
                  summary: Public plan catalog
                  value:
                    data:
                      - key: free
                        name: Free
                        billing_tier: free
                        trial_days: 0
                        tagline: Sports matching, crypto prices, and Polymarket wallet data while you build.
                        monthly_price_cents: 0
                        included_requests_per_month: 1000
                        overage_cents_per_1k: 0
                        rate_limit_per_min: 10
                        max_keys: 1
                        features:
                          - Sports matching (list with league, date and participant filters, lookup by identifier, opt-in canonical submarkets)
                          - Binance crypto prices
                          - Unified markets catalog with provider and category filters (/v1/markets, /v1/categories)
                          - Market detail with live prices, liquidity & volume (/v1/markets/{market_id})
                          - Event lookup with Kalshi sibling fanout (/v1/events/{event_id})
                          - Polymarket wallet profiles (/v1/polymarket/wallet)
                          - Polymarket wallet positions (/v1/polymarket/wallet/positions)
                          - Canonical event and market identities
                          - Community support
                        cta_label: Start free
                        highlighted: false
                        contact_sales: false
                      - key: starter
                        name: Starter
                        billing_tier: starter
                        trial_days: 0
                        tagline: Production REST access for early apps and internal tools.
                        monthly_price_cents: 2900
                        included_requests_per_month: 100000
                        overage_cents_per_1k: 25
                        rate_limit_per_min: 60
                        max_keys: 3
                        features:
                          - Everything in Free
                          - 100K monthly requests and 60 req/min
                          - Email support
                        cta_label: Subscribe
                        highlighted: false
                        contact_sales: false
                      - key: pro
                        name: Pro
                        billing_tier: pro
                        trial_days: 0
                        tagline: Higher-throughput research and heavier production workloads.
                        monthly_price_cents: 7900
                        included_requests_per_month: 500000
                        overage_cents_per_1k: 12
                        rate_limit_per_min: 300
                        max_keys: 10
                        features:
                          - Everything in Starter
                          - 500K monthly requests and 300 req/min
                          - Priority support
                        cta_label: Subscribe
                        highlighted: true
                        contact_sales: false
                      - key: business
                        name: Business
                        billing_tier: enterprise
                        trial_days: 0
                        tagline: High-volume team workloads with room for realtime expansion.
                        monthly_price_cents: 19900
                        included_requests_per_month: 2000000
                        overage_cents_per_1k: 6
                        rate_limit_per_min: 1000
                        max_keys: 25
                        features:
                          - Everything in Pro
                          - 2M monthly allowance and 25 API keys
                          - Best fit for larger teams and future realtime workflows
                        cta_label: Subscribe
                        highlighted: false
                        contact_sales: false
                      - key: enterprise
                        name: Enterprise
                        billing_tier: ""
                        trial_days: 0
                        tagline: Custom onboarding, SLAs, and bespoke throughput.
                        monthly_price_cents: 0
                        included_requests_per_month: 0
                        overage_cents_per_1k: 0
                        rate_limit_per_min: 0
                        max_keys: 0
                        features:
                          - Custom limits and support model
                          - SLA and dedicated escalation path
                          - Manual provisioning for bespoke workloads
                        cta_label: Contact sales
                        highlighted: false
                        contact_sales: true
        "503":
          description: Public plan catalog is not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/matching-markets/sports:
    get:
      summary: List matched sports events
      description: >
        Lists the sports events that more than one venue has matched, soonest
        scheduled start first, with cursor-based pagination (default
        `limit=25`, max `100`). A game stays listed through the day after
        its scheduled date (America/New_York), so the first events can be
        games that have already been played; `include_settled=true` adds
        older ones. Coverage is NBA, WNBA, NHL, MLB, and NFL;
        `data[].league` names the league and is the first segment of the
        canonical `event_id`. Every event has one shape for every venue: it
        lists its participants and submarkets, and each submarket lists the
        venue markets matched to it in `source_markets[]`. Every one of those
        carries the same references: `provider`, the provider's own parent
        `event_id` for `GET /v1/events/{event_id}`, its `market_id` for `GET
        /v1/markets/{market_id}`, and outcome IDs mapped to canonical
        outcomes. A page covers events whose full-game moneyline is matched,
        and each carries only that submarket; `include_submarkets=true` adds
        every other matched submarket, and events matched only on spreads,
        totals, team totals or props.


        Each event says when it is scheduled. `scheduled_date` is the game's
        calendar day in America/New_York and is always present;
        `scheduled_start` is its start time in UTC when a venue published one,
        and `null` when the venues published only a date.


        Games whose date has passed are excluded unless you ask for them with
        `include_settled=true`. Narrow the list with `league`,
        `scheduled_date` and `participant`: each is a membership filter on
        the same matched set, they compose, and `pagination.total` counts the
        filtered set rather than every event. To look events up by an
        identifier you already hold, a canonical `event_id` or a venue's own
        market, slug, event, outcome token or `conditionId`, use `GET
        /v1/matching-markets/sports/lookup`: it answers in one call, in full,
        and says what each identifier found, which a page cannot.


        Player props use strict settlement-equivalent matching by default.
        Set `include_submarkets=true&player_prop_match=same_prop` to compare
        roster-verified props with the same player, game, statistic, full-game
        period, and threshold even when settlement rules differ or remain
        unverified. Each player prop includes a nine-dimension rule matrix.
        This policy applies only to player props, not game lines; a same-prop
        match is not a guarantee of identical payouts or a perfect hedge.


        Every moneyline, spread, total and team total carries `settlement_equivalence`:
        whether the venues in the submarket settle a tied game, overtime and
        extra innings, and a line that lands exactly the same way. Send
        `include_rules=true` to also get the rows behind it,
        `rule_comparisons`: three rows, `tie`, `overtime` and `push`, with
        each venue's value, a description and a link to the evidence. They
        are left out by default because they are most of an event's size.
        The matrix is published beside the match and never changes which
        venues are paired: a venue that settles an NFL tie differently from
        another is still in the submarket, and the matrix says so. A rule
        nobody has written down is `unknown`, and unknown never means
        equivalent.


        A team total is a team's own total (`market_type=team_total`, "the
        Eagles over 24.5 points"), not the game's: its `subject` is the team,
        its `metric` is `points`, and two venues' lines are one submarket only
        for the same team, period and line. Today it is matched for the NFL,
        on Kalshi and Polymarket.


        Matching runs continuously in the background and this endpoint reads
        the stored result, so every response carries `snapshot.observed_at`:
        how old the venue data behind it is. Check it before treating an
        event a response does not contain as unmatched — an empty page from a
        snapshot that stopped updating looks exactly like one from a current
        snapshot.
      operationId: getSportsMatchingMarkets
      x-fern-pagination:
        cursor: $request.cursor
        next_cursor: $response.pagination.next_cursor
        results: $response.data
      security:
        - BearerAuth: []
      x-unkey-permission: markets.matching.read
      parameters:
        - name: limit
          in: query
          required: false
          description: >
            Maximum number of matched events to return per page. Range 1–100,
            default 25.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          description: >
            Opaque cursor from a previous response's `pagination.next_cursor`
            (`nextCursor` in TypeScript, `NextCursor` in Go). Must be used
            with the same filter set — a cursor from
            `include_settled=true` cannot be replayed against
            `include_settled=false`, and a cursor from one `league`,
            `scheduled_date` or `participant` cannot be replayed under
            another; either returns `400`.
          schema:
            type: string
        - name: league
          in: query
          required: false
          description: >
            Restrict the list to one league, matched against each event's own
            `league` value. Without it every league is listed.


            Only the leagues listed here are accepted, case-insensitively. Any
            other value returns `400` listing them — an unrecognized filter is
            never ignored, because a silently dropped filter returns a full
            page that looks filtered.


            `pagination.total` counts only that league's events, and cursors
            are bound to the filter that created them: replay a `next_cursor`
            with the same filters, or start again from the first page.
          schema:
            type: string
            enum:
              - nba
              - wnba
              - nfl
              - mlb
              - nhl
        - name: scheduled_date
          in: query
          required: false
          description: >
            Restrict the list to the games on one calendar day, as
            `YYYY-MM-DD` in America/New_York: the `scheduled_date` each event
            publishes. That is the day its `event_id` ends in, or later for a
            postponed game. A game that starts at 8:30 PM Eastern on the 20th
            is on the 20th, although its `scheduled_start` is on the 21st in
            UTC.


            The default population is today's games plus a one-day grace (see
            `include_settled`), so a date further in the past matches nothing
            unless `include_settled=true`, which also reads the days of the
            settled archive. Any value that is not a calendar date in that
            form returns `400`. `pagination.total` counts only that day's
            events, and cursors are bound to the filter.
          schema:
            type: string
            format: date
        - name: participant
          in: query
          required: false
          description: >
            Restrict the list to events that have a participant with this
            `key` (`participants[].key`, such as `okc`), matched
            case-insensitively. Participant keys are unique only within a
            league: `la` is the Rams in the NFL and the Sparks in the WNBA, so
            pair this with `league` unless you want both. Send one key; a
            comma-joined value is a `400`. `pagination.total` counts only the
            matching events, and cursors are bound to the filter.
          schema:
            type: string
        - name: include_settled
          in: query
          required: false
          description: >
            Selects which events this request draws from. Defaults to
            `false`: the current games, meaning events whose scheduled start
            has not certainly passed — today's games, plus a one-day grace so
            a late start that runs past midnight Eastern is never dropped
            mid-play. Set it to `true` to also get the settled archive:
            events that left the live matching run, usually because the
            venues settled or delisted them, kept for 30 days. That adds games
            whose date is further in the past, including ones a venue still
            lists as open, and games a venue cancelled, which can be
            future-dated: Polymarket resolves a game that was never played
            50-50, as it did a postseason game its series never needed,
            before its scheduled day. The default population leaves a
            cancelled game out, so `false` never lists a game that was not
            played.


            Venues settle and delist a game's markets at different times, so
            the endpoint filters on the game's own date (its scheduled start,
            and never earlier than the date its `event_id` ends in) rather than
            on an upstream status. Nothing is reported as settled that the
            venue has not settled; these events are simply not *current*,
            which is what the default page is for. A postponed game takes its
            new date, so its event stays current until the makeup game.


            A game that has started is served with the fullest mapping it
            had — the version from before its first venue settled and
            delisted it — rather than only the venues still listing it. That
            holds in the default population too, so today's finished games
            stay listed with every venue until they are past-dated; with
            `true` it holds for older games as well. Games archived before
            2026-10-04 21:26 UTC kept only the venues that still listed them
            when they left (usually two), and age out of the 30-day archive by
            2026-11-04.
          schema:
            type: boolean
            default: false
        - name: player_prop_match
          in: query
          required: false
          description: >
            Player-prop admission policy. It requires `include_submarkets=true`;
            with `include_submarkets=false`, which is the default on a page,
            it is a 400. Matched case-insensitively. `strict` (the
            default) includes only groups of at least two distinct providers
            with verified player/game/stat/threshold/side identity and a
            completely reviewed equivalent settlement profile. `same_prop`
            retains exact prop identity but also admits different or
            unverified settlement rules, identified by `settlement_equivalence`
            and `rule_comparisons`. Unknown rules never prove equivalence.
            Applies with `include_settled`; cursors cannot be reused between
            policies. Does not change game-line matching or the default
            moneyline projection. Historical snapshots without verified player
            identity do not expose props.
          schema:
            type: string
            enum: [strict, same_prop]
            default: strict
        - name: include_submarkets
          in: query
          required: false
          description: >
            When `true`, each event lists every matched submarket (spreads,
            totals, period lines and player props) instead of only its
            full-game moneyline, and events matched only on those submarkets
            are included too. When `false`, an event appears only when its
            full-game moneyline is matched, so `submarkets` is never empty.
            Defaults to `false`, which keeps a page small. Every submarket has
            the same shape, so code written against the moneyline reads the
            rest unchanged. This is an identity mapping only; fetch current
            status, quotes, and liquidity from the referenced market
            resources. Explicit `player_prop_match` requires this to be
            `true`.


            Such pages are large: on 2026-10-04 a 50-event page with every
            submarket was about 880 KB (190 KB gzipped), and a 20-event
            `player_prop_match=same_prop` page about 3 MB. Send
            `Accept-Encoding: gzip` and a smaller `limit` when you need them.
          schema:
            type: boolean
        - name: include_rules
          in: query
          required: false
          description: >
            When `true`, every moneyline, spread and total also lists
            `rule_comparisons`: the three rows (`tie`, `overtime` and `push`),
            each with every source market's value, a description and an
            evidence link, that sit behind its `settlement_equivalence`. When
            `false`, the default, a game line carries `settlement_equivalence`
            alone: the verdict is always there and only the rows are left out.
            The rows are about 60% of an event's bytes on a busy football
            slate, so ask for them when you want the evidence. A player prop
            always carries its own matrix, whatever this is. The flag changes
            what each event lists and never which events a page returns, so a
            `cursor` works with either value.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: A page of matched canonical events, soonest scheduled start first
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SportsMatchingListResponse"
              examples:
                list_mode:
                  summary: 'List mode: each event''s full-game moneyline, every venue in one shape'
                  value:
                    data:
                    - event_id: nba-okc-sas-2026-10-20
                      sport: basketball
                      league: nba
                      title: Oklahoma City Thunder vs. San Antonio Spurs
                      scheduled_start: '2026-10-20T23:30:00.000Z'
                      scheduled_date: '2026-10-20'
                      participants:
                      - key: okc
                        name: Oklahoma City Thunder
                        role: away
                      - key: sas
                        name: San Antonio Spurs
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:okc
                          label: Oklahoma City Thunder win
                          type: participant
                          side: win
                        - key: winner:sas
                          label: San Antonio Spurs win
                          type: participant
                          side: win
                        source_markets:
                        - provider: kalshi
                          event_id: KXNBAGAME-26OCT20OKCSAS
                          market_id: KXNBAGAME-26OCT20OKCSAS-OKC
                          market_name: Oklahoma City wins
                          outcomes:
                          - canonical_outcome_key: winner:okc
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:sas
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: kalshi
                          event_id: KXNBAGAME-26OCT20OKCSAS
                          market_id: KXNBAGAME-26OCT20OKCSAS-SAS
                          market_name: San Antonio wins
                          outcomes:
                          - canonical_outcome_key: winner:sas
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:okc
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: sxbet
                          event_id: L19766755
                          market_id: '0xb4d047a709aae881e5ccad9d123592967644ee1df1f17078c762b388e41b81c5'
                          market_name: San Antonio Spurs vs Oklahoma City Thunder
                          outcomes:
                          - canonical_outcome_key: winner:sas
                            label: San Antonio Spurs
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:okc
                            label: Oklahoma City Thunder
                            outcome_id: outcomeTwo
                        settlement_equivalence: equivalent
                    - event_id: nfl-pit-no-2026-10-25
                      sport: football
                      league: nfl
                      title: Pittsburgh Steelers vs. New Orleans Saints
                      scheduled_start: '2026-10-25T17:00:00.000Z'
                      scheduled_date: '2026-10-25'
                      participants:
                      - key: pit
                        name: Pittsburgh Steelers
                        role: away
                      - key: 'no'
                        name: New Orleans Saints
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:pit
                          label: Pittsburgh Steelers win
                          type: participant
                          side: win
                        - key: winner:no
                          label: New Orleans Saints win
                          type: participant
                          side: win
                        source_markets:
                        - provider: polymarket
                          event_id: nfl-pit-no-2026-10-25
                          market_id: '4551269'
                          market_name: Steelers vs. Saints
                          market_slug: nfl-pit-no-2026-10-25
                          outcomes:
                          - canonical_outcome_key: winner:pit
                            label: Steelers
                            outcome_id: '115700113006785753352548302007769096699086928102312992900278010746804635156828'
                          - canonical_outcome_key: winner:no
                            label: Saints
                            outcome_id: '16013029087006534797924862977185419395437649810045162973335886530817974510015'
                        - provider: sxbet
                          event_id: L18900793
                          market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                          market_name: New Orleans Saints vs Pittsburgh Steelers
                          outcomes:
                          - canonical_outcome_key: winner:no
                            label: New Orleans Saints
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:pit
                            label: Pittsburgh Steelers
                            outcome_id: outcomeTwo
                        settlement_equivalence: different
                    pagination:
                      limit: 25
                      total: 140
                      has_more: true
                      next_cursor: eyJzIjoiNTRiYmFiZjEiLCJ0IjoiMjAyNi0wNi0wNlQwMDowMDowMFoiLCJpIjoibWxiLWJvcy1ueXktMjAyNi0wNi0wNiJ9
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
                list_filtered:
                  summary: 'Filtered list: league=nfl&scheduled_date=2026-10-25&participant=no (the Saints'' key) matches one game, and pagination.total counts only the filtered set'
                  value:
                    data:
                    - event_id: nfl-pit-no-2026-10-25
                      sport: football
                      league: nfl
                      title: Pittsburgh Steelers vs. New Orleans Saints
                      scheduled_start: '2026-10-25T17:00:00.000Z'
                      scheduled_date: '2026-10-25'
                      participants:
                      - key: pit
                        name: Pittsburgh Steelers
                        role: away
                      - key: 'no'
                        name: New Orleans Saints
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:pit
                          label: Pittsburgh Steelers win
                          type: participant
                          side: win
                        - key: winner:no
                          label: New Orleans Saints win
                          type: participant
                          side: win
                        source_markets:
                        - provider: polymarket
                          event_id: nfl-pit-no-2026-10-25
                          market_id: '4551269'
                          market_name: Steelers vs. Saints
                          market_slug: nfl-pit-no-2026-10-25
                          outcomes:
                          - canonical_outcome_key: winner:pit
                            label: Steelers
                            outcome_id: '115700113006785753352548302007769096699086928102312992900278010746804635156828'
                          - canonical_outcome_key: winner:no
                            label: Saints
                            outcome_id: '16013029087006534797924862977185419395437649810045162973335886530817974510015'
                        - provider: sxbet
                          event_id: L18900793
                          market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                          market_name: New Orleans Saints vs Pittsburgh Steelers
                          outcomes:
                          - canonical_outcome_key: winner:no
                            label: New Orleans Saints
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:pit
                            label: Pittsburgh Steelers
                            outcome_id: outcomeTwo
                        settlement_equivalence: different
                    pagination:
                      limit: 25
                      total: 1
                      has_more: false
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
                list_with_rules:
                  summary: 'include_rules=true: each game line also lists rule_comparisons, the tie, overtime and push rows behind its settlement_equivalence, with every source market''s value and the evidence for it (abridged here to one game: league=nfl&participant=no&include_rules=true)'
                  value:
                    data:
                    - event_id: nfl-pit-no-2026-10-25
                      sport: football
                      league: nfl
                      title: Pittsburgh Steelers vs. New Orleans Saints
                      scheduled_start: '2026-10-25T17:00:00.000Z'
                      scheduled_date: '2026-10-25'
                      participants:
                      - key: pit
                        name: Pittsburgh Steelers
                        role: away
                      - key: 'no'
                        name: New Orleans Saints
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:pit
                          label: Pittsburgh Steelers win
                          type: participant
                          side: win
                        - key: winner:no
                          label: New Orleans Saints win
                          type: participant
                          side: win
                        source_markets:
                        - provider: polymarket
                          event_id: nfl-pit-no-2026-10-25
                          market_id: '4551269'
                          market_name: Steelers vs. Saints
                          market_slug: nfl-pit-no-2026-10-25
                          outcomes:
                          - canonical_outcome_key: winner:pit
                            label: Steelers
                            outcome_id: '115700113006785753352548302007769096699086928102312992900278010746804635156828'
                          - canonical_outcome_key: winner:no
                            label: Saints
                            outcome_id: '16013029087006534797924862977185419395437649810045162973335886530817974510015'
                        - provider: sxbet
                          event_id: L18900793
                          market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                          market_name: New Orleans Saints vs Pittsburgh Steelers
                          outcomes:
                          - canonical_outcome_key: winner:no
                            label: New Orleans Saints
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:pit
                            label: Pittsburgh Steelers
                            outcome_id: outcomeTwo
                        settlement_equivalence: different
                        rule_comparisons:
                        - rule: tie
                          label: 'Tied game or period'
                          comparison: different
                          source_values:
                          - provider: polymarket
                            market_id: '4551269'
                            value: half_payout
                            description: 'The market''s rules, or its game''s rules, resolve a tied game 50-50.'
                            evidence_url: 'https://polymarket.com/event/nfl-pit-no-2026-10-25'
                          - provider: sxbet
                            market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                            value: void
                            description: 'SX Bet''s help center does not say what a tied NFL game pays, but SX Bet voided the moneyline of the 2026-10-11 Giants at Commanders tie (27-27 after overtime, reported outcome 0).'
                            evidence_url: 'https://api.sx.bet/markets/find?marketHashes=0x39ec7ed0520273f139db9e5840917a31c4376306366d69dbb7cc4f21f8f78c18'
                        - rule: overtime
                          label: 'Overtime and extra innings'
                          comparison: equivalent
                          source_values:
                          - provider: polymarket
                            market_id: '4551269'
                            value: included
                            description: 'The market''s rules, or its game''s rules, count overtime.'
                            evidence_url: 'https://polymarket.com/event/nfl-pit-no-2026-10-25'
                          - provider: sxbet
                            market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                            value: included
                            description: 'SX Bet: "All bets on the game and all bets on the second half will include overtime scoring."'
                            evidence_url: 'https://help.sx.bet/en/articles/5090593-american-football-betting-rules'
                        - rule: push
                          label: 'Line landing exactly'
                          comparison: equivalent
                          source_values:
                          - provider: polymarket
                            market_id: '4551269'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://polymarket.com/event/nfl-pit-no-2026-10-25'
                          - provider: sxbet
                            market_id: '0x001cd92a53345352d1e28da827c9f1cab0d417878de7e8f2e979c7dd58ba6c59'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://help.sx.bet/en/articles/3613418-general-betting-rules'
                    pagination:
                      limit: 25
                      total: 1
                      has_more: false
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
        "400":
          description: >
            Invalid query parameters: a `league` outside the covered leagues, a
            `scheduled_date` that is not a calendar date, or an empty filter; a
            retired parameter (its message names the replacement: `event_id`
            and `source_id` moved to `GET /v1/matching-markets/sports/lookup`);
            a parameter sent more than once; an invalid flag, `limit` or
            `cursor`; or a cursor replayed under a different filter set. The
            message quotes the offending value.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the caller's
            paid subscription is in a payment-recovery state. This endpoint is
            available on every plan, so 402 never indicates a tier mismatch
            here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: API key exceeded its rate limit
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: API-key authorization dependency failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Matching markets reader unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/matching-markets/sports/lookup:
    get:
      summary: Look up matched sports events by identifier
      description: >
        Finds the matched sports events that hold identifiers you already
        have, in one unpaginated call: a canonical event key as `event_id`,
        and any identifier a venue publishes (a market's `market_id`,
        `market_slug` or `event_id`; on Polymarket, Predict and AlphaArcade an
        outcome token; on Polymarket also a `conditionId`) as
        `source_id={provider}:{id}`. Send at least one, and up to 100 unique
        identifiers across both parameters.


        Each event found is in `data` once, in the shape `GET
        /v1/matching-markets/sports` lists (with its `scheduled_start` and
        `scheduled_date`), however many identifiers found it. `lookups` lists
        every identifier you sent under its spelling as sent, with the
        `event_id`s it found, or none: that is how a miss is told from a hit
        without comparing your inputs to the response. An identifier that
        finds nothing is a `200`, not a `404`.


        A lookup returns each event in full: every matched submarket, so a
        spread ticker's answer contains the spread it names. Send
        `include_submarkets=false` for only each event's full-game moneyline.
        It reads the same population as the list, so a game whose date has
        passed, or that a venue cancelled, is not found unless
        `include_settled=true`.


        Player props use strict settlement-equivalent matching by default;
        `player_prop_match=same_prop` compares roster-verified props with the
        same player, game, statistic, full-game period, and threshold even
        when settlement rules differ or remain unverified. A prop's own
        identifier finds its game under either policy when the game has
        another matched submarket, while the prop itself appears only when the
        policy admits it.


        Every moneyline, spread, total and team total carries `settlement_equivalence`, as
        on the list route, so a lookup by a Kalshi spread ticker answers
        whether that spread settles like the same line on another venue. Send
        `include_rules=true` to also get the `rule_comparisons` rows behind
        it (`tie`, `overtime` and `push`, with each venue's value and
        evidence).


        Matching runs continuously in the background and this endpoint reads
        the stored result, so every response carries `snapshot.observed_at`:
        how old the venue data behind it is. Check it before treating an
        identifier that found nothing as unmatched — a lookup that finds
        nothing in a snapshot that stopped updating looks exactly like one
        that finds nothing in a current one.
      operationId: lookupSportsMatchingMarkets
      security:
        - BearerAuth: []
      x-unkey-permission: markets.matching.read
      parameters:
        - name: event_id
          in: query
          required: false
          description: >
            Canonical event key(s) to look up (for example,
            `nba-okc-sas-2026-10-20`), matched case-insensitively. Repeat the
            parameter for several events (do not comma-join them), and combine
            it freely with `source_id`, up to 100 unique identifiers in all.
            At least one `event_id` or `source_id` is required. A venue's own
            ID here (a Kalshi ticker, a numeric ID, an SX Bet `L…` fixture, an
            `0x` hash, a ULID or a `{provider}:{id}` composite) is a `400`
            telling you to send it as `source_id`, and so is an empty value.
            So is a value that matches no canonical event but is a venue's
            identifier, most often a Polymarket slug: a night game's slug
            carries the UTC date (`nfl-sea-den-2026-10-16` for the canonical
            `nfl-sea-den-2026-10-15`), and Polymarket spells some teams
            differently (`cal` for Calgary, `la` for the Rams).
          schema:
            type: array
            maxItems: 100
            uniqueItems: true
            items:
              type: string
          style: form
          explode: true
        - name: source_id
          in: query
          required: false
          description: >
            Venue identifier(s) to look up, as `{provider}:{id}`: any
            `market_id`, `market_slug` or `event_id` that a
            `data[].submarkets[].source_markets[]` entry publishes,
            prefixed with that entry's `provider`. On Polymarket, Predict,
            AlphaArcade and Hyperliquid an outcome token
            (`outcomes[].outcome_id`) works too, because those tokens identify
            one market (a Hyperliquid token starts with `#`, which a URL must
            send as `%23`), and on Polymarket so does
            a market's `conditionId`, the ID wallet positions carry. Elsewhere
            outcome IDs are side names such as `yes` that repeat in every
            market: Kalshi's, Pred's and Limitless's `yes`/`no` and SX Bet's
            `outcomeOne`/`outcomeTwo` are a `400` that points to `market_id`.
            For
            example `kalshi:KXNBAGAME-26OCT20OKCSAS` (an event ticker),
            `kalshi:KXNBAGAME-26OCT20OKCSAS-OKC` (a market ticker),
            `sxbet:L19766755` (a fixture), or Pred's
            `pred:<parent_market_id>:<child_market_id>` pair, or a Limitless
            market's slug. Only the text before the first colon is the
            provider, and it must be one of `kalshi`, `polymarket`, `predict`,
            `sxbet`, `alpha-arcade`, `prophetx`, `pred`, `limitless` or
            `hyperliquid`;
            anything else is a `400` naming the value.
            Matching is case-insensitive. `lookups` reports every identifier
            under its spelling as sent (whitespace around the provider and ID
            trimmed), with the canonical events it found. Repeat the parameter
            for several identifiers, across
            providers (a comma-joined list or an empty value is a `400`);
            combined with `event_id`, up to 100 unique identifiers in all. At
            least one `event_id` or `source_id` is required. A lookup finds
            the event an identifier belongs to and, by default, returns it in
            full, so the submarket the identifier names is in it unless
            `include_submarkets=false`. A player prop finds its game under any
            `player_prop_match` policy when the game has another matched
            submarket, while the prop itself appears only when the policy
            admits it; a game matched only on props the policy excludes is not
            returned.
          schema:
            type: array
            maxItems: 100
            uniqueItems: true
            items:
              type: string
          style: form
          explode: true
        - name: include_settled
          in: query
          required: false
          description: >
            Selects which events the lookup searches. Defaults to `false`: only
            events whose scheduled start has not certainly passed — today's
            games, plus a one-day grace so a late start that runs past
            midnight Eastern is never dropped mid-play. Set it to `true` to
            also search the settled archive: events that left the live
            matching run, usually because the venues settled or delisted them.
            That adds games whose date is further in the past, including ones
            a venue still lists as open, and games a venue cancelled, which
            can be future-dated; the default leaves a cancelled game out.


            Because it selects the population, a lookup for a past-dated or
            cancelled game answers `200` with the identifier listed in
            `lookups` with no events, unless this is `true`. A game that has started is served
            with the fullest mapping it had, as on the list: a lookup by
            another venue's identifier finds nothing for games archived before
            2026-10-04 21:26 UTC, which kept only the venues that still listed
            them and age out of the 30-day archive by 2026-11-04.
          schema:
            type: boolean
            default: false
        - name: player_prop_match
          in: query
          required: false
          description: >
            Player-prop admission policy. It requires `include_submarkets` to
            be `true`, which it is by default on a lookup; with
            `include_submarkets=false` it is a 400. Matched
            case-insensitively. `strict` (the default) includes only groups of
            at least two distinct providers with verified
            player/game/stat/threshold/side identity and a completely reviewed
            equivalent settlement profile. `same_prop` retains exact prop
            identity but also admits different or unverified settlement rules,
            identified by `settlement_equivalence` and `rule_comparisons`.
            Unknown rules never prove equivalence. Applies with
            `include_settled`. Does not change game-line matching.
            Historical snapshots without verified player identity do not
            expose props.
          schema:
            type: string
            enum: [strict, same_prop]
            default: strict
        - name: include_submarkets
          in: query
          required: false
          description: >
            When `true`, each event lists every matched submarket (spreads,
            totals, period lines and player props); when `false`, only its
            full-game moneyline, and an event appears only when that is
            matched. Defaults to `true` on a lookup, which returns the events
            it finds in full: a spread ticker shows its spread. Every submarket
            has the same shape, so code written against the moneyline reads the
            rest unchanged. This is an identity mapping only; fetch current
            status, quotes, and liquidity from the referenced market
            resources. Explicit `player_prop_match` requires this to be
            `true`.
          schema:
            type: boolean
            default: true
        - name: include_rules
          in: query
          required: false
          description: >
            When `true`, every moneyline, spread and total also lists
            `rule_comparisons`: the three rows (`tie`, `overtime` and `push`),
            each with every source market's value, a description and an
            evidence link, that sit behind its `settlement_equivalence`. When
            `false`, the default, a game line carries `settlement_equivalence`
            alone: the verdict is always there and only the rows are left out.
            The rows are about 60% of an event's bytes on a busy football
            slate, and a lookup returns every submarket, so ask for them when
            you want the evidence. A player prop always carries its own
            matrix, whatever this is. The flag changes what each event lists
            and never which events are found.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: The matched canonical events the identifiers found, each once, with what each identifier found
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SportsMatchingLookupResponse"
              examples:
                lookup_mode:
                  summary: 'Lookup by two venues'' identifiers for one game: the event is in data once, and lookups names what each identifier found (abridged here to the moneyline)'
                  value:
                    data:
                    - event_id: nba-okc-sas-2026-10-20
                      sport: basketball
                      league: nba
                      title: Oklahoma City Thunder vs. San Antonio Spurs
                      scheduled_start: '2026-10-20T23:30:00.000Z'
                      scheduled_date: '2026-10-20'
                      participants:
                      - key: okc
                        name: Oklahoma City Thunder
                        role: away
                      - key: sas
                        name: San Antonio Spurs
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:okc
                          label: Oklahoma City Thunder win
                          type: participant
                          side: win
                        - key: winner:sas
                          label: San Antonio Spurs win
                          type: participant
                          side: win
                        source_markets:
                        - provider: kalshi
                          event_id: KXNBAGAME-26OCT20OKCSAS
                          market_id: KXNBAGAME-26OCT20OKCSAS-OKC
                          market_name: Oklahoma City wins
                          outcomes:
                          - canonical_outcome_key: winner:okc
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:sas
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: kalshi
                          event_id: KXNBAGAME-26OCT20OKCSAS
                          market_id: KXNBAGAME-26OCT20OKCSAS-SAS
                          market_name: San Antonio wins
                          outcomes:
                          - canonical_outcome_key: winner:sas
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:okc
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: sxbet
                          event_id: L19766755
                          market_id: '0xb4d047a709aae881e5ccad9d123592967644ee1df1f17078c762b388e41b81c5'
                          market_name: San Antonio Spurs vs Oklahoma City Thunder
                          outcomes:
                          - canonical_outcome_key: winner:sas
                            label: San Antonio Spurs
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:okc
                            label: Oklahoma City Thunder
                            outcome_id: outcomeTwo
                        settlement_equivalence: equivalent
                    lookups:
                      kalshi:KXNBAGAME-26OCT20OKCSAS:
                        event_ids:
                        - nba-okc-sas-2026-10-20
                      sxbet:L19766755:
                        event_ids:
                        - nba-okc-sas-2026-10-20
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
                lookup_no_match:
                  summary: 'A lookup that matched nothing: lookups lists the identifier with no events, and snapshot.observed_at shows the snapshot is still updating'
                  value:
                    data: []
                    lookups:
                      kalshi:KXNBA-27-SAS:
                        event_ids: []
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
                lookup_full_event:
                  summary: 'A spread ticker finds its game in full: the moneyline and the spread it names, matched across venues (abridged here to the moneyline and one spread)'
                  value:
                    data:
                    - event_id: nfl-sea-den-2026-10-15
                      sport: football
                      league: nfl
                      title: Seattle Seahawks vs. Denver Broncos
                      scheduled_start: '2026-10-16T00:15:00.000Z'
                      scheduled_date: '2026-10-15'
                      participants:
                      - key: sea
                        name: Seattle Seahawks
                        role: away
                      - key: den
                        name: Denver Broncos
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:sea
                          label: Seattle Seahawks win
                          type: participant
                          side: win
                        - key: winner:den
                          label: Denver Broncos win
                          type: participant
                          side: win
                        source_markets:
                        - provider: kalshi
                          event_id: KXNFLGAME-26OCT15SEADEN
                          market_id: KXNFLGAME-26OCT15SEADEN-DEN
                          market_name: Denver wins
                          outcomes:
                          - canonical_outcome_key: winner:den
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:sea
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: kalshi
                          event_id: KXNFLGAME-26OCT15SEADEN
                          market_id: KXNFLGAME-26OCT15SEADEN-SEA
                          market_name: Seattle wins
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:den
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: polymarket
                          event_id: nfl-sea-den-2026-10-16
                          market_id: '4212356'
                          market_name: Seahawks vs. Broncos
                          market_slug: nfl-sea-den-2026-10-16
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: Seahawks
                            outcome_id: '61085243734073593459419955931627362545950081431149347585770687210061098950896'
                          - canonical_outcome_key: winner:den
                            label: Broncos
                            outcome_id: '78653414984017864835330593213700943479354040352698721042453780387223265497055'
                        - provider: predict
                          event_id: '1966227'
                          market_id: '1966227'
                          market_name: Seahawks vs. Broncos
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: SEA
                            outcome_id: '92983278711950580174489764972959289587766978917748908103204076257830283078463'
                            side: 'yes'
                          - canonical_outcome_key: winner:den
                            label: DEN
                            outcome_id: '75713059634539392977306406262459535635177860280597617684338927195076481444975'
                            side: 'no'
                        - provider: sxbet
                          event_id: L19431555
                          market_id: '0x92288450e95f04c6c5c0d10eba0e820a746903410763401bafdce56a32ffb384'
                          market_name: Denver Broncos vs Seattle Seahawks
                          outcomes:
                          - canonical_outcome_key: winner:den
                            label: Denver Broncos
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:sea
                            label: Seattle Seahawks
                            outcome_id: outcomeTwo
                        settlement_equivalence: different
                      - key: spread|full|spread:den|3.5
                        market_type: spread
                        segment: full
                        display_name: Denver Broncos -3.5
                        metric: spread
                        line: -3.5
                        subject:
                          type: participant
                          key: den
                          name: Denver Broncos
                        outcomes:
                        - key: cover:den
                          label: Denver Broncos covers -3.5
                          type: participant
                          side: cover
                        - key: not_cover:den
                          label: Denver Broncos does not cover -3.5
                          type: participant
                          side: not_cover
                        source_markets:
                        - provider: kalshi
                          event_id: KXNFLSPREAD-26OCT15SEADEN
                          market_id: KXNFLSPREAD-26OCT15SEADEN-DEN4
                          market_name: Denver wins by over 3.5 points?
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: not_cover:den
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: polymarket
                          event_id: nfl-sea-den-2026-10-16
                          market_id: '5126843'
                          market_name: 'Spread: Broncos (-3.5)'
                          market_slug: nfl-sea-den-2026-10-16-spread-home-3pt5
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: Broncos
                            outcome_id: '66687183497995998313187260270461603445037919098033048212310694618993665413167'
                          - canonical_outcome_key: not_cover:den
                            label: Seahawks
                            outcome_id: '34935678240172142451066439913204728345738080612535299714429050260011568826516'
                        - provider: sxbet
                          event_id: L19431555
                          market_id: '0x4703010fbb76917d799c0df6c2886d52b863d04c1578157f431b9684b13bdaa0'
                          market_name: Denver Broncos -3.5 vs Seattle Seahawks +3.5
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: Denver Broncos -3.5
                            outcome_id: outcomeOne
                          - canonical_outcome_key: not_cover:den
                            label: Seattle Seahawks +3.5
                            outcome_id: outcomeTwo
                        settlement_equivalence: equivalent
                    lookups:
                      kalshi:KXNFLSPREAD-26OCT15SEADEN-DEN4:
                        event_ids:
                        - nfl-sea-den-2026-10-15
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
                lookup_with_rules:
                  summary: 'include_rules=true on a lookup: the spread ticker''s game in full, each game line with the rule_comparisons behind its settlement_equivalence (abridged here to the moneyline and one spread)'
                  value:
                    data:
                    - event_id: nfl-sea-den-2026-10-15
                      sport: football
                      league: nfl
                      title: Seattle Seahawks vs. Denver Broncos
                      scheduled_start: '2026-10-16T00:15:00.000Z'
                      scheduled_date: '2026-10-15'
                      participants:
                      - key: sea
                        name: Seattle Seahawks
                        role: away
                      - key: den
                        name: Denver Broncos
                        role: home
                      submarkets:
                      - key: moneyline|full|winner
                        market_type: moneyline
                        segment: full
                        display_name: Winner
                        metric: winner
                        outcomes:
                        - key: winner:sea
                          label: Seattle Seahawks win
                          type: participant
                          side: win
                        - key: winner:den
                          label: Denver Broncos win
                          type: participant
                          side: win
                        source_markets:
                        - provider: kalshi
                          event_id: KXNFLGAME-26OCT15SEADEN
                          market_id: KXNFLGAME-26OCT15SEADEN-DEN
                          market_name: Denver wins
                          outcomes:
                          - canonical_outcome_key: winner:den
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:sea
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: kalshi
                          event_id: KXNFLGAME-26OCT15SEADEN
                          market_id: KXNFLGAME-26OCT15SEADEN-SEA
                          market_name: Seattle wins
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: winner:den
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: polymarket
                          event_id: nfl-sea-den-2026-10-16
                          market_id: '4212356'
                          market_name: Seahawks vs. Broncos
                          market_slug: nfl-sea-den-2026-10-16
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: Seahawks
                            outcome_id: '61085243734073593459419955931627362545950081431149347585770687210061098950896'
                          - canonical_outcome_key: winner:den
                            label: Broncos
                            outcome_id: '78653414984017864835330593213700943479354040352698721042453780387223265497055'
                        - provider: predict
                          event_id: '1966227'
                          market_id: '1966227'
                          market_name: Seahawks vs. Broncos
                          outcomes:
                          - canonical_outcome_key: winner:sea
                            label: SEA
                            outcome_id: '92983278711950580174489764972959289587766978917748908103204076257830283078463'
                            side: 'yes'
                          - canonical_outcome_key: winner:den
                            label: DEN
                            outcome_id: '75713059634539392977306406262459535635177860280597617684338927195076481444975'
                            side: 'no'
                        - provider: sxbet
                          event_id: L19431555
                          market_id: '0x92288450e95f04c6c5c0d10eba0e820a746903410763401bafdce56a32ffb384'
                          market_name: Denver Broncos vs Seattle Seahawks
                          outcomes:
                          - canonical_outcome_key: winner:den
                            label: Denver Broncos
                            outcome_id: outcomeOne
                          - canonical_outcome_key: winner:sea
                            label: Seattle Seahawks
                            outcome_id: outcomeTwo
                        settlement_equivalence: different
                        rule_comparisons:
                        - rule: tie
                          label: 'Tied game or period'
                          comparison: different
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-DEN'
                            value: half_payout
                            description: 'The market''s rules, or its game''s rules, resolve a tied game 50-50.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLGAME-26OCT15SEADEN-DEN'
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-SEA'
                            value: half_payout
                            description: 'The market''s rules, or its game''s rules, resolve a tied game 50-50.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLGAME-26OCT15SEADEN-SEA'
                          - provider: polymarket
                            market_id: '4212356'
                            value: half_payout
                            description: 'The market''s rules, or its game''s rules, resolve a tied game 50-50.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: predict
                            market_id: '1966227'
                            value: half_payout
                            description: 'The market''s rules, or its game''s rules, resolve a tied game 50-50.'
                            evidence_url: 'https://api.predict.fun/v1/markets/1966227'
                          - provider: sxbet
                            market_id: '0x92288450e95f04c6c5c0d10eba0e820a746903410763401bafdce56a32ffb384'
                            value: void
                            description: 'SX Bet''s help center does not say what a tied NFL game pays, but SX Bet voided the moneyline of the 2026-10-11 Giants at Commanders tie (27-27 after overtime, reported outcome 0).'
                            evidence_url: 'https://api.sx.bet/markets/find?marketHashes=0x39ec7ed0520273f139db9e5840917a31c4376306366d69dbb7cc4f21f8f78c18'
                        - rule: overtime
                          label: 'Overtime and extra innings'
                          comparison: equivalent
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-DEN'
                            value: included
                            description: 'Kalshi: "Unless otherwise specified, all overtime periods are included in determining the winner."'
                            evidence_url: 'https://assets.kalshi.com/contract_terms/FOOTBALLGAMEWIN.pdf'
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-SEA'
                            value: included
                            description: 'Kalshi: "Unless otherwise specified, all overtime periods are included in determining the winner."'
                            evidence_url: 'https://assets.kalshi.com/contract_terms/FOOTBALLGAMEWIN.pdf'
                          - provider: polymarket
                            market_id: '4212356'
                            value: included
                            description: 'The market''s rules, or its game''s rules, count overtime.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: predict
                            market_id: '1966227'
                            value: included
                            description: 'The market''s rules, or its game''s rules, count overtime.'
                            evidence_url: 'https://api.predict.fun/v1/markets/1966227'
                          - provider: sxbet
                            market_id: '0x92288450e95f04c6c5c0d10eba0e820a746903410763401bafdce56a32ffb384'
                            value: included
                            description: 'SX Bet: "All bets on the game and all bets on the second half will include overtime scoring."'
                            evidence_url: 'https://help.sx.bet/en/articles/5090593-american-football-betting-rules'
                        - rule: push
                          label: 'Line landing exactly'
                          comparison: equivalent
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-DEN'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLGAME-26OCT15SEADEN-DEN'
                          - provider: kalshi
                            market_id: 'KXNFLGAME-26OCT15SEADEN-SEA'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLGAME-26OCT15SEADEN-SEA'
                          - provider: polymarket
                            market_id: '4212356'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: predict
                            market_id: '1966227'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://api.predict.fun/v1/markets/1966227'
                          - provider: sxbet
                            market_id: '0x92288450e95f04c6c5c0d10eba0e820a746903410763401bafdce56a32ffb384'
                            value: not_applicable
                            description: 'A moneyline has no line to land on exactly.'
                            evidence_url: 'https://help.sx.bet/en/articles/3613418-general-betting-rules'
                      - key: spread|full|spread:den|3.5
                        market_type: spread
                        segment: full
                        display_name: Denver Broncos -3.5
                        metric: spread
                        line: -3.5
                        subject:
                          type: participant
                          key: den
                          name: Denver Broncos
                        outcomes:
                        - key: cover:den
                          label: Denver Broncos covers -3.5
                          type: participant
                          side: cover
                        - key: not_cover:den
                          label: Denver Broncos does not cover -3.5
                          type: participant
                          side: not_cover
                        source_markets:
                        - provider: kalshi
                          event_id: KXNFLSPREAD-26OCT15SEADEN
                          market_id: KXNFLSPREAD-26OCT15SEADEN-DEN4
                          market_name: Denver wins by over 3.5 points?
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: 'Yes'
                            outcome_id: 'yes'
                            side: 'yes'
                          - canonical_outcome_key: not_cover:den
                            label: 'No'
                            outcome_id: 'no'
                            side: 'no'
                        - provider: polymarket
                          event_id: nfl-sea-den-2026-10-16
                          market_id: '5126843'
                          market_name: 'Spread: Broncos (-3.5)'
                          market_slug: nfl-sea-den-2026-10-16-spread-home-3pt5
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: Broncos
                            outcome_id: '66687183497995998313187260270461603445037919098033048212310694618993665413167'
                          - canonical_outcome_key: not_cover:den
                            label: Seahawks
                            outcome_id: '34935678240172142451066439913204728345738080612535299714429050260011568826516'
                        - provider: sxbet
                          event_id: L19431555
                          market_id: '0x4703010fbb76917d799c0df6c2886d52b863d04c1578157f431b9684b13bdaa0'
                          market_name: Denver Broncos -3.5 vs Seattle Seahawks +3.5
                          outcomes:
                          - canonical_outcome_key: cover:den
                            label: Denver Broncos -3.5
                            outcome_id: outcomeOne
                          - canonical_outcome_key: not_cover:den
                            label: Seattle Seahawks +3.5
                            outcome_id: outcomeTwo
                        settlement_equivalence: equivalent
                        rule_comparisons:
                        - rule: tie
                          label: 'Tied game or period'
                          comparison: equivalent
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLSPREAD-26OCT15SEADEN-DEN4'
                            value: not_possible
                            description: 'A spread or total has no tie result of its own: a tied game or period is settled by the line like any other result.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLSPREAD-26OCT15SEADEN-DEN4'
                          - provider: polymarket
                            market_id: '5126843'
                            value: not_possible
                            description: 'A spread or total has no tie result of its own: a tied game or period is settled by the line like any other result.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: sxbet
                            market_id: '0x4703010fbb76917d799c0df6c2886d52b863d04c1578157f431b9684b13bdaa0'
                            value: not_possible
                            description: 'A spread or total has no tie result of its own: a tied game or period is settled by the line like any other result.'
                            evidence_url: 'https://help.sx.bet/en/articles/3613418-general-betting-rules'
                        - rule: overtime
                          label: 'Overtime and extra innings'
                          comparison: equivalent
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLSPREAD-26OCT15SEADEN-DEN4'
                            value: included
                            description: 'Kalshi: "All scoring in overtime periods counts toward the final margin."'
                            evidence_url: 'https://assets.kalshi.com/contract_terms/FOOTBALLSPREAD.pdf'
                          - provider: polymarket
                            market_id: '5126843'
                            value: included
                            description: 'The market''s rules, or its game''s rules, count overtime.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: sxbet
                            market_id: '0x4703010fbb76917d799c0df6c2886d52b863d04c1578157f431b9684b13bdaa0'
                            value: included
                            description: 'SX Bet: "All bets on the game and all bets on the second half will include overtime scoring."'
                            evidence_url: 'https://help.sx.bet/en/articles/5090593-american-football-betting-rules'
                        - rule: push
                          label: 'Line landing exactly'
                          comparison: equivalent
                          source_values:
                          - provider: kalshi
                            market_id: 'KXNFLSPREAD-26OCT15SEADEN-DEN4'
                            value: not_applicable
                            description: 'A half-point line cannot be landed on exactly, so there is nothing to push.'
                            evidence_url: 'https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLSPREAD-26OCT15SEADEN-DEN4'
                          - provider: polymarket
                            market_id: '5126843'
                            value: not_applicable
                            description: 'A half-point line cannot be landed on exactly, so there is nothing to push.'
                            evidence_url: 'https://polymarket.com/event/nfl-sea-den-2026-10-16'
                          - provider: sxbet
                            market_id: '0x4703010fbb76917d799c0df6c2886d52b863d04c1578157f431b9684b13bdaa0'
                            value: not_applicable
                            description: 'A half-point line cannot be landed on exactly, so there is nothing to push.'
                            evidence_url: 'https://help.sx.bet/en/articles/3613418-general-betting-rules'
                    lookups:
                      kalshi:KXNFLSPREAD-26OCT15SEADEN-DEN4:
                        event_ids:
                        - nfl-sea-den-2026-10-15
                    snapshot:
                      observed_at: '2026-10-03T23:11:41.515Z'
        "400":
          description: >
            Invalid query parameters: neither `event_id` nor `source_id` sent
            (`missing_parameter`); a `source_id` that is not `{provider}:{id}`
            with a matching provider, or that names a side rather than a
            market (`kalshi:yes`); a venue's own ID sent as `event_id`,
            including a Polymarket slug that matches no canonical event; an
            empty or comma-joined value; more than 100 unique identifiers; a
            parameter this route does not read (`limit`, `cursor` and the list
            filters belong to `GET /v1/matching-markets/sports`); a control
            parameter sent more than once; or an invalid flag. The message
            quotes the offending value, and `errors` lists every bad
            identifier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the caller's
            paid subscription is in a payment-recovery state. This endpoint is
            available on every plan, so 402 never indicates a tier mismatch
            here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: API key exceeded its rate limit
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: API-key authorization dependency failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Matching markets reader unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/markets:
    get:
      summary: List markets
      description: >
        Returns a paginated list of unified markets from all supported
        prediction market providers. Uses cursor-based pagination with
        default `limit=25`, max `100`.


        Providers are walked in a fixed sequence, so without a filter the
        first pages are all Kalshi. Narrow the traversal with `provider`
        and/or `category`; both are membership filters on the same immutable
        catalog snapshot, both compose, and `pagination.total` always counts
        the filtered set rather than the whole catalog.


        This endpoint reads a stored snapshot rather than calling the venues,
        so the rows are as old as the last ingestion crawl, not as old as the
        request. `snapshot.observed_at` reports that age and is stable across
        a cursor traversal.


        The catalog lists markets that were open when their venue was last
        crawled; closed and settled markets are not in it, and rows carry no
        `status`. A listed market may have closed since that crawl, so read
        its current `status` from `GET /v1/markets/{market_id}`, which asks
        the venue at request time.


        Kalshi's multivariate-event markets (user-built parlays under
        `KXMVECROSSCATEGORY…` tickers) are excluded from the catalog and from
        `pagination.total`; every leg they combine is listed individually. A
        parlay ticker still resolves on `GET /v1/markets/{market_id}`.
      operationId: getMarkets
      x-fern-pagination:
        cursor: $request.cursor
        next_cursor: $response.pagination.next_cursor
        results: $response.data
      security:
        - BearerAuth: []
      x-unkey-permission: markets.read
      parameters:
        - name: limit
          in: query
          required: false
          description: >
            Maximum number of markets to return per page. Range 1–100,
            default 25.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          description: >
            Opaque cursor from a previous response's `pagination.nextCursor`
            in the SDKs (raw JSON: `pagination.next_cursor`). Market cursors
            stay bound to the immutable catalog snapshot that issued them.
            Replaced snapshots normally remain available for up to 8 hours,
            but storage pressure can evict a retained snapshot sooner; retry
            from the first page after a stale-cursor `400`.
          schema:
            type: string
        - name: category
          in: query
          required: false
          description: >
            Canonical top-level category filter. This is PredictorSDK's
            normalized category, not a provider-native tag. Cursors are bound
            to the category filter used to create them.
          schema:
            $ref: "#/components/schemas/MarketCategory"
        - name: provider
          in: query
          required: false
          description: >
            Restrict the page to one provider, matched against each row's own
            `provider` value. Without it, providers are walked in the order
            below and a caller wanting a later one has to paginate through
            every earlier provider's rows first.


            Only the canonical provider IDs are accepted, case-insensitively.
            Any other value returns `400` listing the legal ones — an
            unrecognized filter is never ignored, because a silently dropped
            filter returns a full unfiltered page that looks filtered.


            `pagination.total` counts only the selected provider's rows, and
            cursors are bound to the filter that created them: replay a
            `next_cursor` with the same `provider` value, or start again from
            the first page.


            This is a catalog membership filter, and it is spelled `provider`
            because that is the field it selects on. The same name is the
            override on `GET /v1/markets/{market_id}` and
            `GET /v1/events/{event_id}`, where it names the provider an
            identifier should be resolved against rather than filtering a
            list.
          schema:
            type: string
            enum:
              - kalshi
              - polymarket
              - predict
              - sxbet
              - hyperliquid
              - alpha-arcade
              - prophetx
              - limitless
              - pred
      responses:
        "200":
          description: Paginated list of markets
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketsListResponse"
              examples:
                default:
                  summary: First page of markets
                  value:
                    # Providers are walked in sequence, so the first pages
                    # are all Kalshi, whose own category for each series sets
                    # `category` (Politics and Finance here). `unknown` is a
                    # real value, for a market whose series Kalshi leaves
                    # uncategorized — see GET /v1/categories for the
                    # taxonomy. Read live 2026-10-09; abridged to two rows,
                    # which is why `limit` is larger than `data`.
                    data:
                      - id: "kalshi:AMAZONFTC-29DEC31"
                        provider: kalshi
                        title: "Will a court find that Amazon has illegally maintained a monopoly?"
                        category: politics
                      - id: "kalshi:APPLEUS-29DEC31"
                        provider: kalshi
                        title: "DOJ wins their anti-trust case against Apple?"
                        category: finance
                    # The oldest provider crawl behind this page, not the
                    # time of the request. Kalshi's uncapped catalog is the
                    # slowest to crawl, so on an unfiltered page it is
                    # usually the provider setting this floor.
                    snapshot:
                      observed_at: "2026-10-09T15:22:30.534Z"
                    pagination:
                      limit: 25
                      total: 546912
                      has_more: true
                      next_cursor: eyJzIjoiNDQxMzZmYTMiLCJ0IjoiMjAyNi0xMC0wOVQxNToyMzoyNi41ODM0MThaIiwiaSI6ImthbHNoaTpBUFBMRVVTLTI5REVDMzEifQ
        "400":
          description: Invalid query parameters (e.g. bad limit, malformed/stale cursor, unknown category, unknown provider, or a cursor replayed under different filters)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the caller's
            paid subscription is in a payment-recovery state. This endpoint is
            available on every plan, so 402 never indicates a tier mismatch
            here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: API key exceeded its rate limit
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: API-key authorization dependency failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Markets reader unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/categories:
    get:
      summary: List categories
      description: >
        Returns the canonical top-level categories that can be used to
        filter unified market discovery with `GET /v1/markets?category=...`.
        Categories are PredictorSDK-normalized buckets, not provider-native
        tags. Sports is one category among many; sport/league facets may be
        added later as deeper filters without changing this top-level list.
      operationId: getCategories
      security:
        - BearerAuth: []
      x-unkey-permission: markets.read
      responses:
        "200":
          description: Supported category list
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoriesResponse"
              examples:
                default:
                  summary: Category list
                  value:
                    data:
                      - id: sports
                        name: Sports
                        description: Sports events and markets.
                      - id: politics
                        name: Politics
                        description: Elections, government, and geopolitics markets.
                      - id: crypto
                        name: Crypto
                        description: Cryptocurrency and digital asset markets.
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the caller's
            paid subscription is in a payment-recovery state. This endpoint is
            available on every plan, so 402 never indicates a tier mismatch
            here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: API key exceeded its rate limit
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: API-key authorization dependency failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Categories or API-key authorization dependency is not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/markets/{market_id}:
    get:
      summary: Get market detail
      description: >
        Returns one market from any of the nine supported providers
        (Kalshi, Polymarket, Predict, SX Bet, Hyperliquid, AlphaArcade,
        ProphetX, Limitless, Pred).
        The `market_id` is either the composite form returned by
        `GET /v1/markets` (`{provider}:{native_id}`, e.g.
        `kalshi:KXNBA-27-SAS`) or the
        provider-native identifier. Composite IDs dispatch
        unambiguously by prefix. Native IDs are routed by format
        inference: Kalshi tickers match the all-caps-with-hyphens
        shape (`KX…-…`); SX Bet hashes match `0x` + 64 hex characters.


        **A bare numeric id or kebab-case slug does not name its
        provider.** Polymarket and Predict share both shapes, so when
        `?provider=` is omitted the service probes every candidate and
        answers only if exactly one of them holds that identifier. If
        two do, the identifier names two different real markets and the
        request fails with `409` listing both — it does not pick one.
        Retry with `?provider=` or the composite form.
        Polymarket also resolves a market by its `conditionId`, the id
        Polymarket wallet positions carry (`polymarket:0x…`, or
        `?provider=polymarket`); a bare `0x` + 64 hex id is read as an SX
        Bet hash.
        Hyperliquid integer outcome ids collide
        with Polymarket/Predict numeric ids and are deliberately not
        inferred — route them via the composite form (`hyperliquid:<id>`)
        or `?provider=hyperliquid` (alias `hl`). AlphaArcade market ids
        are ULIDs (26-char Crockford base32, e.g.
        `01K0HQE3CEM2T2RDRWSCJ3V647`); like Hyperliquid they are not
        inferred in v1 — route them via the composite form
        (`alpha-arcade:<ulid>`) or `?provider=alpha-arcade` (alias `aa`).
        ProphetX market ids are `<event_id>:<market_id>` pairs (e.g.
        `1700008812:219` — the market id is a per-sport TYPE id repeated on
        every event of that sport, 219 being Moneyline on every NFL game,
        so the event half is the identity); like Hyperliquid they are not inferred in
        v1 — route them via the composite form (`prophetx:1700008812:219`) or
        `?provider=prophetx` (aliases `prophet-x`, `px`). That pair resolves
        whichever line the venue currently flags favourite, which moves; a
        third part names one line, favourite or not
        (`<event_id>:<market_id>:<line>`, e.g. `1700008812:1700009030:69.5`),
        and a line the venue no longer lists answers `404`. Matched ProphetX
        player props use that form.
        Limitless market ids are kebab-case slugs (e.g.
        `england-vs-sri-lanka-1789383376774`); like Hyperliquid they are
        not inferred in v1 — the shape collides with Polymarket/Predict
        slugs — route them via the composite form
        (`limitless:<slug>`) or `?provider=limitless`.
        Pred market ids are `<parent_market_id>:<child_market_id>` pairs
        of `0x` + 64-hex ids (e.g.
        `pred:0xaa7f…3bf5:0xaa7f…3bf5`); like Hyperliquid they are not
        inferred in v1 — the halves collide textually with SX Bet market
        hashes — route them via the composite form or `?provider=pred`.
        A bare `pred:<parent>` addresses the parent as a group (one
        outcome per child selection, marked from each child's record: its
        declared result once settled, otherwise no price, since no book is
        read); the pair addresses one binary child market, quoted from its
        order book while it trades and marked with its declared result
        once settled.


        **If you already know the provider, always say so.** Every
        listing that hands you an identifier also hands you its
        provider, so the composite form — which `GET /v1/markets`
        returns natively in `data[].id` — or `?provider={row.provider}`
        costs nothing, skips the probe, and cannot 409. It is also
        strictly more available: the probe has to reach both candidates
        to prove there is no collision, so it fails when either is
        having an outage, while a named provider only depends on that
        one.


        Identity fields (`id`, `provider`, `provider_id`, `title`,
        `status` and `outcomes[].name`) are strict-universal: every
        platform's single-market endpoint exposes them natively without
        a second fetch. Close timestamps and parent event ids remain
        omitted (not nullable) — Predict's close time lives on the
        parent category and Polymarket's market record carries no event
        id.


        **What the pricing tier is for, and what it is not.** It reports
        the top of each venue's book, once, at the moment of your
        request. That is enough to see where a market is quoted, to
        compare venues, and to decide where to go and look harder. It is
        NOT an execution feed: there is no depth beyond the best level,
        no streaming, no per-outcome book on the platforms that publish
        only a market-wide mark, and nothing here is reserved for you —
        by the time you act, the level may be gone. Two limits are worth
        knowing before you write a strategy against it. First, `price`
        is a derived display number and a non-null `price` does not
        imply a tradeable one; read `pricing.availability` and prefer
        `bid`/`ask` for anything you intend to act on. Second, quote
        freshness is a property of the venue, not of this API — see
        *Bounding quote freshness* below. Route the actual order through
        the venue's own book.


        The pricing tier adds per-outcome quotes (`price`, `bid`, `ask`
        and `last`, as 0–1 probability numbers — price IS the implied
        probability), a `pricing` envelope (`availability`, `scale`,
        `source`, `as_of`, `as_of_kind`, `observed_at` and `neg_risk`),
        and market-level aggregates (`liquidity_usd`, `volume_24h_usd`,
        `volume_total_usd`, plus Kalshi contract-count mirrors and
        `open_interest`).


        **Where the quotes come from (`pricing.source`).** Kalshi,
        Polymarket and Predict quote from the same record the identity
        fetch returns (`pricing.source=market_record`), and so do the
        group views of Limitless (a group's slug) and Pred (a bare
        `pred:<parent>`), which read no order book and mark each option
        from its own record. Every other market is priced from the
        venue's order book (`pricing.source=orderbook`). ProphetX
        publishes its resting offers on the market record itself, so it
        costs no extra request. SX Bet, Hyperliquid, AlphaArcade, a
        single Limitless market and a Pred child market carry no pricing
        on the market record, so the server makes one bounded second
        fetch to the book — SX Bet's `/orderbook-v3/snapshot`,
        Hyperliquid's merged `l2Book`, AlphaArcade's
        `get-full-orderbook`, Limitless's `/markets/{slug}/orderbook`,
        or Pred's order book for that child. AlphaArcade's is a 4-sided
        YES/NO book: the second side's quotes are derived from the first
        by the cross-side complement, and the catalog midpoint serves as
        the price mark when the book is empty — reported as
        `pricing.availability=indicative`, since a mark that outlives
        its book is not a quote. Limitless and Pred publish the YES
        side only, and the NO side is its complement. Pricing failures
        never fail the lookup: if the book read fails, identity stays
        intact, SX Bet and Hyperliquid (whose only prices are in the
        book) degrade to `pricing.availability=unavailable`, and
        AlphaArcade, Limitless and Pred keep the record's marks, which
        `pricing.availability` then reflects.


        Predict publishes a per-market `spreadThreshold` — the widest
        bid/ask spread it counts as liquidity — and this route honours
        it, so a Predict book outside its own market's threshold reports
        `indicative` rather than lending its midpoint the authority of
        `live`. Aggregates a platform doesn't natively expose are
        explicit `null` (e.g. Kalshi reports volume in contracts, so
        `volume_*_usd` stays null rather than fabricating a USD figure;
        its upstream `liquidity_dollars` field is deprecated and always
        zero, so `liquidity_usd` is null too).


        **Bounding quote freshness.** `pricing.as_of` is the provider's
        own timestamp and does not mean the same thing on every platform
        — on Hyperliquid it moves with the order book, while on Kalshi it
        is a record write measured anywhere from 15 hours to 137 days old
        on markets reporting `status: open` with a live two-sided book.
        `pricing.as_of_kind` names which one you received (`quote` /
        `record_refresh` / `record_static` / `unknown`), so read it before
        applying an age bound to `as_of`; only `quote` tracks the quote
        closely enough to bound at all.
        `pricing.observed_at` is when this server read the quotes, means
        the same thing on every provider, and is therefore the field to
        bound when you need one threshold that behaves identically across
        platforms. It bounds the age of the read, not of the quote: this
        route reads the venue live per request and caches nothing, so on a
        `record_static` provider a fresh `observed_at` beside a day-old
        `as_of` is the honest description of what the venue served, and
        the executable price should come from that venue's own book.


        The `trading_fees` tier reports what the VENUE charges to trade
        this market — its own published fee parameters, normalized to one
        shape across all nine platforms, so cross-venue cost comparison
        stops requiring nine private formulas. Nothing here relates to
        PredictorSDK's subscription pricing. It is always present, and
        `trading_fees.availability` distinguishes published parameters
        from per-account rates that need your own venue credentials
        (SX Bet, Hyperliquid), from a venue that publishes nothing, and
        from a fee of genuinely zero. Only Kalshi costs extra upstream
        hops for it (its parameters live on the parent series plus any
        scheduled per-event override, all TTL-cached and bounded); those
        degrade to `availability: "unavailable"` rather than failing the
        lookup, exactly like the pricing tier. See the
        `MarketDetailTradingFees` schema for the formulas and for what is
        deliberately out of scope.
      operationId: getMarket
      security:
        - BearerAuth: []
      x-unkey-permission: markets.read
      parameters:
        - name: market_id
          in: path
          required: true
          description: >
            Composite (`{provider}:{native_id}`) or provider-native
            market identifier. Examples per provider: Kalshi market
            ticker (`KXNBA-27-SAS`), Polymarket
            numeric id or slug (`540817` or
            `nfl-pit-ne-2026-09-20`), Predict market id
            (`356635`), SX Bet `marketHash` (`0x…64hex`), Hyperliquid
            outcome id (use the composite `hyperliquid:<outcome-id>` or
            `?provider=hyperliquid` — bare integer ids aren't inferred).


            **A bare numeric id or slug is not unique across providers.**
            Polymarket and Predict both use these shapes and their id
            spaces overlap, so sending one without a provider can fail
            with `409` (see that response). Prefer the composite form —
            it is what `GET /v1/markets` returns in `data[].id` — or pass
            `?provider=`.


            **Sports identifiers expire.** Kalshi game tickers and
            Polymarket game slugs are delisted as events settle, and a
            Hyperliquid outcome id lives as long as its question: its
            daily-recurring price outcomes roll over every day, while a
            season-long question (a league winner, say) stays listed
            until it resolves. Take current ids from `GET /v1/markets`
            or `GET /v1/matching-markets/sports` rather than copying one
            out of this reference. Long-dated markets (Kalshi season
            futures, multi-year AlphaArcade questions) and settled
            Polymarket, Predict and SX Bet ids stay resolvable.
          example: kalshi:KXNBA-27-SAS
          schema:
            type: string
            example: kalshi:KXNBA-27-SAS
        - name: provider
          in: query
          required: false
          description: >
            Optional provider override, spelled like the `provider` field
            on `GET /v1/markets` rows and every other response (it was
            `platform` until 2026-10-09; `?platform=` now answers `400`
            naming this parameter). When omitted, inferred from
            the composite prefix or from the native ID format
            (`KX…` → Kalshi, `0x…64hex` → SX Bet — Pred ids share
            that shape and are never inferred; use `pred:…` or
            `?provider=pred`). Numeric IDs and
            kebab-case slugs are shared shape between Polymarket and
            Predict; in that case the service probes both and returns
            `409` rather than guessing if the identifier resolves on
            both. Hyperliquid integer ids
            collide with these numerics and are not inferred — use the
            composite `hyperliquid:<id>` or `?provider=hyperliquid`
            (alias `hl`). AlphaArcade ULIDs and Limitless slugs are not
            inferred either — use the composite form
            (`alpha-arcade:<ulid>`, `limitless:<slug>`) or the matching
            `?provider=` value. Pred pairs (`pred:<parent>:<child>`)
            are likewise composite/`?provider=` only. Passing `provider`
            explicitly skips the probe entirely and is the recommended
            call whenever you know it. When the override
            contradicts a composite prefix (e.g. `kalshi:X` with
            `?provider=polymarket`), the request returns 400.
          schema:
            type: string
            enum:
              - kalshi
              - polymarket
              - predict
              - sxbet
              - hyperliquid
              - alpha-arcade
              - prophetx
              - limitless
              - pred
      responses:
        "200":
          description: Market detail
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketDetailResponse"
              examples:
                kalshi_nba_futures:
                  summary: Kalshi NBA futures market (quotes from the market record)
                  value:
                    id: kalshi:KXNBA-27-SAS
                    provider: kalshi
                    provider_id: KXNBA-27-SAS
                    title: Will San Antonio win the 2027 Pro Basketball Finals?
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "yes"
                        price: 0.235
                        bid: 0.23
                        ask: 0.24
                        last: 0.23
                        bid_size: 21414.84
                        ask_size: 133432.35
                      - name: "No"
                        outcome_id: "no"
                        price: 0.765
                        bid: 0.76
                        ask: 0.77
                        last: 0.77
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: market_record
                      # Kalshi's updated_time is a record write that does not
                      # move while the market is quoted. On this long-dated
                      # futures market that leaves as_of ~27 DAYS behind
                      # observed_at, on a live two-sided book — the gap
                      # scales with how long ago the record was last written,
                      # not with the quote. Bound observed_at, not as_of, on
                      # this provider (#399).
                      as_of: "2026-07-24T16:41:17.536752Z"
                      as_of_kind: record_static
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      # KXNBA is one of the 129 live series on Kalshi's
                      # `quadratic_with_maker_fees` schedule, so the maker
                      # leg is CHARGED here (0.25 × the taker coefficient),
                      # not zero. Most series charge makers nothing; this
                      # one is deliberately an example that doesn't.
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: series_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.07
                        exponent: 1
                        rounding:
                          direction: up
                          increment: 0.0001
                      maker:
                        model: quadratic
                        rate: 0.0175
                        exponent: 1
                        rounding:
                          direction: up
                          increment: 0.0001
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                    volume_24h_contracts: 1049.81
                    volume_total_contracts: 629558.2
                    open_interest: 436690.85
                polymarket_with_aggregates:
                  summary: Polymarket market with USD liquidity/volume aggregates
                  value:
                    id: polymarket:558968
                    provider: polymarket
                    provider_id: "558968"
                    title: Will Egypt win the 2026 FIFA World Cup?
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "30499731947464516579580181356221397335865912996104577000510883912653418218808"
                        price: 0.0025
                        bid: 0.002
                        ask: 0.003
                        last: 0.003
                      - name: "No"
                        outcome_id: "65652065262666433168143828460188123377751865571317729312655170081534213236077"
                        price: 0.9975
                        bid: null
                        ask: null
                        last: null
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: market_record
                      as_of: "2026-08-21T03:38:12.539286Z"
                      as_of_kind: record_refresh
                      observed_at: "2026-08-21T03:41:07.000Z"
                      neg_risk: true
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.05
                        exponent: 1
                        rounding:
                          direction: nearest
                          increment: 0.00001
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: 8112916.41585
                    volume_24h_usd: 3902738.7224900015
                    volume_total_usd: 40230495.82577211
                predict_no_upstream_timestamp:
                  summary: Predict market (record carries no quote timestamp)
                  value:
                    id: predict:1515533
                    provider: predict
                    provider_id: "1515533"
                    title: Baltimore Orioles vs. St. Louis Cardinals
                    status: open
                    outcomes:
                      - name: BAL
                        outcome_id: "61151258497776987420696110042838742444279082295354607614121723394535086670261"
                        price: 0.47
                        bid: 0.46
                        ask: 0.48
                        last: null
                        bid_size: 200
                        ask_size: 240
                      - name: STL
                        outcome_id: "113497955451003041067705013388451551716108870115723523462720366028680396761417"
                        price: 0.53
                        bid: 0.52
                        ask: 0.54
                        last: null
                        bid_size: 240
                        ask_size: 200
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: market_record
                      # Predict's market record carries no quote timestamp of
                      # any kind, so as_of is null and as_of_kind is unknown.
                      # observed_at is the ONLY freshness signal available on
                      # this provider — bound that, and take executable price
                      # from Predict's own book.
                      as_of: null
                      as_of_kind: unknown
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: min_price
                        rate: 0.02
                        exponent: null
                        rounding: null
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                predict_indicative_wide_book:
                  summary: >-
                    Predict market quoted wider than the venue's own
                    spreadThreshold — priced, but availability=indicative
                  value:
                    id: predict:22273
                    provider: predict
                    provider_id: "22273"
                    title: Eduardo Bolsonaro
                    status: open
                    outcomes:
                      # 0.001 / 0.142 is a 14.1c book on a market whose
                      # venue-declared threshold is 6c. The 0.0715 midpoint
                      # is arithmetic, not a level anyone is offering: the
                      # only real prices here are the bid and the ask, 14c
                      # apart. Diffing this 0.0715 against another venue's
                      # live quote manufactures an edge that does not exist.
                      - name: "Yes"
                        outcome_id: "98754482311544589417576294196662399275012043631109979838557649892628773795146"
                        price: 0.0715
                        bid: 0.001
                        ask: 0.142
                        last: null
                        bid_size: 2424.437
                        ask_size: 32
                      - name: "No"
                        outcome_id: "75749100448803321830359124945705972314055390590238000296728875641602754856566"
                        price: 0.9285
                        bid: 0.858
                        ask: 0.999
                        last: null
                        bid_size: 32
                        ask_size: 2424.437
                    pricing:
                      # indicative, not live: both sides are quoted, so this
                      # is not `no_quotes` and not `partial` — but Predict
                      # publishes spreadThreshold 0.06 for this market and
                      # both books are 0.141 wide, so the venue itself does
                      # not count either as liquidity. Measured live
                      # 2026-08-25: this state covers 56% of two-sided
                      # Predict outcomes across a 6,122-market sample.
                      availability: indicative
                      scale: probability_0_1
                      source: market_record
                      as_of: null
                      as_of_kind: unknown
                      observed_at: "2026-08-21T03:41:07.000Z"
                      neg_risk: true
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: min_price
                        rate: 0.02
                        exponent: null
                        rounding: null
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                sxbet_orderbook_sourced:
                  summary: SX Bet market (quotes via the bounded order-book fetch)
                  # Read from production 2026-10-03 (NFL moneyline). Each bid
                  # is that side's best maker quote; each ask is 1 minus the
                  # other side's bid.
                  value:
                    id: "sxbet:0xf4d50ecf868b089c11ac428be04faefb336112cf318df0d85733246bdb2634d9"
                    provider: sxbet
                    provider_id: "0xf4d50ecf868b089c11ac428be04faefb336112cf318df0d85733246bdb2634d9"
                    title: Washington Commanders vs Indianapolis Colts
                    status: open
                    outcomes:
                      - name: Washington Commanders
                        outcome_id: outcomeOne
                        price: 0.34
                        bid: 0.3375
                        ask: 0.3425
                        last: null
                      - name: Indianapolis Colts
                        outcome_id: outcomeTwo
                        price: 0.66
                        bid: 0.6575
                        ask: 0.6625
                        last: null
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      # Always null on SX Bet: the order book publishes a
                      # monotonic `version` and no wall-clock stamp, so
                      # observed_at is the field to bound.
                      as_of: null
                      as_of_kind: unknown
                      observed_at: "2026-10-03T16:50:31.863Z"
                    trading_fees:
                      availability: account_specific
                      observed_at: null
                      source: venue_schedule
                      charge_basis: settlement_profit
                      taker: null
                      maker: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                sxbet_pricing_degraded:
                  summary: SX Bet with the order-book fetch timed out — identity intact
                  # The same market as sxbet_orderbook_sourced, so the two
                  # examples differ only in the pricing tier.
                  value:
                    id: "sxbet:0xf4d50ecf868b089c11ac428be04faefb336112cf318df0d85733246bdb2634d9"
                    provider: sxbet
                    provider_id: "0xf4d50ecf868b089c11ac428be04faefb336112cf318df0d85733246bdb2634d9"
                    title: Washington Commanders vs Indianapolis Colts
                    status: open
                    outcomes:
                      - name: Washington Commanders
                        outcome_id: outcomeOne
                        price: null
                        bid: null
                        ask: null
                        last: null
                      - name: Indianapolis Colts
                        outcome_id: outcomeTwo
                        price: null
                        bid: null
                        ask: null
                        last: null
                    pricing:
                      availability: unavailable
                      scale: probability_0_1
                      source: orderbook
                      as_of: null
                      as_of_kind: unknown
                      # Null because the pricing fetch itself failed: nothing
                      # was observed, so there is no observation to stamp.
                      observed_at: null
                    trading_fees:
                      availability: account_specific
                      observed_at: null
                      source: venue_schedule
                      charge_basis: settlement_profit
                      taker: null
                      maker: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                hyperliquid_orderbook_sourced:
                  summary: >-
                    Hyperliquid outcome market (quotes via merged l2Book): one
                    team in the season-long 2026/27 Premier League winner
                    question, which stays listed until it resolves in June
                    2027.
                  # A daily-recurring outcome (Hyperliquid's crypto price
                  # contracts) rolls over every day, so an example must come
                  # from a season-long question like this one. See AGENTS.md ->
                  # Published Example Identifiers.
                  value:
                    id: hyperliquid:1473
                    provider: hyperliquid
                    provider_id: "1473"
                    title: "2026/2027 English Premier League winner — Arsenal"
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "#14730"
                        price: 0.61044
                        bid: 0.61041
                        ask: 0.61047
                        last: null
                        bid_size: 384
                        ask_size: 423
                      - name: "No"
                        outcome_id: "#14731"
                        price: 0.38956
                        bid: 0.38953
                        ask: 0.38959
                        last: null
                        bid_size: 423
                        ask_size: 384
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      as_of: "2026-10-05T03:50:06.697Z"
                      as_of_kind: quote
                      observed_at: "2026-10-05T03:50:06.112Z"
                    trading_fees:
                      availability: account_specific
                      observed_at: null
                      source: venue_schedule
                      charge_basis: close_or_settlement
                      taker:
                        model: notional
                        rate: null
                        exponent: null
                        rounding: null
                      maker:
                        model: notional
                        rate: null
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                alpha_arcade_orderbook_sourced:
                  summary: AlphaArcade binary market (quotes via 4-sided book)
                  value:
                    id: alpha-arcade:01K0HQE3CEM2T2RDRWSCJ3V647
                    provider: alpha-arcade
                    provider_id: 01K0HQE3CEM2T2RDRWSCJ3V647
                    title: "Presidential Election Winner 2028: JD Vance"
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "16040015440196279900485035793550429453516625694844857319147506590755961451627"
                        price: 0.2325
                        bid: 0.2275
                        ask: 0.2375
                        last: null
                      - name: "No"
                        outcome_id: "94476829201604408463453426454480212459887267917122244941405244686637914508323"
                        price: 0.7675
                        bid: 0.7625
                        ask: 0.7725
                        last: null
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      # Neither the catalog record nor get-full-orderbook
                      # carries a wall clock, so as_of is null and unknown —
                      # observed_at is the only freshness signal here.
                      as_of: null
                      as_of_kind: unknown
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.07
                        exponent: 1
                        rounding:
                          direction: up
                          increment: 0.000001
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                prophetx_orderbook_sourced:
                  summary: ProphetX NFL moneyline (quotes via resting levels)
                  value:
                    id: prophetx:1700008812:219
                    provider: prophetx
                    provider_id: "1700008812:219"
                    title: "Buffalo Bills at Los Angeles Rams: Moneyline"
                    status: open
                    outcomes:
                      - name: Los Angeles Rams
                        outcome_id: "4"
                        price: 0.616808
                        bid: 0.612403
                        ask: 0.621212
                        last: null
                      - name: Buffalo Bills
                        outcome_id: "5"
                        price: 0.383193
                        bid: 0.378788
                        ask: 0.387597
                        last: null
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      # Per-side stamps are the resting orders' post times
                      # (nanosecond updated_at), oldest first. Measured live
                      # 2026-10-09 on Bills at Rams (event 1700008812).
                      as_of: "2026-10-09T15:24:53.309Z"
                      as_of_kind: quote
                      observed_at: "2026-10-09T15:47:14.339Z"
                    trading_fees:
                      availability: unavailable
                      observed_at: null
                      source: venue_schedule
                      charge_basis: fill
                      taker: null
                      maker: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                limitless_orderbook_sourced:
                  summary: Limitless cricket moneyline (quotes via the YES-side book)
                  value:
                    id: limitless:england-vs-sri-lanka-1790164801377
                    provider: limitless
                    provider_id: england-vs-sri-lanka-1790164801377
                    title: England vs Sri Lanka
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "yes"
                        price: 0.875
                        bid: 0.855
                        ask: 0.895
                        last: null
                      - name: "No"
                        outcome_id: "no"
                        price: 0.125
                        bid: 0.105
                        ask: 0.145
                        last: null
                    pricing:
                      # The book is YES-side only; the NO quotes are the
                      # venue-documented complement (bid = 1-ask,
                      # ask = 1-bid), so both outcomes read off one
                      # two-sided book. Measured live 2026-09-23.
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      as_of: "2026-09-23T12:30:01.930Z"
                      as_of_kind: record_refresh
                      observed_at: "2026-09-23T14:50:00.000Z"
                    trading_fees:
                      # The taker curve is published as table rows with no
                      # closed form, so the taker leg stays null and the
                      # descriptor is partial; makers are explicitly free.
                      availability: partial
                      observed_at: null
                      source: venue_schedule
                      charge_basis: fill
                      taker: null
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: 0
                pred_orderbook_sourced:
                  summary: Pred NFL moneyline child (quotes via the child order book)
                  value:
                    # Single-child parents repeat the parent id as the child
                    # id; the pair form is still required — a bare child id
                    # cannot resolve (the orderbook path needs the parent).
                    id: pred:0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5:0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5
                    provider: pred
                    provider_id: 0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5:0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5
                    title: "Moneyline - 1x2: Green Bay Packers"
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "yes"
                        price: 0.72
                        bid: 0.70
                        ask: 0.74
                        last: null
                      - name: "No"
                        outcome_id: "no"
                        price: 0.28
                        bid: 0.26
                        ask: 0.30
                        last: null
                    pricing:
                      # Book prices are cents (70 = $0.70); the NO side is
                      # the complement (bid = 1-ask, ask = 1-bid). The book
                      # snapshot time tracks the book, so as_of_kind is
                      # quote. Measured live 2026-09-23 on Falcons at
                      # Packers (Sept 25).
                      availability: live
                      scale: probability_0_1
                      source: orderbook
                      as_of: "2026-09-23T13:33:14.866Z"
                      as_of_kind: quote
                      observed_at: "2026-09-23T13:35:00.000Z"
                    trading_fees:
                      # Pred's published schedule (fees page + orders
                      # reference, read 2026-09-24): takers only, fee =
                      # 0.025 × shares × price × (1 − price) — the
                      # quadratic model at exponent 1 — rounded DOWN to
                      # 1e-6, with maker 0% asserted. fee_rate_bps 250
                      # is on every active parent measured 2026-09-24.
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.025
                        exponent: 1
                        rounding:
                          direction: down
                          increment: 0.000001
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                alpha_arcade_indicative_empty_book:
                  summary: >-
                    AlphaArcade market whose book is empty — the catalog
                    midpoint is the only price, so availability=indicative
                  value:
                    id: alpha-arcade:01K0HQEM34CD3PR78XY6W1RZX8
                    provider: alpha-arcade
                    provider_id: 01K0HQEM34CD3PR78XY6W1RZX8
                    title: "Presidential Election Winner 2028: Gavin Newsom"
                    status: open
                    outcomes:
                      # Nothing is resting on either side of this option's
                      # book, so bid and ask are null and price falls back to
                      # the catalog midpoint. That midpoint outlives the book
                      # it came from, which is exactly why it cannot be read
                      # as a quote: a market with no orders in it is not
                      # offering a leading 2028 candidate at 0.0001%.
                      - name: "Yes"
                        outcome_id: "98250445447699368679516529207365255018790721464590833209064266254238063117329"
                        price: 0.000001
                        bid: null
                        ask: null
                        last: null
                      - name: "No"
                        outcome_id: "18823838997443878656879952590502524526556504037944392973476854588563571859850"
                        price: 0.999999
                        bid: null
                        ask: null
                        last: null
                    pricing:
                      # The distinction `indicative` was added for. Sibling
                      # AlphaArcade markets that have never traded carry no
                      # midpoint at all and report `no_quotes` (18 of the 19
                      # matched markets on 2026-08-25); this one carries a
                      # stale one. Same upstream fact — an empty book — so the
                      # two must not answer differently about whether a
                      # tradeable quote exists.
                      availability: indicative
                      scale: probability_0_1
                      source: orderbook
                      as_of: null
                      as_of_kind: unknown
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.07
                        exponent: 1
                        rounding:
                          direction: up
                          increment: 0.000001
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                kalshi_mlb_scheduled_fee_change:
                  summary: >-
                    Kalshi MLB total — half-rate pre-game with the published
                    restore to full rate at first pitch
                  value:
                    id: kalshi:KXMLBTOTAL-26AUG212015LAATEX-9
                    provider: kalshi
                    provider_id: KXMLBTOTAL-26AUG212015LAATEX-9
                    title: Over 8.5 runs scored
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "yes"
                        price: 0.415
                        bid: 0.41
                        ask: 0.42
                        last: 0.42
                      - name: "No"
                        outcome_id: "no"
                        price: 0.585
                        bid: 0.58
                        ask: 0.59
                        last: 0.58
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: market_record
                      as_of: "2026-08-21T03:12:44.108271Z"
                      as_of_kind: record_static
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: series_record
                      charge_basis: fill
                      taker:
                        model: quadratic
                        rate: 0.035
                        exponent: 1
                        rounding:
                          direction: up
                          increment: 0.0001
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                      scheduled_change:
                        effective_at: "2026-08-22T00:15:00Z"
                        taker:
                          model: quadratic
                          rate: 0.07
                          exponent: 1
                          rounding:
                            direction: up
                            increment: 0.0001
                        maker:
                          model: none
                          rate: 0
                          exponent: null
                          rounding: null
                    liquidity_usd: null
                    volume_24h_usd: null
                    volume_total_usd: null
                    volume_24h_contracts: 41210.0
                    volume_total_contracts: 118904.0
                    open_interest: 62550.0
                polymarket_fee_free_market:
                  summary: >-
                    Polymarket geopolitics market — fees are asserted ZERO,
                    which is a value, not a gap
                  value:
                    id: polymarket:567621
                    provider: polymarket
                    provider_id: "567621"
                    title: Will China invade Taiwan before 2027?
                    status: open
                    outcomes:
                      - name: "Yes"
                        outcome_id: "21742633143463906290569050155826241533067272736897614950488156847949938836455"
                        price: 0.055
                        bid: 0.054
                        ask: 0.056
                        last: 0.055
                      - name: "No"
                        outcome_id: "48331043336612883890938759509493159234755048973500640148014422747788308965732"
                        price: 0.945
                        bid: null
                        ask: null
                        last: null
                    pricing:
                      availability: live
                      scale: probability_0_1
                      source: market_record
                      as_of: "2026-08-21T03:37:11.204Z"
                      as_of_kind: record_refresh
                      observed_at: "2026-08-21T03:41:07.000Z"
                    trading_fees:
                      availability: published
                      observed_at: "2026-08-21T03:41:07.000Z"
                      source: market_record
                      charge_basis: fill
                      taker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                      maker:
                        model: none
                        rate: 0
                        exponent: null
                        rounding: null
                    liquidity_usd: 412881.02
                    volume_24h_usd: 18422.51
                    volume_total_usd: 9184203.77
        "400":
          description: >
            `market_id` missing or invalid, `provider` query value
            unknown, or composite-prefix conflict with the `provider`
            override.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the
            caller's paid subscription is in a payment-recovery
            state. This endpoint is available on every plan, so 402
            never indicates a tier mismatch here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Resource not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: >
            `market_id` is ambiguous: it resolves to a different real
            market on more than one provider, so no single market can
            honestly be returned. Retry with `?provider=` naming one of
            the `candidates`, or use the composite `{provider}:{id}`
            form. Only bare identifiers whose shape is shared between
            Polymarket and Predict (numeric ids, kebab-case slugs) can
            produce this; composite ids, explicit `?provider=`, Kalshi
            tickers and SX Bet hashes never do.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AmbiguousIdentifierError"
        "429":
          description: API key exceeded its rate limit, or upstream rate-limited the lookup.
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Upstream provider failed for an unclassified reason.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: >
            The resolved provider isn't configured on this server
            (e.g. `?provider=predict` with no PREDICT_API_KEY
            available at boot).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/crypto-prices/binance:
    get:
      summary: Get Binance crypto prices
      description: >
        Returns per-second price data for a Binance trading pair in the
        paginated envelope every list route uses: `data` holds the prices and
        `pagination` the page. When called without a time range, returns the
        latest price (one item, `has_more: false`). With `start_time` and/or
        `end_time`, returns historical per-second prices newest first: each
        page is the `limit` newest seconds at or before `end_time` (or the
        current second), never older than `start_time`. A range longer than
        `limit` continues through `pagination.next_cursor`, which you send
        back as `cursor`, and walking every page returns every second of the
        range exactly once. `pagination.total` is always `null`: Binance does
        not count a range and this route does not walk one to find out.
        Unknown or invalid symbols return `200` with an empty `data` array
        and `has_more: false`. The envelope replaced
        `{prices, pagination_key, total}` on 2026-10-09; the old request
        parameter `pagination_key` now answers `400` naming `cursor`.
      operationId: getBinanceCryptoPrices
      x-fern-pagination:
        cursor: $request.cursor
        next_cursor: $response.pagination.next_cursor
        results: $response.data
      security:
        - BearerAuth: []
      x-unkey-permission: crypto.prices.read
      parameters:
        - name: currency
          in: query
          required: true
          description: >
            Binance trading pair (e.g. `btcusdt`, `ethusdt`, `solusdt`).
            Must contain only alphanumeric characters (no hyphens, underscores,
            or other separators). Uppercase is accepted and automatically
            lowercased (e.g. `BTCUSDT` → `btcusdt`). Must be a valid Binance
            symbol; unknown symbols return `200` with an empty `data` array.
          schema:
            type: string
            example: btcusdt
        - name: start_time
          in: query
          required: false
          description: >
            Start of the time range as a Unix timestamp in milliseconds
            (inclusive). Negative values are clamped to 0.
          schema:
            type: integer
            format: int64
        - name: end_time
          in: query
          required: false
          description: >
            End of the time range as a Unix timestamp in milliseconds
            (inclusive). Negative values are clamped to 0.
          schema:
            type: integer
            format: int64
        - name: limit
          in: query
          required: false
          description: >
            Maximum number of prices per page, 1 to 100; a larger value is a
            `400` (it was silently clamped to 100 until 2026-10-09).
            Defaults to 100 when a time range is present. Without a time
            range, this parameter is still validated but otherwise ignored:
            the endpoint always returns the single latest price, and
            `pagination.limit` reports `1`.
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: >
            The `pagination.next_cursor` of a previous response, sent
            unchanged, to fetch the next (older) page of results. It carries
            the `start_time` of the request that issued it, so the next page
            stays inside the range whether or not you send `start_time`
            again; sending a different `start_time` with it is a `400`.
            `end_time` is ignored when a cursor is sent. It was named
            `pagination_key` until 2026-10-09; that name now answers `400`
            naming this one.
          schema:
            type: string
      responses:
        "200":
          description: Crypto price data
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CryptoPricesResponse"
              examples:
                default:
                  summary: A page of a range, with more to fetch
                  value:
                    data:
                      - symbol: btcusdt
                        value: 71392.81
                        timestamp: 1775680556000
                      - symbol: btcusdt
                        value: 71392.82
                        timestamp: 1775680555000
                      - symbol: btcusdt
                        value: 71388.51
                        timestamp: 1775680554000
                    pagination:
                      limit: 3
                      total: null
                      has_more: true
                      next_cursor: eyJpZCI6IlBSSUNFI2J0Y3VzZHQiLCJ0aW1lc3RhbXAiOjE3NzU2ODA1NTQwMDB9
                latest_price:
                  summary: No time range, so the single latest price
                  value:
                    data:
                      - symbol: btcusdt
                        value: 71392.81
                        timestamp: 1775680556000
                    pagination:
                      limit: 1
                      total: null
                      has_more: false
                unknown_symbol:
                  summary: An unknown symbol is an empty page, not an error
                  value:
                    data: []
                    pagination:
                      limit: 1
                      total: null
                      has_more: false
        "400":
          description: Invalid or missing query parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            The caller's paid subscription is in a payment-recovery state, or
            (for the Free tier) the monthly request allowance is exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded (API key or upstream)
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Bad gateway
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Crypto prices service not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/polymarket/wallet:
    get:
      summary: Get Polymarket wallet profile
      description: >
        Returns the public profile (image and display name) for a Polymarket
        wallet. Accepts either a wallet `address` (proxy or signer EOA) or a
        Polymarket `username`. Exactly one of the two must be supplied —
        passing both returns `400`.


        When `address` is the underlying signer EOA, the endpoint resolves
        it to the deterministic proxy address and returns the proxy's
        profile, with `signer` echoing the input.


        When `username` is supplied, the endpoint resolves it to the
        wallet via Polymarket's profile search. Match is case-insensitive
        and exact: a query of `Theo` resolves the user literally named
        `theo` but does not resolve `theo46` or `Theo47`. A leading `@` is
        accepted (and stripped) as a convenience for callers used to
        Twitter-style handles. `signer` is always `null` on the username
        path. Profiles that don't exist (or only match fuzzily) return
        `404`.
      operationId: getPolymarketWallet
      security:
        - BearerAuth: []
      x-unkey-permission: polymarket.wallet.read
      parameters:
        - name: address
          in: query
          required: false
          description: >
            Wallet address to look up. May be a Polymarket proxy address or
            the underlying signer EOA — the endpoint resolves either form.
            Must match `^0x[a-fA-F0-9]{40}$`. Mixed-case input is accepted
            and lowercased in the response. Mutually exclusive with
            `username`; exactly one of the two is required.
          schema:
            type: string
            pattern: ^0x[a-fA-F0-9]{40}$
            example: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
        - name: username
          in: query
          required: false
          description: >
            Polymarket display name to look up. Match is case-insensitive
            and exact against the user's stored `name` (so `Car`, `car`,
            and `CAR` all resolve, but `Theo` does not match `Theo47`). A
            leading `@` is accepted and stripped before the lookup.
            Mutually exclusive with `address`; exactly one of the two is
            required. Example: `Car`.
          schema:
            type: string
            minLength: 1
      responses:
        "200":
          description: Public profile for the wallet
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolymarketWalletResponse"
              examples:
                proxy_input:
                  value:
                    address: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
                    signer: null
                    profile_image: "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-501613.png"
                    display_name: "Car"
                eoa_input:
                  value:
                    address: "0x0d56fead54dd28cb7a903e7c6f2aeed9a26d6910"
                    signer: "0x00002569317dc5588b59bae5f63f5f8945912e58"
                    profile_image: null
                    display_name: "Danalvarez79 - 6558"
                username_input:
                  value:
                    address: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
                    signer: null
                    profile_image: "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-501613.png"
                    display_name: "Car"
                empty_profile:
                  value:
                    address: "0x0000000000000000000000000000000000000000"
                    signer: null
                    profile_image: null
                    display_name: "Whirlwind-Catalogue"
        "400":
          description: >
            Missing parameters (neither `address` nor `username` supplied),
            conflicting parameters (both supplied), invalid `address`
            format, or empty `username` after stripping a leading `@`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            The caller's paid subscription is in a payment-recovery state, or
            (for the Free tier) the monthly request allowance is exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Resource not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded (API key or upstream)
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Bad gateway
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Polymarket wallet service not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/polymarket/wallet/positions:
    get:
      summary: List Polymarket wallet positions
      description: >
        Returns the current Polymarket positions for a wallet. The upstream
        source is Polymarket Data API v2; this public route keeps its existing
        response shape. Accepts either a wallet `address` (proxy address
        only — see note below)
        or a Polymarket `username`. Exactly one of the two must be
        supplied — passing both returns `400`.


        v1 surfaces a minimal field set so the endpoint scaffolding can
        be verified end-to-end: `condition_id` (which market),
        `outcome` (which side), and `shares` (how much). Title/slug,
        avg/current price, PnL (`cash_pnl`, `realized_pnl`),
        `redeemable`/`mergeable` flags, and event metadata will be added
        in follow-ups.


        `pagination.total` is always `null` because the upstream Data API
        does not return a total count and this route does not walk the pages
        to compute one (it published `0` until 2026-10-09, which read as an
        empty wallet beside a page of positions); `0` only ever means an
        empty result on the routes that count. Rely on `has_more` +
        `next_cursor` to paginate.


        **EOA inputs are not auto-resolved on this endpoint.** Unlike
        `/v1/polymarket/wallet`, this endpoint does not perform the
        EOA→proxy CREATE2 resolution. Callers with a signer EOA should
        call `/v1/polymarket/wallet` first to resolve the proxy, then
        pass the returned `address`. Passing an EOA directly will return
        an empty `data` array.
      operationId: listPolymarketWalletPositions
      x-fern-pagination:
        cursor: $request.cursor
        next_cursor: $response.pagination.next_cursor
        results: $response.data
      security:
        - BearerAuth: []
      x-unkey-permission: polymarket.positions.read
      parameters:
        - name: address
          in: query
          required: false
          description: >
            Polymarket proxy wallet address. Must match
            `^0x[a-fA-F0-9]{40}$`. Mixed-case input is accepted and
            lowercased in the response. Mutually exclusive with
            `username`; exactly one of the two is required.
          schema:
            type: string
            pattern: ^0x[a-fA-F0-9]{40}$
            example: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
        - name: username
          in: query
          required: false
          description: >
            Polymarket display name to resolve to a proxy wallet. Match
            is case-insensitive and exact against the user's stored
            `name`. A leading `@` is accepted and stripped. Mutually
            exclusive with `address`.
          schema:
            type: string
            minLength: 1
        - name: limit
          in: query
          required: false
          description: >
            Number of items requested for the first page. Defaults to 50.
            Once a cursor is supplied, the cursor's page size is used.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          required: false
          description: >
            Opaque cursor from a previous response's `pagination.next_cursor`.
            Bound to the resolved wallet address — replaying a cursor
            against a different wallet returns `400`. Cursors issued by the
            previous offset-based implementation are rejected; restart from
            the first page. A cursor cannot carry a page size above 100.
          schema:
            type: string
      responses:
        "200":
          description: Wallet positions
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PolymarketPositionsResponse"
              examples:
                with_positions:
                  # The two rows show Yes/No disambiguation on the same
                  # condition_id. has_more is exact and the cursor is opaque;
                  # the token below illustrates the public wrapper shape.
                  value:
                    address: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
                    data:
                      - condition_id: "0x7ad403c3508f8e3912940fd1a913f227591145ca0614074208e0b962d5fcc422"
                        outcome: "Yes"
                        shares: 300000
                      - condition_id: "0x7ad403c3508f8e3912940fd1a913f227591145ca0614074208e0b962d5fcc422"
                        outcome: "No"
                        shares: 300000
                    pagination:
                      limit: 2
                      total: null
                      has_more: true
                      next_cursor: "eyJzIjoiYjU3MWNkYTAiLCJ1IjoibmF0aXZlLW5leHQifQ"
                empty:
                  # v2 rejects protocol contract addresses — the zero address
                  # that v1 served as 200 [] now answers 400 upstream, so the
                  # example uses an unused ordinary wallet. Emptiness is a
                  # property of the wallet, not of the address.
                  value:
                    address: "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678"
                    data: []
                    pagination:
                      limit: 50
                      total: null
                      has_more: false
                      # next_cursor omitted — server response uses omitempty
                      # so the field is absent (not "") when has_more is false.
        "400":
          description: >
            Missing or conflicting parameters, invalid address format,
            invalid limit, invalid/scope-mismatched cursor, or a request
            rejected by Polymarket (including protocol contract addresses
            such as the zero address).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            The caller's paid subscription is in a payment-recovery
            state, or (for the Free tier) the monthly request allowance
            is exhausted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Username did not resolve to a wallet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded (API key or upstream)
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Bad gateway
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Polymarket positions service not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/events/{event_id}:
    get:
      summary: Get event with nested markets
      description: >
        Returns a single event and the markets nested under it on the
        identified provider. The `event_id` is the provider's native
        identifier — a Kalshi `event_ticker`, a Polymarket event slug,
        an SX Bet `eventId`, a Predict market identifier, a
        Hyperliquid question/outcome integer id, an AlphaArcade
        market ULID, or a Pred parent market id. The `provider` is
        inferred from the ID format when unambiguous (`KX…` → Kalshi,
        `L\d+` → SX Bet). The composite form returned by
        `GET /v1/markets` (`{provider}:{native_id}`, e.g.
        `predict:1607914`) also dispatches unambiguously by prefix.


        **A bare numeric id or kebab-case slug does not name its
        provider.** Polymarket and Predict share both shapes, so when
        `?provider=` is omitted the service probes every candidate and
        answers only if exactly one of them holds that identifier. If
        two do, the identifier names two different real events and the
        request fails with `409` listing both — it does not pick one.
        Retry with `?provider=` or the composite form. Hyperliquid
        integer ids also collide with those numerics and require
        `?provider=hyperliquid` (alias `hl`). AlphaArcade ULIDs are not
        inferred in v1 either — require `?provider=alpha-arcade` (alias
        `aa`). ProphetX event ids are bare positive integers that collide
        with Polymarket/Predict numerics — require `?provider=prophetx`
        (aliases `prophet-x`, `px`) or the composite form
        (`prophetx:1700008812`). Limitless event ids are kebab-case slugs that
        collide with Polymarket/Predict slugs — require
        `?provider=limitless` or the composite form
        (`limitless:england-vs-sri-lanka-1789383376774`). Pred event ids
        are parent market ids (`0x` + 64 hex) that collide textually
        with SX Bet market hashes — require `?provider=pred` or the
        composite form (`pred:0x…`). An AlphaArcade multi-choice market resolves to an event
        whose nested markets are its options; a binary market (or a
        single option id) resolves to a single-market event.


        **If you already know the provider, always say so.** Every
        listing that hands you an identifier also hands you its
        provider, so `?provider={row.provider}` (or the composite form)
        costs nothing, skips the probe, and cannot 409. It is also
        strictly more available: the probe has to reach both candidates
        to prove there is no collision, so it fails when either is
        having an outage, while a named provider only depends on that
        one.


        The response is deliberately minimal: each market is returned
        with its provider-native `market_id` and a human-readable
        `title`. Send it to `GET /v1/markets/{market_id}` with this
        response's `provider` as `?provider=` (or as the composite
        `{provider}:{market_id}`). Pricing, volume, status, and
        timestamps are not in this response yet — they will be added as
        additive fields to `EventMarket`. The endpoint mirrors the
        `/v1/markets` rollout pattern (titles first, fields later).


        **Kalshi sibling fanout.** A single Kalshi sports game lives
        across multiple event tickers that share a game suffix — e.g.
        `KXMLBGAME-26OCT152000MILLAD` holds the moneyline,
        `KXMLBF5TOTAL-26OCT152000MILLAD` holds the first-five-innings
        totals, and so on.
        When the supplied event_ticker belongs to a sport in the
        sibling registry (MLB, NBA, NFL, NHL, WNBA today), this
        endpoint fans out across known sibling series in parallel and
        merges their markets into one response. Siblings that don't
        exist for a particular game silently drop. Siblings that
        error are reported under `fanout.siblings_missing`; the
        primary event still returns 200 in that case. Only the
        primary fetch failing produces a 4xx/5xx — partial fanouts
        never fail the request.


        **Polymarket** events already nest the moneyline plus all
        spread/totals/game-level prop markets under a single event
        slug, so no fanout is performed. **SX Bet** fixtures
        similarly bundle game lines per `eventId`, but only while the
        fixture trades: SX Bet lists a fixture's markets through its
        active-markets route, which drops the fixture once it stops
        trading, and has no keyless route that lists them by fixture
        id afterwards. The `L…` id of a game that has ended answers
        `404`, although a matching row keeps carrying it through the
        day after the game; the same row's `market_id` still resolves on
        `GET /v1/markets/{market_id}` (status `settled`). **Predict**
        currently treats `event_id` as a market identifier and wraps
        the single market as a 1-element event response, since the
        upstream `event` concept on Predict is closer to a category
        than to a multi-market container. **Hyperliquid** maps a
        question id to its named outcome markets, or wraps a standalone
        outcome id as a single-market event.
      operationId: getEvent
      security:
        - BearerAuth: []
      x-unkey-permission: events.read
      parameters:
        - name: event_id
          in: path
          required: true
          description: >
            Provider-native event identifier. Examples per provider:
            Kalshi event ticker (`KXNBAGAME-26OCT20OKCSAS`),
            Polymarket event slug (`nfl-pit-ne-2026-09-20`), SX Bet
            event id (`L19766755`), Predict market id (`1607914`),
            Hyperliquid question or outcome integer id (requires
            `?provider=hyperliquid` since integer ids aren't inferred),
            Pred parent market id (`0x…64hex`, requires `?provider=pred`
            since the shape collides with SX Bet market hashes).
            The composite `{provider}:{native_id}` form (e.g.
            `predict:1607914`) is accepted here too and dispatches
            without probing.


            **A bare numeric id or slug is not unique across providers.**
            Polymarket and Predict both use these shapes and their id
            spaces overlap, so sending one without a provider can fail
            with `409` (see that response). Pass `?provider=` — every
            source market in `GET /v1/matching-markets/sports` carries the
            `provider` that goes with its `event_id`.


            **Sports identifiers expire.** Game tickers and slugs are
            delisted once an event settles, and a Hyperliquid id lives
            as long as its question: its daily-recurring price outcomes
            roll over every day, while a season-long question stays
            listed until it resolves. Take current ones from
            `GET /v1/matching-markets/sports` (every source market carries
            its provider's `event_id`) rather than copying one out of this
            reference.
          example: KXNBAGAME-26OCT20OKCSAS
          schema:
            type: string
            example: KXNBAGAME-26OCT20OKCSAS
        - name: provider
          in: query
          required: false
          description: >
            Optional provider override, spelled like the `provider` field
            of this response and every other one (it was `platform` until
            2026-10-09; `?platform=` now answers `400` naming this
            parameter). When omitted, inferred from
            the `event_id` format: `KX…` → Kalshi, `L\d+` → SX Bet.
            Numeric IDs and kebab-case slugs are shared shape between
            Polymarket and Predict; in that case the service probes both
            and returns `409` rather than guessing if the identifier
            resolves on both. Hyperliquid question/outcome integer ids
            collide with these numerics and are not inferred — pass
            `?provider=hyperliquid` (alias `hl`). Pred parent ids share
            the `0x…64hex` shape with SX Bet market hashes and are not
            inferred either — pass `?provider=pred`. Passing `provider`
            explicitly skips the probe entirely and is the recommended
            call whenever you know it. Supplying a value that
            contradicts a composite `{provider}:` prefix is a `400`.
          schema:
            type: string
            enum:
              - kalshi
              - polymarket
              - predict
              - sxbet
              - hyperliquid
              - alpha-arcade
              - prophetx
              - limitless
              - pred
      responses:
        "200":
          description: Event with nested markets
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventResponse"
              examples:
                kalshi_mlb_with_fanout:
                  summary: Kalshi MLB game with sibling fanout
                  # Ten sibling series are tried for an MLB game. Three days
                  # before first pitch Kalshi lists only the extra-innings
                  # market besides the moneyline, so nine land in
                  # siblings_missing and the request still returns 200 with
                  # the markets that did resolve. Partial coverage is the
                  # contract, not an error. Read live 2026-10-09; a game on
                  # the day lists most of the ten.
                  value:
                    event_id: KXMLBGAME-26OCT152000MILLAD
                    provider: kalshi
                    title: "Game 4: Milwaukee vs Los Angeles D"
                    markets:
                      - market_id: KXMLBGAME-26OCT152000MILLAD-MIL
                        title: Milwaukee wins
                      - market_id: KXMLBGAME-26OCT152000MILLAD-LAD
                        title: Los Angeles D wins
                      - market_id: KXMLBEXTRAS-26OCT152000MILLAD-EXTRAS
                        title: "Will there be extra innings in the Milwaukee vs Los Angeles D game originally scheduled for Oct 15, 2026?"
                    fanout:
                      siblings_attempted:
                        - KXMLBSPREAD-26OCT152000MILLAD
                        - KXMLBTOTAL-26OCT152000MILLAD
                        - KXMLBTEAMTOTAL-26OCT152000MILLAD
                        - KXMLBF3-26OCT152000MILLAD
                        - KXMLBF5-26OCT152000MILLAD
                        - KXMLBF5SPREAD-26OCT152000MILLAD
                        - KXMLBF5TOTAL-26OCT152000MILLAD
                        - KXMLBF7-26OCT152000MILLAD
                        - KXMLBRFI-26OCT152000MILLAD
                        - KXMLBEXTRAS-26OCT152000MILLAD
                      siblings_returned:
                        - KXMLBEXTRAS-26OCT152000MILLAD
                      siblings_missing:
                        - KXMLBF3-26OCT152000MILLAD
                        - KXMLBF5-26OCT152000MILLAD
                        - KXMLBF5SPREAD-26OCT152000MILLAD
                        - KXMLBF5TOTAL-26OCT152000MILLAD
                        - KXMLBF7-26OCT152000MILLAD
                        - KXMLBRFI-26OCT152000MILLAD
                        - KXMLBSPREAD-26OCT152000MILLAD
                        - KXMLBTEAMTOTAL-26OCT152000MILLAD
                        - KXMLBTOTAL-26OCT152000MILLAD
                polymarket_nfl:
                  summary: Polymarket NFL game (no fanout needed)
                  value:
                    event_id: nfl-pit-ne-2026-09-20
                    provider: polymarket
                    title: Steelers vs. Patriots
                    markets:
                      - market_id: "3482641"
                        title: Steelers vs. Patriots
                      - market_id: "3517138"
                        title: "Spread: Patriots (-6.5)"
                hyperliquid_question:
                  summary: >-
                    Hyperliquid question with its named outcome markets: the
                    season-long 2026/27 Premier League winner, listed until it
                    resolves in June 2027.
                  value:
                    event_id: "198"
                    provider: hyperliquid
                    title: 2026/2027 English Premier League winner
                    markets:
                      - market_id: "1473"
                        title: "2026/2027 English Premier League winner — Arsenal"
                      - market_id: "1474"
                        title: "2026/2027 English Premier League winner — Manchester City"
                      - market_id: "1475"
                        title: "2026/2027 English Premier League winner — Liverpool"
                      - market_id: "1476"
                        title: "2026/2027 English Premier League winner — Chelsea"
                      - market_id: "1477"
                        title: "2026/2027 English Premier League winner — Manchester United"
                      - market_id: "1478"
                        title: "2026/2027 English Premier League winner — Tottenham Hotspur"
                alpha_arcade_multi_choice:
                  summary: AlphaArcade multi-choice market as a nested event
                  value:
                    event_id: 01K0HQDM64MEEWPTRWQ60HMTH8
                    provider: alpha-arcade
                    title: Presidential Election Winner 2028
                    markets:
                      - market_id: 01K0HQE3CEM2T2RDRWSCJ3V647
                        title: "Presidential Election Winner 2028: JD Vance"
                      - market_id: 01K0HQEM34CD3PR78XY6W1RZX8
                        title: "Presidential Election Winner 2028: Gavin Newsom"
                prophetx_nfl_event:
                  summary: ProphetX NFL event with game-line markets
                  # Read live 2026-10-09 and abridged: the real event lists
                  # 177 markets (halves, quarters, props), each titled
                  # "<event>: <market>".
                  value:
                    event_id: "1700008812"
                    provider: prophetx
                    title: Buffalo Bills at Los Angeles Rams
                    markets:
                      - market_id: "1700008812:219"
                        title: "Buffalo Bills at Los Angeles Rams: Moneyline"
                      - market_id: "1700008812:223"
                        title: "Buffalo Bills at Los Angeles Rams: Spread"
                      - market_id: "1700008812:225"
                        title: "Buffalo Bills at Los Angeles Rams: Total Points"
                limitless_group_event:
                  summary: Limitless group (NegRisk) event with one nested market per outcome
                  value:
                    event_id: uefa-nl-andorra-vs-malta-1789912715515
                    provider: limitless
                    title: "UEFA NL, Andorra vs Malta"
                    markets:
                      - market_id: andorra-1789912715540
                        title: Andorra
                      - market_id: malta-1789912715557
                        title: Malta
                      - market_id: draw-1789912715571
                        title: Draw
                pred_parent_event:
                  summary: Pred parent market with one nested market per child selection
                  value:
                    event_id: "0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5"
                    provider: pred
                    title: Moneyline - 1x2
                    markets:
                      - market_id: 0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5:0xaa7f7cbef93752280ee0e20a5ef6210f8e368867dd6ffaf0b1c22efbe27d3bf5
                        title: Green Bay Packers
        "400":
          description: >
            Invalid `event_id`, or an unsupported value supplied for
            `?provider=`, or a composite prefix that contradicts it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "402":
          description: >
            Free tier monthly request allowance is exhausted, or the
            caller's paid subscription is in a payment-recovery
            state. This endpoint is available on every plan, so 402
            never indicates a tier mismatch here.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredError"
        "403":
          description: API key lacks the required permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: >
            Event not found on the resolved provider. For SX Bet that
            includes a fixture whose game has ended: SX Bet lists a
            fixture's markets only while it trades.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: >
            `event_id` is ambiguous: it resolves to a different real
            event on more than one provider, so no single event can
            honestly be returned. Retry with `?provider=` naming one of
            the `candidates`, or use the composite `{provider}:{id}`
            form. Only bare identifiers whose shape is shared between
            Polymarket and Predict (numeric ids, kebab-case slugs) can
            produce this; composite ids, explicit `?provider=`, Kalshi
            tickers and SX Bet fixture ids never do.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AmbiguousIdentifierError"
        "429":
          description: Rate limit exceeded (API key or upstream)
          headers:
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Upstream provider error fetching the primary event
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Events service not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"


components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
  parameters: {}

  headers:
    RateLimitLimit:
      description: >
        Requests this key may make in a 60-second window. Equal to the
        `rate_limit_per_min` published for the key's plan on `GET /v1/plans`,
        and stable until the plan changes. This is the number to pace against.
      schema:
        type: integer
        format: int64
    RateLimitRemaining:
      description: >
        **Advisory only. Not a countdown, and not safe to build a limiter
        from.** It reports what the rate limiter believed was still available
        when this request was authorized, and the limiter's budget recovers
        continuously as earlier requests age out — so between two strictly
        sequential responses inside one `X-RateLimit-Reset` window this value
        may repeat, skip values, or **increase**.


        Observed behaviour rather than a guarantee, measured live 2026-08-26
        over 120 strictly sequential requests on a 1000/min key (three windows,
        ~40 requests each): **42 increases, the largest +8**. Across the same
        samples it never fell faster than one per request, and by the end of a
        burst it over-stated the true remaining budget by up to **44**
        requests. The gap grows with how fast you send, so it is least accurate
        exactly when a caller consults it. Treat a low value as a reason to
        slow down; never treat a high one as permission to burst.


        To pace locally, count your own requests over a rolling 60 seconds
        against `X-RateLimit-Limit`, and back off on `429` plus `Retry-After`.
      schema:
        type: integer
        format: int64
    RateLimitReset:
      description: >
        End of the current 60-second rate-limit window, as a Unix timestamp in
        **seconds** (never milliseconds). It is a step function, not a rolling
        deadline: every response inside one window reports the same instant, so
        two windows are distinguishable and this value is stable to pace
        against.


        **It is a window boundary, not a refill instant.** The reported budget
        recovers continuously through the window instead of stepping back to
        `X-RateLimit-Limit` when this passes — four boundary crossings measured
        live 2026-08-26 moved by +2, +2, +5 and 0, never to the limit. Use it
        as an upper bound on how long a `429` can last, not as the moment a
        fresh allowance begins.
      schema:
        type: integer
        format: int64
    RetryAfter:
      description: >
        Whole seconds to wait before retrying. Derived from the same window
        instant as `X-RateLimit-Reset` on the same response, so the two never
        disagree. Present on every API-key `429`; an upstream provider `429`
        may omit it. This and the `429` status itself are the authoritative
        back-off signals — prefer them over `X-RateLimit-Remaining`.
      schema:
        type: integer
        format: int64
    RequestId:
      description: >
        An opaque id the API generates for each request and sends on **every**
        response: a success, any error, a router-level `404` or `405`, a
        rejected parameter, a `401`, a `402` and a `429`. Quote it when you
        report a problem; it names that one request in our logs.


        It is a short, URL-safe string (letters, digits, `_` and `-`). Treat
        it as opaque: do not parse it, and do not rely on its length, its
        prefix or any ordering. Every request gets a new one, including a
        retry of a failed request. The API never takes it from you: an
        `X-Request-Id` request header is ignored and not echoed back, so it
        is not a way to correlate your own requests. This is stated once here
        and in the API description, not on each operation, because it is true
        of every route.
      schema:
        type: string
        example: req_7kq2m4vb3xjw5d6a

  schemas:
    PlansResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Plan"

    Plan:
      type: object
      description: >
        Public plan metadata. Stripe price IDs and legacy provisioning IDs are
        intentionally absent.
      required:
        - key
        - name
        - billing_tier
        - trial_days
        - tagline
        - monthly_price_cents
        - included_requests_per_month
        - overage_cents_per_1k
        - rate_limit_per_min
        - max_keys
        - features
        - cta_label
        - highlighted
        - contact_sales
      properties:
        key:
          type: string
          description: Stable public plan key.
          example: pro
        name:
          type: string
          example: Pro
        billing_tier:
          type: string
          description: Runtime entitlement tier; empty for a manually provisioned contact-sales plan.
          example: pro
        trial_days:
          type: integer
          minimum: 0
          example: 0
        tagline:
          type: string
        monthly_price_cents:
          type: integer
          format: int64
          minimum: 0
          description: Monthly display price in US cents; zero for free or contact-sales plans.
        included_requests_per_month:
          type: integer
          format: int64
          minimum: 0
        overage_cents_per_1k:
          type: integer
          format: int64
          minimum: 0
        rate_limit_per_min:
          type: integer
          minimum: 0
        max_keys:
          type: integer
          minimum: 0
        features:
          type: array
          items:
            type: string
        cta_label:
          type: string
        highlighted:
          type: boolean
        contact_sales:
          type: boolean

    SportsMatchingListResponse:
      type: object
      required:
        - data
        - pagination
        - snapshot
      properties:
        data:
          type: array
          description: >
            One page of matched events, soonest scheduled start first, each
            once. Present on every response, as `[]` when nothing matched.
          items:
            $ref: "#/components/schemas/CanonicalSportsEvent"
        pagination:
          description: >
            Pagination metadata for the current page. `total` counts the events
            that match the request's filters, not every matched event.
          $ref: "#/components/schemas/PaginationBlock"
        snapshot:
          $ref: "#/components/schemas/SportsMatchingSnapshot"
          description: >
            Freshness of the matching snapshot this response was read from.
            Describes the DATA; `pagination` describes the page.

    SportsMatchingLookupResponse:
      type: object
      required:
        - data
        - lookups
        - snapshot
      properties:
        data:
          type: array
          description: >
            The matched events the identifiers found, soonest scheduled start
            first, each once however many identifiers found it. Present on
            every response, as `[]` when nothing matched.
          items:
            $ref: "#/components/schemas/CanonicalSportsEvent"
        lookups:
          type: object
          description: >
            Every `event_id` and `source_id` sent, under its spelling as sent
            (whitespace around the provider and ID trimmed), with the canonical
            events in `data` that hold it. An identifier that found nothing is
            listed with no events.
          additionalProperties:
            $ref: "#/components/schemas/SportsMatchingLookupResult"
        snapshot:
          $ref: "#/components/schemas/SportsMatchingSnapshot"
          description: >
            Freshness of the matching snapshot this response was read from.
            Present even when no identifier matched, which is when it matters
            most.

    SportsMatchingLookupResult:
      type: object
      required:
        - event_ids
      description: What one lookup identifier found.
      properties:
        event_ids:
          type: array
          description: >
            The `event_id`s of the canonical events in `data` that hold the
            identifier, matched case-insensitively. Usually one; several only
            when venues reuse an identifier; none when no event in the
            selected population holds it: the market is matched by no other
            venue, the game is past-dated or was cancelled (see
            `include_settled`), its
            full-game moneyline is unmatched under `include_submarkets=false`,
            the game's only matches are player props the `player_prop_match`
            policy excludes (try `same_prop`), or the identifier is unknown.
          items:
            type: string

    SportsMatchingSnapshot:
      type: object
      required:
        - observed_at
      description: >
        Freshness of the matching snapshot a `GET /v1/matching-markets/sports`
        or `GET /v1/matching-markets/sports/lookup` response was read from. Matching runs continuously in the background,
        so an event a response does not contain may be unmatched across
        venues, or missing from a snapshot that stopped updating. This block
        tells you whether the snapshot is still updating.
      properties:
        observed_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When PredictorSDK began the OLDEST venue read behind the matching
            snapshot this response came from — a conservative freshness floor
            for every event in that snapshot, in millisecond-precision ISO
            8601. Present on both routes, including a lookup that matched
            nothing, which is when it matters most.


            **What to expect.** Matching re-reads every venue about every 30
            seconds plus the time the read itself takes, and keeps a venue's
            last good read for at most 10 minutes after that read finished.
            While matching is healthy this value is therefore never more than
            about 12 minutes old, and normally under two minutes. Anything
            older means the snapshot has stopped updating: treat an event the
            response does not contain as unknown rather than unmatched. The
            value dates the venue reads the snapshot holds; a venue whose reads
            keep failing is dropped from it once its last good read is 10
            minutes old, so that venue's events can be missing even from a
            current snapshot.


            `observed_at` is a PredictorSDK read time, the same meaning it
            carries on `pricing.observed_at` and on the catalog's
            `snapshot.observed_at`. It is not when a venue last changed a
            market, and not a game time. It is the start of the read, never
            its end, so it can never claim data is newer than it is.


            Each response reports the snapshot it read. Pages of one cursor
            traversal can come from successive snapshots, so the value can
            advance between pages. Events that `include_settled=true` adds
            from the settled archive have left the current snapshot; they keep
            the identity they had when they left it, which can be days old,
            and do not affect this value.


            `null` when the age is unknown, for example while a snapshot
            written before this field existed is still being served.

    CanonicalSportsEvent:
      type: object
      required:
        - event_id
        - title
        - sport
        - league
        - scheduled_start
        - scheduled_date
        - participants
        - submarkets
      properties:
        event_id:
          type: string
          description: Stable canonical event key.
          example: nba-okc-sas-2026-10-20
        sport:
          type: string
          description: >
            Canonical sport slug. `basketball`, `hockey`, `baseball`, or
            `football` today.
          example: basketball
        league:
          type: string
          description: >
            Canonical league slug. Cross-platform matching covers `nba`,
            `wnba`, `nhl`, `mlb`, and `nfl` today. The value is the first segment of
            `event_id`, so `nba-okc-sas-2026-10-20` is an NBA game. Treat this
            as an open set — leagues are added without a breaking change.
          example: nba
        title:
          type: string
          example: Philadelphia 76ers vs Sacramento Kings
        scheduled_start:
          type: string
          format: date-time
          nullable: true
          description: >
            When the game starts, in UTC at millisecond precision, when a venue
            published an exact start time for it; `null` otherwise. Venues
            often publish only the game's date (Kalshi's tickers carry a date
            and no time), and a venue's "time to be announced" placeholder is
            not a start, so a game whose venues say nothing more is `null`
            here and `scheduled_date` still names its day. Games matched
            before start times were recorded read `null` too, including
            archived games for up to 30 days after that change. `null` means
            "no exact start is known", never a guess at one.


            This is schedule identity, not a status: it does not say whether
            the game has started, ended or been postponed. When venues
            disagree about the start, the earliest exact one is published. A
            postponed game takes its new start when the venues publish it.
        scheduled_date:
          type: string
          format: date
          description: >
            The game's calendar day in America/New_York, as `YYYY-MM-DD`.
            Always present, even when `scheduled_start` is `null`. It is the
            day the game's start falls on in Eastern time, and never earlier
            than the day the `event_id` ends in, which is how a game whose
            start is unknown is still dated. A game that starts at 8:30 PM
            Eastern on the 20th is on the 20th, although its
            `scheduled_start` is on the 21st in UTC. It is the value the
            `scheduled_date` filter on `GET /v1/matching-markets/sports`
            matches, and the day the default list retires the game by.
        participants:
          type: array
          items:
            $ref: "#/components/schemas/CanonicalSportsParticipant"
        submarkets:
          type: array
          items:
            $ref: "#/components/schemas/CanonicalSportsSubmarket"

    CanonicalSportsParticipant:
      type: object
      required:
        - key
        - name
        - role
      properties:
        key:
          type: string
          example: phi
        name:
          type: string
          example: Philadelphia 76ers
        role:
          type: string
          example: away

    CanonicalSportsSubmarket:
      type: object
      required:
        - key
        - market_type
        - segment
        - display_name
        - metric
        - outcomes
        - source_markets
      properties:
        settlement_equivalence:
          type: string
          enum: [equivalent, different, unverified]
          description: >
            Whether the venues in this submarket settle it the same way,
            across the rules in `rule_comparisons`. Always present on
            moneyline, spread, total, team total and player prop submarkets, whether or
            not `include_rules` asked for the rows behind it. `equivalent`
            means every rule is known for every venue and agrees; `different`
            means at least one known difference, even if other rules are
            unknown;
            `unverified` means no difference was found but a rule is unknown
            for at least one venue, so equivalence has not been established.
            A player prop is `equivalent` only with a complete reviewed
            profile for every source contract. A game line is compared on
            three rules, `tie`, `overtime` and `push`, and the comparison
            never changes which venues are paired: it is the evidence a
            consumer needs to decide whether a pairing is a hedge. In
            practice a game line differs on a tie only in a league that can
            end a game level, which today is the NFL (about 0.3% of games).
        rule_comparisons:
          type: array
          description: >
            The rule matrix behind `settlement_equivalence`: one row per rule,
            each listing every source market's value. A moneyline, spread,
            total or team total carries three rows, `tie`, `overtime` and `push`, only when
            the request sent `include_rules=true`; without it they are
            omitted and the verdict stands alone. A player prop always
            carries its nine. Filter rows with `comparison=different` for
            known differences and `comparison=unverified` for gaps. Row-level
            agreement is not a complete contract review. Source values
            reference individual native market IDs, not venue-wide defaults.
            Evidence describes captured source clauses; additional unreviewed
            terms can apply. No prices or payout estimates.
          items:
            $ref: "#/components/schemas/PlayerPropRuleComparison"
        key:
          type: string
          description: >
            Stable canonical submarket key within the event. A second-half or
            fourth-quarter spread, total or team total whose venues' own rules
            count overtime ends its detail with `:incl_ot`
            (`spread|half:2|spread:jax:incl_ot|2.5`); the same line on a venue
            whose rules exclude overtime has the key without the token, so the
            two are never one submarket. A team total's key names its team and
            states its overtime rule the way a game total's does
            (`team_total|full|team_total:phi:incl_ot|24.5`), so two teams' lines
            never share a key.
          example: total|full|total|218.5
        market_type:
          type: string
          description: >
            What the submarket is: `moneyline`, `spread`, `total` (the game's
            total points), `team_total` (one team's own points, such as the
            Eagles over 24.5; its `subject` is the team), or `player_prop`.
            Not an enum: a value a client does not know yet is a new kind of
            market, never a different shape of an old one, and a client reads
            it as text. A team total is matched for the NFL, on Kalshi and
            Polymarket.
          example: total
        segment:
          type: string
          description: >
            `full`, `half:1`, `half:2`, or `quarter:1` to `quarter:4`. A half or
            quarter means regulation play, except a second half or fourth
            quarter whose venues count overtime, which says so in
            `rules.settlement` (`incl_ot`), in its `key` and in its
            `display_name`.
          example: full
        display_name:
          type: string
          description: >
            Human-readable label. A second-half or fourth-quarter line that
            counts overtime ends with ` (incl. OT)`, because a bare `2H` reads
            as regulation play. A team total names its team:
            `Philadelphia Eagles Total 24.5`, `1H Philadelphia Eagles Total
            13.5`.
          example: Total 218.5
        metric:
          type: string
          description: >
            What the submarket measures: `winner`, `spread`, `total`, a player
            prop's statistic, or, for a team total, the team's own score in
            its sport's unit (`points` for the NFL).
          example: total
        line:
          type: number
          format: double
          description: >
            Unsigned threshold for totals, team totals and player props;
            signed handicap for spreads. Omitted for moneyline markets.
          example: 218.5
        subject:
          description: >
            Participant or player whose line is represented. Present for
            subject-owned markets such as spreads, team totals and player
            props; omitted for event-owned moneylines and totals.
          allOf:
            - $ref: "#/components/schemas/CanonicalSportsSubject"
        rules:
          $ref: "#/components/schemas/CanonicalSportsRules"
        outcomes:
          type: array
          items:
            $ref: "#/components/schemas/CanonicalSportsOutcome"
        source_markets:
          type: array
          items:
            $ref: "#/components/schemas/CanonicalSportsSourceMarket"

    PlayerPropRuleComparison:
      type: object
      description: >
        One rule of a submarket's rule matrix, compared across its venues. The
        name is historical: player props introduced it, and game lines reuse
        it with the rules `tie`, `overtime` and `push` instead of a second
        type, so one model reads both. A player prop carries the nine rules
        from `non_participation` to `resolution_source`; a moneyline, spread
        or total carries `tie`, `overtime` and `push`, when the request sent
        `include_rules=true`.
      required: [rule, label, comparison, source_values]
      properties:
        rule:
          type: string
          enum: [non_participation, overtime, stat_definition, threshold, stat_corrections, postponement, cancellation, interruption, resolution_source, tie, push]
          description: >
            `overtime` is shared by props and game lines. `tie` and `push`
            appear on game lines only: `tie` is what the venue does when the
            game or period ends level, and `push` what it does when the
            result lands exactly on a whole-number line.
        label:
          type: string
          description: Human-readable name of the rule dimension.
        comparison:
          type: string
          enum: [equivalent, different, unverified]
          description: Known differences take precedence over unknown source values.
        source_values:
          type: array
          items:
            $ref: "#/components/schemas/PlayerPropRuleSourceValue"

    PlayerPropRuleSourceValue:
      type: object
      description: >
        One source market's value for one rule. Shared by player props and
        game lines, like `PlayerPropRuleComparison`.
      required: [provider, market_id, value, description]
      properties:
        provider:
          type: string
          description: Lowercase provider slug matching a source_markets entry.
        market_id:
          type: string
          description: Exact native market ID matching a source_markets entry.
        value:
          type: string
          description: >
            Machine-readable observed rule value, or `unknown`. Values are
            extensible; clients must tolerate unfamiliar values and read
            description. Never interpret unknown as no restriction. Game
            lines use `tie`: `half_payout` (each side is paid half),
            `void` (stakes returned), `loses` (the named result loses),
            `separate_outcome` (the venue lists a tie contract of its own),
            `not_possible` (the contract cannot end level: a league that
            plays on until one side wins, or a half-point line);
            `overtime`: `included`, `excluded` or `not_applicable`
            (overtime cannot change the result: a tied NHL game is decided
            by exactly one goal, so a puck line of more than one goal
            settles the same either way); and `push`: `void`, `loses` or
            `not_applicable` (a moneyline or a half-point line has nothing
            to land on). Each is `unknown` where the venue's rules do not
            say.
        description:
          type: string
          description: Plain-language meaning, including relevant qualifications.
        evidence_url:
          type: string
          description: >
            Source contract containing the observed clause; omitted for
            unknown rules. For a game line it is the venue's published rules
            document (a Kalshi contract-terms PDF, SX Bet's help center, a
            ProphetX CFTC filing), the record holding the market's own rules
            text, or, where a venue writes no rule, the venue's record of a
            game it settled under it (the description says so).

    CanonicalSportsSubject:
      type: object
      required:
        - type
        - key
      properties:
        type:
          type: string
          description: Canonical subject kind, such as `participant` or `player`.
          example: participant
        key:
          type: string
          description: Stable subject key within the canonical sports namespace.
          example: hou
        name:
          type: string
          description: Human-readable subject name.
          example: Houston Rockets
        team:
          type: string
          description: Canonical team key when the subject is a player.
          example: hou

    CanonicalSportsRules:
      type: object
      properties:
        settlement:
          type: string
          description: >
            Overtime treatment the submarket's key carries: `incl_ot` (the
            default) or `reg` on a full-game total or team total, and
            `incl_ot` on a second-half or fourth-quarter spread, total or team
            total whose venues' own rules count overtime. A half or quarter means regulation play, so
            a regulation line publishes no `rules`. Kept for compatibility;
            the `overtime` row of the submarket's `rule_comparisons` (sent
            with `include_rules=true`) is each venue's own statement of the
            same rule and never contradicts it.
          example: incl_ot

    CanonicalSportsOutcome:
      type: object
      description: >
        One outcome of a canonical submarket. A moneyline lists the away team
        first, a spread `cover` before `not_cover`, and a total, team total or
        player prop `over` before `under`.
      required:
        - key
        - label
        - type
        - side
      properties:
        key:
          type: string
          description: >
            What the outcome pays on: `winner:<participant key>` on a moneyline,
            `cover:<subject>` and `not_cover:<subject>` on a spread (the subject
            is the side giving points), and `over` and `under` on a total, a
            team total (the subject's own points) or a player prop. Each source
            outcome's `canonical_outcome_key` is one of these.
          example: under
        label:
          type: string
          example: Under
        type:
          type: string
          example: position
        side:
          type: string
          example: under

    CanonicalSportsSourceMarket:
      type: object
      description: >
        One venue market matched to a canonical submarket. Every provider uses
        the same fields: `provider`; the provider's parent `event_id`, for
        `GET /v1/events/{event_id}`; the market's own `market_id`, for
        `GET /v1/markets/{market_id}`; and outcomes whose `outcome_id`s are the
        ones market detail returns, each mapped to a canonical outcome. Send
        either ID with this market's `provider`, as the composite
        `{provider}:{id}` or as the `provider` query parameter: a bare Predict
        ID can also name a Polymarket market, and AlphaArcade, ProphetX, Pred,
        Limitless and Hyperliquid IDs are never inferred. Any of `event_id`, `market_id`
        or `market_slug`, prefixed with `provider`, finds this event again
        through `source_id`.
      required:
        - provider
        - event_id
        - market_id
        - market_name
        - outcomes
      properties:
        provider:
          type: string
          enum:
            - kalshi
            - polymarket
            - predict
            - sxbet
            - alpha-arcade
            - prophetx
            - pred
            - limitless
            - hyperliquid
        event_id:
          type: string
          description: >
            The provider's own parent event for this market, for
            `GET /v1/events/{event_id}`: Kalshi's event ticker (a game's spread
            and total markets sit under events of their own), Polymarket's
            event slug, Predict's market ID, SX Bet's `L…` fixture ID,
            AlphaArcade's parent market ULID, ProphetX's integer event ID,
            Pred's parent market ID, Limitless's market slug, or Hyperliquid's
            outcome ID (a Hyperliquid game winner is its own event). It names a
            parent, not this market, so it is not a `market_id`: a Kalshi event
            ticker answers `404` on market detail. SX Bet lists a fixture's
            markets only while it trades, so once the game has ended its `L…`
            id answers `404` on `GET /v1/events/{event_id}` while this
            market's `market_id` still resolves on market detail.
        market_id:
          type: string
          description: >
            Exact provider-native market identifier. It resolves on
            `GET /v1/markets/{market_id}`, either as-is or prefixed with this
            row's `provider` in the composite form `{provider}:{market_id}`.
            A ProphetX player prop's `market_id` names the line it was matched
            on, `<event_id>:<market_id>:<line>`: the pair alone resolves to
            whichever line ProphetX currently favours, and that moves before
            kickoff.


            One provider can contribute MORE THAN ONE entry to the same
            submarket, because a provider is free to model one canonical market
            as several native ones. Kalshi does exactly that for a game
            moneyline: it lists one binary market per team ("Arizona wins",
            "San Francisco wins"), so a Kalshi moneyline row appears twice, once
            per team ticker, each with its own `yes`/`no` outcomes. Group by
            `provider` if you need one row per venue; do not assume the list has
            at most one entry per provider.
        market_name:
          type: string
          description: >
            The market's title, as `GET /v1/markets/{market_id}` titles it. SX
            Bet's market record names only the game, so an SX Bet title is its
            two selection names (`Dallas Cowboys -6.5 vs Tampa Bay Buccaneers
            +6.5`).
        market_slug:
          type: string
          description: >
            The venue's slug for this market, where it has one (Polymarket and
            AlphaArcade). `source_id` accepts it. For market detail send
            `market_id`, which works on every venue:
            `GET /v1/markets/{market_id}` also accepts a Polymarket slug, but
            not an AlphaArcade one.
        outcomes:
          type: array
          items:
            $ref: "#/components/schemas/CanonicalSportsSourceOutcome"

    CanonicalSportsSourceOutcome:
      type: object
      required:
        - canonical_outcome_key
        - label
        - outcome_id
      properties:
        canonical_outcome_key:
          type: string
          description: >
            Canonical outcome this source-native selection represents: read the
            team or side it pays on from here, not from `label`.
          example: under
        label:
          type: string
          description: >
            The venue's own name for this selection, as market detail names it:
            Polymarket's and AlphaArcade's outcome names, SX Bet's and
            ProphetX's selection names (a spread label carries its sign),
            Hyperliquid's side names (`LAR`, `Rams`: each deployer names its
            sides its own way), and `Yes` or `No` on Kalshi, Pred and
            Limitless, whose markets are binary questions such as "Atlanta
            wins".
          example: "No"
        outcome_id:
          type: string
          description: >
            Exact provider-native outcome identifier. It is never a universal
            cross-provider outcome ID.


            On every provider this is the same value
            `GET /v1/markets/{market_id}` returns as `outcomes[].outcome_id`
            for the market named by this row's `market_id`, so the two surfaces
            join directly.


            `polymarket`, `predict` and `alpha-arcade` publish a per-outcome
            token (Polymarket CLOB token id, Predict on-chain id, AlphaArcade
            CLOB token id), so `source_id={provider}:{outcome_id}` finds this
            event from the token alone. A token is unique within its provider,
            not across them: AlphaArcade mirrors Polymarket markets and
            publishes their CLOB tokens, so the same token can appear under
            both. Key a token by `provider` as well. `sxbet` and `kalshi` publish no
            per-outcome token at all, so their references are market-scoped and
            must be read together with `market_id`: an SX Bet market has one
            hash and two named positions (`outcomeOne` / `outcomeTwo`), and a
            Kalshi market is binary (`yes` / `no`). Both are the spelling the
            venue itself uses to address a side — SX Bet keys its order-book
            snapshot by `outcomeOne`/`outcomeTwo`, and Kalshi keys its book by
            `yes`/`no` and reports a trade's `taker_side` the same way.
            `prophetx` publishes small market-scoped integer outcome ids
            (e.g. `4`, `5`) that join to the same market's detail row,
            like SX Bet's positions, and `pred` and `limitless` publish
            `yes` / `no` like Kalshi. `hyperliquid` publishes each side's spot
            coin, `#` followed by ten times the outcome ID plus the side
            (`#114580` and `#114581` for outcome `11458`), the name its order
            book trades the side under; like a Polymarket token it identifies
            one market. A market-scoped outcome id does not
            identify a market: `source_id` answers `400` for Kalshi's, Pred's,
            Limitless's and SX Bet's.
        side:
          type: string
          description: Optional source-native side such as `yes` or `no`.
          example: "no"

    PaginationBlock:
      type: object
      required:
        - limit
        - total
        - has_more
      properties:
        limit:
          type: integer
          description: >
            Page size this page was served with. It echoes the first-page
            `limit` query value; for cursor-native passthrough endpoints, a
            supplied cursor's page size takes precedence on later pages.
          example: 25
        total:
          type: integer
          nullable: true
          description: >
            Total matching items across all pages, when the route can count
            them. `null` when it cannot: `GET /v1/polymarket/wallet/positions`
            and `GET /v1/crypto-prices/binance` read upstreams that give no
            count, and neither route walks a result to find one. `0`
            therefore only ever means an empty result. Rely on `has_more` and
            `next_cursor` to paginate, whatever `total` says.
          example: 143
        has_more:
          type: boolean
          description: Whether additional pages exist beyond this one.
          example: true
        next_cursor:
          type: string
          description: >
            Opaque cursor for fetching the next page. Pass back via the
            `cursor` query parameter. Omitted when `has_more` is `false`.
            Clients must preserve the same filter set when re-using a
            cursor — mismatches return `400`.

    MarketCategory:
      type: string
      description: >
        PredictorSDK-normalized top-level category. This is intentionally
        broad and stable across providers; provider-native tags are not
        exposed as canonical categories.
      enum:
        - sports
        - politics
        - crypto
        - finance
        - economics
        - weather
        - culture
        - technology
        - entertainment
        - other
        - unknown
      example: sports

    CategoryInfo:
      type: object
      required:
        - id
        - name
        - description
      properties:
        id:
          $ref: "#/components/schemas/MarketCategory"
        name:
          type: string
          description: Human-readable category label.
          example: Sports
        description:
          type: string
          description: Short description of the category's intended scope.
          example: Sports events and markets.

    CategoriesResponse:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/CategoryInfo"

    CryptoPricesResponse:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          description: >
            One page of prices, newest first, one per second. A single item
            when no time range was sent; `[]` for an unknown symbol.
          items:
            $ref: "#/components/schemas/CryptoPriceItem"
        pagination:
          description: >
            Pagination metadata for the current page. `total` is always `null`
            on this route, and `next_cursor` goes back as `cursor`.
          $ref: "#/components/schemas/PaginationBlock"

    CryptoPriceItem:
      type: object
      required:
        - symbol
        - value
        - timestamp
      properties:
        symbol:
          type: string
          description: Trading pair in lowercase (e.g. `btcusdt`).
          example: btcusdt
        value:
          type: number
          format: double
          description: Close price for the 1-second interval.
          example: 71392.81
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp in milliseconds for the start of the 1-second interval.
          example: 1775680556000

    MarketsListResponse:
      type: object
      required:
        - data
        - snapshot
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/UnifiedMarket"
          description: Array of markets for the current page.
        snapshot:
          $ref: "#/components/schemas/MarketsSnapshot"
          description: >
            Freshness of the catalog snapshot this page was served from.
            Describes the DATA; `pagination` describes the page.
        pagination:
          $ref: "#/components/schemas/PaginationBlock"
          description: Pagination metadata for the current page.

    MarketsSnapshot:
      type: object
      required:
        - observed_at
      description: >
        Freshness of the immutable catalog snapshot a `GET /v1/markets` page
        was served from. This endpoint reads a stored snapshot rather than
        calling the venues, so the age of the rows is not the age of the
        request.
      properties:
        observed_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When PredictorSDK began reading the OLDEST provider catalog
            contributing rows to this response — a conservative freshness
            floor for the whole page: no row in it is older than this. In
            millisecond-precision ISO 8601.


            `observed_at` is a PredictorSDK read time, the same meaning it
            carries on `pricing.observed_at` and `trading_fees.observed_at`.
            The difference is WHICH read: those two stamp the upstream call
            this request made, while this one stamps the background ingestion
            crawl that built the snapshot, so it is normally minutes to hours
            old rather than milliseconds. It is not `as_of` because no venue
            published it.


            It is the START of that crawl, never its end or the moment its
            snapshot was published. A full crawl takes up to about a minute
            (Kalshi's, the longest, about 45 seconds), and a row read at the
            start of a crawl is that much older than the snapshot's
            publication, so a later stamp would claim fresher rows than the
            first ones are. The matching endpoint's `snapshot.observed_at` is
            a read start for the same reason.


            **It is a floor, not a single fact.** Providers are crawled on
            independent schedules, so the catalog is a merge of snapshots of
            different ages and one of them is the stalest. Narrow the response
            with `?provider=` to get that provider's own read time exactly —
            under a filter this field describes only the selected provider,
            the same way `pagination.total` does.


            Stable for an entire cursor traversal: a cursor stays bound to the
            snapshot that issued it, so every page of one traversal reports
            the identical value even while newer snapshots are published.


            `null` when the bound snapshot carries no read time. An unstamped
            provider makes the whole value null rather than being skipped,
            because reporting a newer floor than the data supports would let
            stale rows through a correct freshness bound.

    PolymarketWalletResponse:
      type: object
      required:
        - address
        - signer
        - profile_image
        - display_name
      properties:
        address:
          type: string
          description: >
            Polymarket proxy address (lowercased) the profile is keyed on.
            When the input was a signer EOA, this is the resolved proxy.
          example: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
        signer:
          type: string
          nullable: true
          description: >
            Original signer EOA from the request, when the input was an
            EOA that resolved to a different proxy. `null` when the input
            was already a proxy address (no resolution needed) or when no
            populated profile could be found for the resolved proxy.
          example: "0x00002569317dc5588b59bae5f63f5f8945912e58"
        profile_image:
          type: string
          format: uri
          nullable: true
          description: URL of the wallet's profile image, or `null` if unset.
          example: "https://polymarket-upload.s3.us-east-2.amazonaws.com/profile-image-501613.png"
        display_name:
          type: string
          nullable: true
          description: >
            User-chosen display name when set; otherwise the auto-generated
            pseudonym Polymarket assigns. `null` only when both are missing.
          example: "Car"

    PolymarketPositionsResponse:
      type: object
      required:
        - address
        - data
        - pagination
      properties:
        address:
          type: string
          description: Resolved Polymarket proxy wallet (lowercased).
          example: "0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b"
        data:
          type: array
          items:
            $ref: "#/components/schemas/PolymarketPosition"
        pagination:
          $ref: "#/components/schemas/PaginationBlock"

    PolymarketPosition:
      type: object
      required:
        - condition_id
        - outcome
        - shares
      properties:
        condition_id:
          type: string
          description: Polymarket condition ID (`0x`-prefixed hex).
          example: "0x7ad403c3508f8e3912940fd1a913f227591145ca0614074208e0b962d5fcc422"
        outcome:
          type: string
          description: Outcome label held in this position (`Yes`/`No` for binary markets).
          example: "Yes"
        shares:
          type: number
          format: double
          description: Current number of outcome shares held, not lifetime shares bought.
          example: 300000

    UnifiedMarket:
      type: object
      required:
        - id
        - provider
        - title
        - category
      properties:
        id:
          type: string
          description: >
            Composite market identifier in the format `{provider}:{provider_id}`
            (e.g. `kalshi:AMAZONFTC-29DEC31`).
          example: "kalshi:AMAZONFTC-29DEC31"
        provider:
          type: string
          description: Prediction market provider.
          enum:
            - kalshi
            - polymarket
            - predict
            - sxbet
            - hyperliquid
            - alpha-arcade
            - prophetx
            - limitless
            - pred
          example: kalshi
        title:
          type: string
          description: Human-readable market title/question.
          example: "Will a court find that Amazon has illegally maintained a monopoly?"
        category:
          $ref: "#/components/schemas/MarketCategory"

    MarketDetailResponse:
      type: object
      required:
        - id
        - provider
        - provider_id
        - title
        - status
        - outcomes
        - pricing
        - trading_fees
        - liquidity_usd
        - volume_24h_usd
        - volume_total_usd
      description: >
        Single-market detail across all nine supported platforms.
        Identity fields are strict-universal (no second fetch on any
        platform); the pricing tier carries per-outcome quotes plus
        market-level aggregates with explicit nulls where a platform
        doesn't natively expose a figure — values are never fabricated.
        The trading_fees tier applies the same rule to the venue's own
        published fee parameters. closes_at/event_id remain deliberately
        omitted, see the endpoint description for the rationale.
      properties:
        id:
          type: string
          description: >
            Composite market identifier in the format
            `{provider}:{provider_id}`. Matches the `id` field
            returned by `GET /v1/markets` so list output flows into
            detail lookups without preprocessing.
          example: kalshi:KXNBA-27-SAS
        provider:
          type: string
          description: Prediction market provider the market_id resolved against.
          enum:
            - kalshi
            - polymarket
            - predict
            - sxbet
            - hyperliquid
            - alpha-arcade
            - prophetx
            - limitless
            - pred
          example: kalshi
        provider_id:
          type: string
          description: >
            Provider-native market identifier. Kalshi ticker,
            Polymarket numeric id, Predict numeric id, SX Bet
            `marketHash`, Hyperliquid outcome id, AlphaArcade market ULID,
            ProphetX `<event_id>:<market_id>` (resolving the favourite
            primary-line strike) or `<event_id>:<market_id>:<line>` (that
            line), or Limitless slug. For Polymarket
            markets resolved by slug, this is normalized to the numeric
            id. Hyperliquid integer ids collide with Polymarket/Predict
            numeric ids, so look them up via the composite id
            (`hyperliquid:<id>`, as returned by `/v1/markets`) or
            `?provider=hyperliquid`. Limitless slugs collide with
            Polymarket/Predict slugs, so look them up via the composite
            id (`limitless:<slug>`) or `?provider=limitless`. Pred
            `<parent>:<child>` pairs share the `0x…64hex` shape with SX
            Bet hashes, so look them up via the composite id
            (`pred:<parent>:<child>`) or `?provider=pred`; a bare
            `pred:<parent>` addresses the parent as a group.
          example: KXNBA-27-SAS
        title:
          type: string
          description: >
            Human-readable market title. Each platform exposes a
            slightly different field — Kalshi `title`, Polymarket
            `question`, Predict `title`, SX Bet composed from outcome
            labels (team-pair fallback) so a game's moneyline, spread,
            and total markets stay distinguishable, and Hyperliquid
            composed from outcome/question metadata.
          example: Will San Antonio win the 2026 Pro Basketball Finals?
        status:
          type: string
          enum:
            - open
            - closed
            - settled
          description: >
            Normalized lifecycle status: `open` takes orders, `closed`
            has stopped trading without a final result, and `settled`
            has the venue's published result. Only an `open` market can
            report `pricing.availability: live`. Mapping per platform:
            Kalshi `active` → open · `determined` or `amended` with the
            result on the record (yes, no or scalar), and `finalized` →
            settled · `initialized`, `inactive`, `closed` and `disputed`
            → closed (a challenged result is not settled). Polymarket
            `closed` with UMA resolution status `resolved` → settled ·
            any other `closed` market (a resolution proposal pending, or a
            pre-2023 market with no UMA status) → closed · otherwise open.
            Predict `status=RESOLVED` → settled · `tradingStatus=OPEN` →
            open · otherwise closed. SX Bet: a reported result
            (`reportedDate`, a void included) → settled · otherwise
            `ACTIVE` → open · otherwise closed; SX Bet leaves some settled
            markets `ACTIVE`, so its `status` alone never decides
            settlement. Hyperliquid: an outcome listed in its question's
            `settledNamedOutcomes` → settled · otherwise open. AlphaArcade
            `isResolved` → settled · otherwise open. ProphetX `active` →
            open · otherwise closed; ProphetX serves only upcoming and live
            events, so a ProphetX market never reads `settled` — once its
            game is over the market id answers `404`. Limitless `FUNDED` /
            `FUNDED_FLAGGED` → open · `RESOLVED` → settled · `LOCKED`,
            `DRAFT` and anything else → closed. Pred `active` → open ·
            `resolved` / `redeemed` → settled · anything else → closed.
            Unknown upstream values map to closed.
          example: open
        outcomes:
          type: array
          minItems: 1
          description: >
            Outcomes with per-outcome quotes, in the venue's order.


            A two-sided market lists both sides, and `outcomes[0]` is the
            platform's primary/headline outcome: Kalshi `Yes`,
            Polymarket's first outcome token (its `bestBid`/`bestAsk`
            side), Predict `indexSet=1`, SX Bet `outcomeOne`,
            Hyperliquid's first `sideSpec`, ProphetX's first selection,
            and `Yes` on AlphaArcade, Limitless and Pred. Render
            `outcomes[0].price` as the headline probability; do NOT
            search for an outcome named "Yes" (names are free-text on
            Predict, SX Bet, Hyperliquid and ProphetX).


            A market that groups several options lists one outcome per
            option: an AlphaArcade multi-choice market, a Limitless
            group, and a Pred parent. Each outcome is that option's YES,
            and its `outcome_id` is the option's own composite market id,
            which this endpoint resolves. A group has no headline
            outcome, and it can list dozens of outcomes (21 on
            AlphaArcade's 2028 presidential market), so never assume two.
          items:
            $ref: "#/components/schemas/MarketDetailOutcome"
        pricing:
          $ref: "#/components/schemas/MarketDetailPricing"
        trading_fees:
          $ref: "#/components/schemas/MarketDetailTradingFees"
        liquidity_usd:
          type: number
          nullable: true
          description: >
            Resting order-book depth valued in USD — strictly CLOB
            book depth, never an AMM pool size or a synthetic score.
            Polymarket exposes it natively (`liquidityNum`); null for
            Kalshi (its upstream `liquidity_dollars` is deprecated and
            always zero), Predict (stats is null on the record), and
            SX Bet/Hyperliquid (no scalar without summing the raw order book).
          example: 8112916.41585
        volume_24h_usd:
          type: number
          nullable: true
          description: >
            Trailing-24h traded volume in USD notional. Null where the
            platform doesn't denominate volume in USD — notably Kalshi
            (contracts; see `volume_24h_contracts`) — or doesn't expose
            a volume aggregate at all (SX Bet, Hyperliquid, Predict's record).
          example: 3902738.7224900015
        volume_total_usd:
          type: number
          nullable: true
          description: >
            Lifetime traded volume in USD notional. Same per-platform
            availability as `volume_24h_usd`. Never fabricated by
            converting contract counts through a price.
          example: 40230495.82577211
        volume_24h_contracts:
          type: number
          description: >
            Trailing-24h traded volume in contracts (Kalshi
            `volume_24h_fp`; fractional contracts supported). Omitted
            for platforms that denominate volume in USD.
          example: 5258039.8
        volume_total_contracts:
          type: number
          description: >
            Lifetime traded volume in contracts (Kalshi `volume_fp`).
            Omitted for platforms that denominate volume in USD.
          example: 55595118.18
        open_interest:
          type: number
          description: >
            Total outstanding contracts (Kalshi `open_interest_fp`).
            Contracts, not USD — a positioning gauge kept separate from
            `liquidity_usd` (depth). Omitted where the platform doesn't
            expose market-level open interest.
          example: 24585005.01

    MarketDetailOutcome:
      type: object
      required:
        - name
        - price
        - bid
        - ask
        - last
      properties:
        name:
          type: string
          description: >
            Outcome label as the platform reports it. Kalshi binary
            markets normalize to `Yes`/`No`; Polymarket parses the
            stringified outcomes array (also typically `Yes`/`No`);
            Predict reports per-outcome names; SX Bet uses
            outcome-one/outcome-two names (e.g. team labels with
            spreads applied); Hyperliquid uses the outcome `sideSpecs`
            names.
          example: "Yes"
        outcome_id:
          type: string
          description: >
            Stable per-platform key for this outcome: Kalshi, Limitless
            and Pred `yes`/`no`; Polymarket CLOB token id; Predict
            on-chain id; SX Bet `outcomeOne`/`outcomeTwo`; Hyperliquid
            coin encoding (`#<10*outcome+side>`); ProphetX's
            market-scoped selection id (`4`, `5`); AlphaArcade the CLOB
            token id of the Polymarket market it mirrors. On a market
            that groups several options (see `outcomes`) it is the
            option's composite market id, such as
            `alpha-arcade:01K0HQE3CEM2T2RDRWSCJ3V647`. It is the id
            `/v1/matching-markets/sports` publishes for the same side,
            and the join key for future per-outcome sub-resources
            (order-book depth). Omitted only where the venue publishes no
            key for a side: the two sides of an AlphaArcade market that
            mirrors no Polymarket market (21 of 993 two-sided AlphaArcade
            markets on 2026-10-04, none of them sports).
          example: "yes"
        price:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
          description: >
            Current implied probability of this outcome in 0–1 — the
            headline field, equal to the implied probability on every
            supported platform. Derivation cascade: mid of bid/ask when
            two-sided → the single available side → last trade →
            platform mark (Polymarket `outcomePrices`, which preserves
            0/1 resolution marks on settled markets; AlphaArcade's
            catalog midpoint). Because the cascade differs by what each
            platform exposes, `price` is a DISPLAY number — when
            comparing across platforms or sizing trades, prefer
            `bid`/`ask` directly where present. Null when no quote of
            any kind exists.


            **A non-null `price` does not mean a tradeable price.** The
            last two rungs of the cascade produce a number with no book
            behind it, and the mid of a 0.01 / 0.99 book produces a
            confident-looking 0.5 that no one will fill. Read
            `pricing.availability` first: `live` says at least one
            outcome has a book the venue treats as quoted;
            `indicative` says every price here is a mark, a lone side,
            or a book the venue's own spread threshold rejects.


            GUARANTEE: when `pricing.availability` is `live` or
            `indicative`, `price` is non-null on every outcome. Values
            are rounded to at most 6 decimal places.
          example: 0.195
        bid:
          type: number
          nullable: true
          description: >
            Best bid for this outcome in 0–1 probability. Null when
            that book side is empty or the platform doesn't publish
            per-outcome quotes on the record (Polymarket non-primary
            outcomes). Hyperliquid's second side is derived from the
            merged book complement (`1 − first-side ask`), matching the
            platform's order-book structure; no other platform
            synthesizes bid from `1 − ask`.
          example: 0.19
        ask:
          type: number
          nullable: true
          description: >
            Best ask (price to take this outcome) in 0–1 probability.
            For SX Bet this is derived from the opposite side's best
            maker quote (`1 − bid(other)`), which is that book's
            actual taker price. For Hyperliquid's second side this is
            derived from the merged book complement (`1 − first-side
            bid`).
          example: 0.2
        last:
          type: number
          nullable: true
          description: >
            Last traded price for this outcome in 0–1 probability.
            Null where the platform exposes no last-trade on the
            record (Predict, SX Bet, Hyperliquid).
          example: 0.2
        bid_size:
          type: number
          description: >
            Resting size at the best bid in native contract/share
            units (NOT USD). Omitted where the platform publishes no
            per-side size — Kalshi publishes YES-side sizes only;
            Hyperliquid publishes top-of-book sizes from `l2Book`;
            Polymarket and SX Bet publish none on this path.
          example: 715466.16
        ask_size:
          type: number
          description: >
            Resting size at the best ask in native contract/share
            units. Same availability as `bid_size`.
          example: 9256.49

    MarketDetailPricing:
      type: object
      required:
        - availability
        - scale
        - source
        - as_of
        - as_of_kind
        - observed_at
      description: >
        Market-wide quote metadata for the pricing tier. Always present
        on the response; `availability` tells the truth about what the
        tier could hydrate instead of leaving consumers to guess from
        nulls.


        **Bounding quote freshness.** `as_of` is the provider's own
        stamp and means something different on each platform, so read
        `as_of_kind` before applying an age bound to it — only
        `as_of_kind: quote` tracks the quote closely enough to bound at
        all, and how tight that bound can be still varies by venue.
        `observed_at` is
        PredictorSDK's own read time and means the same thing on every
        provider, so it is the field to bound when you need one
        threshold that behaves identically across platforms.
      properties:
        availability:
          type: string
          enum:
            - live
            - indicative
            - partial
            - no_quotes
            - unavailable
          description: >
            How completely — and how honestly — the pricing tier
            hydrated. It answers two questions in this precedence
            order: did every outcome get a price, and is any of those
            prices backed by a book you could actually cross.


            `live` — the market is `open`, every outcome carries a price,
            AND at least one outcome has a two-sided book the venue itself
            treats as quoted. **This is the only value that licenses
            reading `price` as a tradeable level.** A `closed` or `settled`
            market is never `live`, whatever its record still quotes: it
            takes no orders.


            `indicative` — every outcome carries a price, but no
            outcome has such a book behind it. The prices are marks,
            one side of a book with nothing facing it, or a two-sided
            book wider than the venue's own published spread threshold
            for that market. Concretely: an AlphaArcade market whose
            order book is empty, where the catalog midpoint is the only
            price left standing; a Predict market quoted 0.01 / 0.99,
            whose 0.5 midpoint is arithmetic rather than a market
            (Predict publishes a per-market `spreadThreshold` and this
            server honours it); a settled Polymarket market whose 1/0
            `outcomePrices` are resolution marks; or a book with one
            resting order and nothing on the other side. `price` is
            still populated and still the venue's own number — treat it
            as roughly where the market is thought to be, never as a
            level you can trade or arbitrage against. **Filter or flag
            `indicative` before computing cross-venue edges**: an
            `indicative` 0.5 next to a `live` 0.735 elsewhere is not a
            23¢ opportunity, it is one venue with no book.


            `partial` — some but not all outcomes priced.
            Incompleteness is reported ahead of quote quality because it
            is the louder warning, so a `partial` market says nothing
            about the book behind the prices it does carry.


            `no_quotes` — the pricing read succeeded and there is
            nothing at all: no book and no mark (SX Bet or Hyperliquid
            with no resting orders; Kalshi provisional/multivariate
            markets whose quotes are empty-book placeholders;
            AlphaArcade markets that have never traded). An empty book
            truthfully read is still an observation, so `observed_at` is
            populated.


            `unavailable` — the pricing enrichment fetch failed or
            timed out (SX Bet/Hyperliquid); identity fields are still
            served and `observed_at` is null.
          example: live
        scale:
          type: string
          enum:
            - probability_0_1
          description: >
            Self-describing unit declaration for all price fields.
            Single canonical scale today; new values would be added
            alongside (never replacing) this one.
          example: probability_0_1
        source:
          type: string
          enum:
            - market_record
            - orderbook
          description: >
            Where the quotes came from. `market_record` — embedded in
            the same record as the identity fetch: Kalshi, Polymarket
            and Predict, and the group views of Limitless and Pred,
            which read no order book. `orderbook` — the platform's
            order book, in a bounded second fetch where it is a
            separate route: SX Bet (`/orderbook-v3/snapshot`),
            Hyperliquid (`l2Book`), AlphaArcade, Limitless, Pred and
            ProphetX. A failed book read keeps `orderbook` and reports
            `availability: unavailable` or the record's marks.
          example: market_record
        as_of:
          type: string
          format: date-time
          nullable: true
          description: >
            The upstream timestamp on this platform's own clock, as
            RFC3339. When the two sides carry independent stamps this is
            the OLDER of them — a conservative floor that never
            over-claims freshness. Null when the record carries no
            timestamp at all (Predict, AlphaArcade, and SX Bet).


            **This field is NOT a uniform freshness bound. Read
            `as_of_kind` first.** What it measures differs per platform:
            on Hyperliquid it is the order book's server time and moves
            with the book, while on Kalshi it is a record write that does
            not move while the market is quoted. Measured live on
            2026-08-24 over two runs — 100 open markets read twice 7.5
            minutes apart, and 96 open markets read twice 11 minutes
            apart:


            * `kalshi` — **15 hours to 137 days old** on markets
            reporting `status: open` and `availability: live` with a real
            two-sided book. The age tracks how long ago the record was
            last written, so it depends entirely on the market: same-day
            game markets ran a median of ~23 h in one sample and ~43 h in
            another, while a broad sample of the market list ran a median
            of ~101 days (`CHINAUSGDP-30`, `status: open`, quoted 0.15 /
            0.19, was stamped 2026-04-09). Advanced on 0 of 39 and 0 of
            42 markets across the two runs, including five whose prices
            moved inside the window. **Not boundable at any threshold —
            do not infer one from these numbers.**

            * `polymarket` — 45 s to 405 s old; advanced on 60 of 60 and
            25 of 25 markets, while only 1 of those 25 prices changed.
            Values recur identically across dozens of unrelated markets
            (one batch write, not one quote). Bounds record age in
            minutes, not quote age in seconds.

            * `hyperliquid` — the `l2Book` server timestamp, a real quote
            stamp and the one field here that genuinely supports a
            seconds-scale bound: polled directly it tracks wall clock to
            the second.

            * `prophetx` — the best offer's own `updated_at` on each side,
            the older of the two. It describes that resting offer, not the
            whole book: it changes when the best offer changes, which makes
            it a quote stamp, but on a thin book an offer can stay best for
            days. Measured from under a minute to 2.6 days old (median
            about 2 hours) across 51 quoted markets sampled from the whole
            catalog on 2026-10-03, including 2.6 days on a market reporting
            `availability: live`. Size the bound to the venue's liquidity,
            not in seconds.

            * `limitless` — the market record's `updatedAt`: a record
            write stamp in the same family as Polymarket's, advancing on
            the venue's own rewrite cadence rather than with the quote.
            Bounds record age in minutes, not quote age in seconds. The
            order book itself carries no wall-clock stamp.

            * `pred` — the child order book's `last_updated_at`: the book
            snapshot time, advancing when the book moves, so an age bound
            on it is meaningful. On a thin book it is legitimately old —
            size the threshold to the venue's liquidity, as with
            ProphetX. Measured seconds-old on a live NFL book
            2026-09-23. Null (with `as_of_kind: unknown`) on the parent
            group view, which performs no book read, and whenever the
            book is gone (a settled child's book 503s upstream).

            * `predict`, `alpha-arcade`, `sxbet` — always null. SX Bet's
            order book publishes a monotonic book `version` and no
            wall-clock stamp anywhere.


            These ranges are observed behaviour, not a contract: they are
            published so a consumer can pick a threshold from measured
            data rather than guessing, and they can change whenever a
            venue changes how it writes its records. Server ingest time
            is never substituted into this field — that would masquerade
            as an upstream stamp. Use `observed_at` for the read time.
          example: "2026-05-31T16:23:22.216198Z"
        as_of_kind:
          type: string
          enum:
            - quote
            - record_refresh
            - record_static
            - unknown
          description: >
            What `as_of` MEASURES on this provider, so one consumer code
            path can bound freshness tightly where the value is a quote
            time and refuse to pretend where it is not. Always present;
            `unknown` whenever `as_of` is null, never an empty string.


            `quote` — the stamp advances when the quote advances, so an
            age bound on it is meaningful. Hyperliquid (`l2Book` server
            time), ProphetX (the best offer's `updated_at`) and Pred (the
            book's `last_updated_at`). How TIGHT that bound can be still
            depends on the venue: Hyperliquid's tracks wall clock to the
            second, while ProphetX's describes the resting best offer and
            is legitimately hours or days old on a thin book. Size the
            threshold to the venue's liquidity; a blanket seconds-scale
            bound rejects most of ProphetX.


            `record_refresh` — the stamp advances on a periodic rewrite
            of the provider's record, independent of whether the quote
            moved. Bounds RECORD age (minutes), not quote age. Polymarket.


            `record_static` — the stamp does not advance while the market
            is actively quoted, so it bounds nothing at any threshold.
            Kalshi. Treat quote freshness as unknown here and do not gate
            on `as_of`; bound `observed_at` instead and take executable
            price from the venue's own book.


            `unknown` — `as_of` is null: the record carries no timestamp
            of any kind (Predict, AlphaArcade, SX Bet). Kept distinct
            from `record_static` because the underlying fact differs even
            though the consumer's action does not.
          example: record_static
        observed_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When PREDICTORSDK read these quotes, as RFC3339 — an
            observation timestamp, not an upstream one. Named
            `observed_at` rather than `as_of` for the same reason
            `trading_fees.observed_at` is: `as_of` is the provider's own
            stamp, and reusing the name for a differently-defined value
            would bake that confusion into a second field.


            This is the one timestamp on the response whose definition
            does not vary by platform, which makes it the field to bound
            when you need a single threshold that behaves identically
            everywhere. It bounds the age of the READ, not the age of the
            quote: on a `record_static` provider a fresh `observed_at`
            beside a 23-hour `as_of` is the honest description of what
            the venue served. `/v1/markets/{market_id}` reads the venue
            live on every request and caches nothing, so this stamp is
            the request time.


            Taken BEFORE the upstream call, so it is never newer than the
            moment the quotes were actually observed and `now -
            observed_at` never understates their age. Null only when
            `availability` is `unavailable` — no quotes were observed, so
            there is nothing to stamp. Populated for `no_quotes`, where an
            empty book is a successful observation.
          example: "2026-08-21T03:41:07.000Z"
        neg_risk:
          type: boolean
          description: >
            True when this market belongs to a negative-risk
            multi-outcome event (Polymarket `negRisk`, Predict
            `isNegRisk`). On a multi-outcome record, outcome prices
            intentionally need not sum to 1 — do not "normalize" the
            book. Note that for the BINARY member markets these
            platforms serve today the flag signals event-level
            structure (this market is one leg of a mutually-exclusive
            set); the binary pair itself still sums to ~1. Omitted
            when false.
          example: true

    MarketDetailTradingFees:
      type: object
      required:
        - availability
        - observed_at
        - source
        - charge_basis
        - taker
        - maker
      description: >
        The trading fee the PREDICTION MARKET charges on a trade in this
        market — the venue's own published parameters, normalized. This
        has nothing to do with PredictorSDK's subscription pricing.


        Always present on the response. Every published model across the
        nine platforms is a per-share fee times the traded share count,
        and only the price term differs, so `taker.model` plus its
        parameters expresses each venue exactly rather than approximating
        any of them — except Limitless, whose taker curve is published
        as table rows with no closed form: its descriptor reports
        `partial` with a null taker leg instead of inventing a number.
        (A Limitless market with fees disabled reports `published` with
        both legs zero — an asserted free, never an unknown. A Pred
        market whose record omits `fee_rate_bps` reports `partial` for
        the same reason: the venue's own integration note says absent
        fee configuration must be confirmed, not assumed fee-free.)
        Compute the fee yourself:


        `fee = shares × f(price)`, then apply `rounding`, where `f` is


        - `quadratic`: `rate × (price × (1 − price)) ^ exponent`

        - `min_price`: `rate × min(price, 1 − price)`

        - `notional`: `rate × price`

        - `none`: `0`


        The effective rate on notional — what you actually compare across
        venues — is `f(price) / price`. For `quadratic` at `exponent: 1`
        that reduces to `rate × (1 − price)`, which is why no single
        basis-point figure can express any of these venues: at
        `rate: 0.07` the effective cost runs from 693 bps at a price of
        0.01 down to 7 bps at 0.99.


        `availability` and the legs are on separate axes on purpose:
        availability says what is KNOWN, the legs say what is CHARGED. A
        fee-free market is `availability: published` with
        `model: "none"` and `rate: 0` — an asserted zero, distinguishable
        from `unpublished` (the venue publishes nothing) and from
        `account_specific` (rates exist but are per-account).


        OUT OF SCOPE and therefore not modeled as fields, though each is
        real and moves the number: rebates (Polymarket's maker-rebate and
        taker-rebate programs), per-account discounts (Predict's 10%
        invite discount), volume/staking tiers and the market deployer's
        own fee, its `deployerFeeScale` (Hyperliquid), and third-party
        pass-through (Kalshi FCM customers, Polymarket builder fees). The parameters here are the venue's PUBLISHED base
        for this market; a specific account may pay less.
      properties:
        availability:
          type: string
          enum:
            - published
            - partial
            - account_specific
            - unpublished
            - unavailable
          description: >
            `published` — the full parameter set for this market is known.
            `partial` — fees may be charged here but a parameter is
            missing or uninterpretable upstream; the legs carry what is
            known and null the rest, never a guess. It also covers the
            case where the parameters in force are known but the venue's
            pending-change list was unreadable, so an omitted
            `scheduled_change` means "unknown" rather than "none".
            `account_specific` —
            rates exist but are set per trading account and require your
            own venue credentials (SX Bet's `GET /user/fees-v3`,
            Hyperliquid's `POST /info {"type":"userFees"}`).
            `unpublished` — the venue publishes no fee model for this
            market at all. `unavailable` — our own bounded parameter
            fetch failed or timed out; identity and pricing are still
            served. Same contract as `pricing.availability`.
          example: published
        observed_at:
          type: string
          format: date-time
          nullable: true
          description: >
            When PredictorSDK READ these parameter values — an observation
            timestamp, not an upstream stamp.


            Named `observed_at` rather than `as_of` deliberately:
            `pricing.as_of` is the provider record's own last-update time,
            which is a different thing, and reusing the name for a
            differently-defined timestamp would bake that confusion into
            a second field. Null when nothing is read at request time
            (`source: venue_schedule`) or when nothing could be read
            (`availability: unavailable`).
          example: "2026-08-21T03:41:07.000Z"
        source:
          type: string
          enum:
            - market_record
            - series_record
            - venue_schedule
          description: >
            Where the parameters came from, so a latency-sensitive
            consumer knows which platforms cost extra hops.
            `market_record` — embedded in the same market record the
            identity fetch already returned (Polymarket `feeSchedule`,
            Predict `feeRateBps`, AlphaArcade `feeBasePercent`); zero
            extra fetches. `series_record` — the parameters live on
            parent records and cost up to three small, TTL-cached hops
            (Kalshi: the event record for its series ticker and any fee
            override in force, the series for the base
            `fee_type`/`fee_multiplier` that override supersedes, then
            the event's pending fee changes for `scheduled_change`).
            `venue_schedule` — the venue exposes no per-market parameter
            and the answer comes from its published schedule (SX Bet,
            Hyperliquid).
          example: market_record
        charge_basis:
          type: string
          nullable: true
          enum:
            - fill
            - settlement_profit
            - close_or_settlement
          description: >
            What the fee is assessed on. `fill` — assessed when the order
            fills, from the traded share count and the fill price.
            `settlement_profit` — assessed at settlement, on profit
            (total return − stake), and only on a position that WON; a
            loss or a void is never charged. `close_or_settlement` —
            assessed when a position is closed or settles, and never when
            one is opened.


            Read this before comparing rates across venues. SX Bet
            charges on winning profit at settlement, so its fee is an
            expected-value haircut rather than an entry cost — a consumer
            that models it as a percentage of notional is wrong about the
            shape before it is wrong about the rate. The reported basis is
            the one that applies to trading: SX Bet's four payout fees.
            Its separate `refundFee` is charged at fill time on the
            capital-efficiency refund rather than on profit, and is out of
            scope here along with its rates.


            Hyperliquid's outcome markets are `close_or_settlement`: its
            fee page says outcome trading "only charges fees when closing
            or settling, not when opening outcome positions", so a buy
            that opens a position costs nothing and outcome trading has no
            maker rebates. The rate is still the account's own
            (`account_specific`, `rate: null`), and a market's deployer can
            add a fee on top through the `deployerFeeScale` in Hyperliquid's
            `outcomeMeta` (`1.0` on outcomes from the `out` and `skew`
            deployers; trade.xyz documents `0`, which adds none, for its
            own).
          example: fill
        taker:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/MarketDetailFeeLeg"
          description: >
            The taker leg: the fee on the incoming order that fills a
            resting one. Always present, and **`null` (never omitted) when
            no model can be stated for this side**. `availability` says
            when. `published`: never null. `partial`: null where the venue
            does not let us state the leg, which today is Limitless's taker
            (a curve with no closed form) and both legs of a market whose
            record omits its fee parameters (or, on Kalshi, names a fee type
            we do not model). `account_specific`: null for
            SX Bet, whose rates are set per account (Hyperliquid returns
            the leg with a null `rate` instead). `unavailable`: always
            null, because the parameters could not be read or the venue
            (ProphetX) publishes none. `unpublished`: always null. So
            `taker.rate` can be read without a null check once
            `availability` is `published`.
        maker:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/MarketDetailFeeLeg"
          description: >
            The maker leg: the fee on the resting order a taker fills.
            Always present, and **`null` (never omitted) when no model can
            be stated for this side**. `availability` says when.
            `published`: never null. `partial`: null for a Polymarket
            market whose record does not say that only takers pay, and
            for both legs of a market whose record omits its fee
            parameters (or, on Kalshi, names a fee type we do not model).
            `account_specific`: null for SX Bet, whose rates are set per
            account (Hyperliquid returns the leg with a null `rate`
            instead). `unavailable`: always null, because the parameters
            could not be read or the venue (ProphetX) publishes none.
            `unpublished`: always null. So `maker.rate` can be read
            without a null check once `availability` is `published`.
        scheduled_change:
          $ref: "#/components/schemas/MarketDetailScheduledFeeChange"

    MarketDetailFeeLeg:
      type: object
      required:
        - model
        - rate
        - exponent
        - rounding
      description: >
        One side's fee model. Both `taker` and `maker` are always
        returned and you pick: PredictorSDK does not infer which side you
        will be, since that needs an order it has not seen. The leg itself
        is `null` on `trading_fees` when the model cannot be stated for
        that side (see `taker` and `maker` there for when), and never null
        on a `scheduled_change`.


        Note that "maker" and "taker" mean the venue's own definition —
        the resting order is the maker, the incoming order that fills it
        is the taker.
      properties:
        model:
          type: string
          enum:
            - quadratic
            - min_price
            - notional
            - none
          description: >
            The price term of the per-share fee.


            `quadratic` — `rate × (price × (1 − price)) ^ exponent`.
            Kalshi (`roundup(0.07 × C × P × (1−P))`, exponent 1 by
            published formula), Polymarket
            (`fee = C × feeRate × p × (1 − p)`, exponent read per
            market), AlphaArcade (`feeBase × quantity × price ×
            (1 − price)`).


            `min_price` — `rate × min(price, 1 − price)`. Predict
            (`Base Fee % × min(Price, 1 − Price) × Shares`). A TENT, not
            a parabola: the effective rate on notional is flat at `rate`
            for every price at or below 0.5 and only declines above it.
            Do not collapse this into `quadratic`; it would overstate the
            fee at every price below 0.5.


            `notional` — `rate × price`, i.e. a flat fraction of traded
            notional with no price curve at all.


            `none` — zero, asserted.
          example: quadratic
        rate:
          type: number
          nullable: true
          description: >
            A DECIMAL FRACTION, never basis points — matching each
            venue's own published unit. `0.07` is 7% of the price term,
            not 7 bps. Null means the shape is known but the value is
            not (Hyperliquid, where the effective rate is account-tiered).
            `0` means zero, asserted.
          example: 0.035
        exponent:
          type: number
          nullable: true
          description: >
            Only meaningful for `model: "quadratic"`; null otherwise.
            Read per market from Polymarket's `feeSchedule.exponent`;
            `1` on Kalshi and AlphaArcade, whose published formulas are
            exponent-1 by construction. Never defaulted when unknown — a
            quadratic leg with an unreadable exponent makes the whole
            descriptor `partial`, because the exponent moves the fee by
            roughly 4x and can flip which venue is cheaper.
          example: 1
        rounding:
          $ref: "#/components/schemas/MarketDetailFeeRounding"

    MarketDetailFeeRounding:
      type: object
      nullable: true
      required:
        - direction
        - increment
      description: >
        The venue's published rounding rule, applied to the computed fee.
        Null where the venue publishes no rule (Predict) — treat that as
        UNKNOWN rather than as "no rounding", which would understate
        small trades.


        Kalshi additionally charges a per-fill rounding fee that restores
        the member's balance precision ($0.01 for non-direct members,
        $0.0001 for direct), offset by a rebate once accumulated rounding
        exceeds $0.01. That depends on fill fragmentation and member
        type, so it is not predictable pre-trade and is deliberately not
        modeled here — expect a small positive difference between the
        figure you compute and the figure Kalshi charges across many
        partial fills.
      properties:
        direction:
          type: string
          enum:
            - up
            - nearest
            - down
          description: >
            `up` — the venue rounds the fee UP to the next increment
            (Kalshi ceils to $0.0001; AlphaArcade ceils to 1e-6 USDC).
            `nearest` — the venue rounds to the nearest increment
            (Polymarket rounds to 5 decimal places, with 0.00001 USDC the
            smallest fee charged). `down` — the venue truncates the fee to
            the increment (Pred computes in 6-decimal base units and
            always rounds down; a fee below the smallest chargeable unit
            — 0.000001 — is zero, not clamped up to it).
          example: up
        increment:
          type: number
          description: The rounding increment in the market's quote currency.
          example: 0.0001

    MarketDetailScheduledFeeChange:
      type: object
      required:
        - effective_at
        - taker
        - maker
      description: >
        The next PUBLISHED change to this market's fee parameters and when
        it takes effect. OMITTED (not null) when there is no pending
        change, matching `pricing.neg_risk`. A change is reported only when
        both of its legs can be stated, so `taker` and `maker` here are
        never null.


        Kalshi is the only platform that publishes this today, and it
        matters: every MLB series currently runs at a `fee_multiplier` of
        0.5 with a per-event override restoring 1.0 at first pitch, so
        the fee DOUBLES mid-market on a game that is already trading. On
        a $100 taker order at a price of 0.42 that is 203 bps before the
        first pitch and 406 bps after — enough to change which venue is
        cheaper for the same canonical outcome at the same price. Without
        this field a consumer mis-costs every order placed near a game
        start with no way to see it coming.
      properties:
        effective_at:
          type: string
          format: date-time
          description: When the new parameters take effect, RFC3339.
          example: "2026-08-22T00:15:00Z"
        taker:
          $ref: "#/components/schemas/MarketDetailFeeLeg"
        maker:
          $ref: "#/components/schemas/MarketDetailFeeLeg"

    EventResponse:
      type: object
      required:
        - event_id
        - provider
        - title
        - markets
      properties:
        event_id:
          type: string
          description: >
            Echo of the provider-native event identifier supplied in
            the request path.
          example: KXMLBGAME-26OCT152000MILLAD
        provider:
          type: string
          enum:
            - kalshi
            - polymarket
            - predict
            - sxbet
            - hyperliquid
            - alpha-arcade
            - prophetx
            - limitless
            - pred
          description: >
            The provider the event_id was resolved against, either
            inferred from the ID format or supplied via `?provider=`.
            It is the value to send as `?provider=` to
            `GET /v1/markets/{market_id}` with a nested market's
            `market_id` (it was named `platform` until 2026-10-09).
          example: kalshi
        title:
          type: string
          description: Human-readable event title from the provider.
          example: "Game 4: Milwaukee vs Los Angeles D"
        markets:
          type: array
          description: >
            Markets nested under this event. Order is provider-native
            for the primary event, followed by markets from fanout
            siblings (Kalshi sports) in registry order.
          items:
            $ref: "#/components/schemas/EventMarket"
        fanout:
          description: >
            Present when the response was assembled from multiple
            upstream events (Kalshi sports sibling fanout). Lists
            which sibling event tickers were attempted, which
            contributed markets, and which failed or didn't exist.
            Absent for non-Kalshi providers and for Kalshi events
            whose series is not in the sibling registry.
          $ref: "#/components/schemas/EventFanout"

    EventMarket:
      type: object
      required:
        - market_id
        - title
      properties:
        market_id:
          type: string
          description: >
            Provider-native market identifier. Kalshi ticker
            (`KXMLBGAME-26OCT152000MILLAD-MIL`), Polymarket numeric
            market id, SX Bet `marketHash`, Predict market id,
            Hyperliquid outcome id, AlphaArcade market ULID, or ProphetX
            `<event_id>:<market_id>`.
          example: KXMLBGAME-26OCT152000MILLAD-MIL
        title:
          type: string
          description: Human-readable market title/question.
          example: Milwaukee wins

    EventFanout:
      type: object
      required:
        - siblings_attempted
        - siblings_returned
        - siblings_missing
      properties:
        siblings_attempted:
          type: array
          items:
            type: string
          description: >
            Sibling event tickers the server attempted to fetch
            alongside the primary event. Determined by the Kalshi
            sibling registry for the primary event's series.
          example:
            - KXMLBSPREAD-26OCT152000MILLAD
            - KXMLBTOTAL-26OCT152000MILLAD
            - KXMLBTEAMTOTAL-26OCT152000MILLAD
            - KXMLBF3-26OCT152000MILLAD
            - KXMLBF5-26OCT152000MILLAD
            - KXMLBF5SPREAD-26OCT152000MILLAD
            - KXMLBF5TOTAL-26OCT152000MILLAD
            - KXMLBF7-26OCT152000MILLAD
            - KXMLBRFI-26OCT152000MILLAD
            - KXMLBEXTRAS-26OCT152000MILLAD
        siblings_returned:
          type: array
          items:
            type: string
          description: >
            Sibling event tickers that returned successfully and
            contributed markets to the merged response.
          example:
            - KXMLBEXTRAS-26OCT152000MILLAD
        siblings_missing:
          type: array
          items:
            type: string
          description: >
            Sibling event tickers that did not exist upstream (404)
            or errored. The primary event still returned 200 — these
            are reported for observability, not as request failures.
          example:
            - KXMLBF3-26OCT152000MILLAD
            - KXMLBF5-26OCT152000MILLAD
            - KXMLBF5SPREAD-26OCT152000MILLAD
            - KXMLBF5TOTAL-26OCT152000MILLAD
            - KXMLBF7-26OCT152000MILLAD
            - KXMLBRFI-26OCT152000MILLAD
            - KXMLBSPREAD-26OCT152000MILLAD
            - KXMLBTEAMTOTAL-26OCT152000MILLAD
            - KXMLBTOTAL-26OCT152000MILLAD

    ErrorResponse:
      type: object
      required:
        - code
        - message
        - status_code
      description: >
        The body of every error this API returns. Branch on `code`, which is
        stable; show `message`, which is written for people and may change.
        A `409` adds `candidates` (see `AmbiguousIdentifierError`) and a `402`
        adds the billing members of `PaymentRequiredError`.
      properties:
        code:
          type: string
          description: >
            Stable machine-readable reason. New codes may be added; a code is
            never renamed or reused. `400`: `unknown_parameter` (the endpoint
            does not read that query parameter), `invalid_parameter` (a value
            is malformed, empty, out of range, sent more than once, or
            contradicts another parameter), `missing_parameter`,
            `invalid_cursor` (a `cursor` that is malformed, stale, or from
            another endpoint or filter set: start again without
            it), `invalid_identifier` (a `market_id` or `event_id` that cannot
            exist on the provider it resolves to, or whose `{provider}:`
            prefix names no provider), `upstream_rejected` (the venue rejected
            the request). `401`: `missing_api_key`, `invalid_api_key`. `402`:
            `payment_required`. `403`: `forbidden`. `404`: `route_not_found`
            (no route matches the path), `market_not_found`,
            `event_not_found`, `profile_not_found`. `405`:
            `method_not_allowed`. `409`: `ambiguous_identifier`. `429`:
            `rate_limited` (the per-minute limit; honour `Retry-After`),
            `usage_limit_exceeded` (an allowance that does not refill within
            the minute), `upstream_rate_limited` (the venue's limit). `502`:
            `upstream_unavailable`. `503`: `service_unavailable`.
        message:
          type: string
          description: >
            What went wrong and how to fix it, for people. It quotes the
            offending value where there is one.
        status_code:
          type: integer
          format: int32
          description: The HTTP status code, repeated for logs that keep only the body.
        param:
          type: string
          description: >
            The query or path parameter the error concerns, when there is one
            (`provider`, `source_id`, `market_id`, …).
        errors:
          type: array
          description: >
            Every problem, when a request has more than one: a lookup batch
            with several bad identifiers, or several malformed parameters.
            `message` describes the first; each entry here names its own
            parameter and value.
          items:
            $ref: "#/components/schemas/ErrorParamProblem"

    ErrorParamProblem:
      type: object
      required:
        - param
        - value
        - message
      description: One problem with one parameter value.
      properties:
        param:
          type: string
        value:
          type: string
          description: The value as sent, truncated if long; empty for an empty value.
        message:
          type: string

    AmbiguousIdentifierError:
      type: object
      required:
        - code
        - message
        - status_code
        - candidates
      description: >
        Error body returned with HTTP 409. The identifier you sent is
        well-formed but not unique: it names a real resource on more
        than one provider, and nothing in the request says which one you
        meant. Polymarket and Predict share both the bare-numeric and
        kebab-case identifier shapes and their id spaces genuinely
        overlap, so this is a routine outcome rather than an edge case —
        measured 2026-08-25, 53 of 93 Predict market ids drawn from
        `GET /v1/matching-markets/sports` also resolved on Polymarket.


        `candidates` lists exactly the providers the identifier resolved
        on, in a stable order, and every entry is a legal `?provider=`
        value. Retry the same identifier with `?provider={candidate}`,
        or with the composite `{provider}:{id}` form, and the lookup is
        deterministic. Clients that fan out over identifiers should
        handle 409 by re-issuing with the provider they already know
        from the listing that produced the id — every list and matching
        response that emits an identifier also emits its provider.
      properties:
        code:
          type: string
          description: Always `ambiguous_identifier`.
        message:
          type: string
          description: >
            Human-readable detail naming the identifier, the providers it
            resolved on, and how to disambiguate.
        param:
          type: string
          description: The parameter holding the identifier, `market_id` or `event_id`.
        candidates:
          type: array
          description: >
            The providers this identifier resolved on. Each value is
            accepted verbatim by the `provider` query parameter.
          items:
            type: string
          example:
            - polymarket
            - predict
        status_code:
          type: integer
          format: int32
          example: 409

    PaymentRequiredErrorAction:
      type: string
      enum:
        - upgrade_plan
        - resolve_payment
      description: Recommended client action for an HTTP 402 response.

    PaymentRequiredError:
      type: object
      required:
        - code
        - message
        - status_code
        - action
        - required_tier
        - current_tier
      description: >
        Error body returned with HTTP 402. The `action` discriminator lets clients
        route to the correct recovery flow: `upgrade_plan` means the caller is on
        a lower tier than the endpoint requires, or the Free monthly allowance is
        exhausted; `resolve_payment` means the caller had a paid subscription
        that entered a payment-recovery state (past_due/unpaid/paused/incomplete)
        and the backend has already downgraded them to Free — the fix is in the
        billing portal, not a new purchase. `required_tier` / `current_tier` are
        always populated; `included_requests_per_month` and
        `current_period_requests` are only set when a Free caller hits the
        monthly allowance.
      properties:
        code:
          type: string
          description: Always `payment_required`.
        message:
          type: string
          description: What the caller needs to do, for people.
        status_code:
          type: integer
          format: int32
        action:
          $ref: "#/components/schemas/PaymentRequiredErrorAction"
        required_tier:
          type: string
          description: >-
            Billing tier that would satisfy the gate: `starter`, `pro` or
            `enterprise`, the `billing_tier` of a plan in `GET /v1/plans`
            (the Business plan's tier is `enterprise`).
        current_tier:
          type: string
          description: >-
            Billing tier currently associated with the caller: `free`,
            `starter`, `pro` or `enterprise`.
        included_requests_per_month:
          type: integer
          format: int64
          description: Monthly Free-tier allowance. Present only when the 402 is caused by the Free cap.
        current_period_requests:
          type: integer
          format: int64
          description: Requests the Free caller has made in the current calendar month. Present only when the 402 is caused by the Free cap.
