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

# Pre-game side observations

> List three groups of Polymarket sports markets that 0xinsider measures but never funds, with a per-sport account of what was eliminated.

Use this to see what gets eliminated before it can become a [pre-game side](/api-reference/endpoint/get-pre-game-sides). The three groups here are measured only: they never feed that route and never change it. Every row carries `observation_only: true`, and no row should ever reach an order executor.

`GET /api/v1/sports-edge-observations` is the deprecated spelling of this path. It stays live and answers the same body, and its responses carry `Deprecation` and `Link` headers that name this path as the successor.

## Parameters

| Parameter  | Description                                                                                                                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cohort`   | Required. Which of the 3 groups to read: `wider_holder`, `in_play`, or `emerging_pile`. Each one is described below.                                                                        |
| `category` | One of the 14 sport buckets. Leave it out to get all 14, including `Table Tennis` and `Pickleball`, which the funded route never covers. A value that is not a sport returns an empty list. |
| `limit`    | How many rows per page, from 1 to 100. The default is 20, and a value outside the range is clamped rather than refused.                                                                     |
| `cursor`   | The `next_cursor` from the previous response, which starts with `seo_v2_`. The server signs it, so an edited cursor is refused, and a cursor from one group cannot be used on another.      |

## The 3 groups

| `cohort`        | What it holds                                                                                                                                                                                                                                                                                               |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wider_holder`  | Markets before kickoff where graded wallets are concentrated on one side, but that the funded route did not admit. That includes markets its per-event and per-sport caps turned away.                                                                                                                      |
| `in_play`       | Games Polymarket confirms are live. When the live board, the holder data, or the one-way read is stale or missing, the row is dropped rather than guessed at.                                                                                                                                               |
| `emerging_pile` | The `wider_holder` rows whose `sharp_pct` is at least `0.75` and below `0.85`, whose `holder_scan_complete` is `true`, and whose kickoff is after a cutoff time. That cutoff is set when you ask for the first page and reused on every page after it. It is a view over the same snapshot, not extra work. |

## Key response fields

| Field                  | Meaning                                                                                                                                                                                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `observation_only`     | Always `true`. It marks the row as evidence, never as an instruction to trade.                                                                                                                                                                                 |
| `holder_scan_complete` | `false` means a Polymarket holder page hit its top-100 limit, so the graded total on that side is a floor rather than the real figure. A row like that can never support a capital decision.                                                                   |
| `holder_snapshot_at`   | The time Polymarket's holder data was observed, never the time it was read from cache. A `null` or stale value drops an `in_play` row.                                                                                                                         |
| `directional_status`   | `available`, `unknown_ungrouped`, `unknown_stale`, or `unavailable`. A `wider_holder` row is still returned when it is `unavailable`, and an `in_play` row is not.                                                                                             |
| `degraded`             | Covers the whole snapshot, not one row. `true` means an operational failure or an unknown completeness state left the evidence partial. A row counted as `capacity_limited` in the funnel does not on its own set it.                                          |
| `funnel.sports[]`      | One row per sport: 14 when you leave `category` out, 1 when you name a sport, and none when you name something that is not a sport. `reconciled` is `true` exactly when `input` equals `terminal_total`, and a mismatch is a fault rather than an empty group. |
| `snapshot_as_of`       | When the shared snapshot finished computing. A healthy `wider_holder` snapshot serves for about 180 seconds, and `in_play` never serves one older than about 30 seconds.                                                                                       |
| `meta.cost`            | `5` when the server computed a fresh snapshot, and `1` on a cache hit or an empty non-sport result.                                                                                                                                                            |

Within the group you asked for, rows are sorted by `directional_rank_score`, then `backing_score`, then `category`, then `condition_id`.

Each row names the heavy side as `side`, its grade-weighted score as `backing_score`, and its signed share of the graded dollars as `side_share`. The deprecated `piled_side`, `conviction_score`, and `smart_score` keys carry the same three values and stay in every response.

The size, price, and event fields on each row are the ones [Pre-game sides](/api-reference/endpoint/get-pre-game-sides) carries. The spec below lists every one of them.

## Errors that are not what they look like

| Answer                                         | Meaning                                                                                                                                                      |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `503` with `error.reason` `read_model_warming` | A refresh is already running, or the route's compute deadline of about 25 seconds ran out. Retry this one route after `Retry-After`. It is not a rate limit. |
| `503` with no `error.reason`                   | The rate limiter itself is unavailable.                                                                                                                      |
| `500` `internal_error`                         | An internal failure: a database error, a coordination failure, or a cursor the server could not sign.                                                        |
| `400` with `error.reason` `cursor_expired`     | The snapshot was replaced while you were paging. Start again from the first page.                                                                            |
| `400` with no `error.reason`                   | The request itself was wrong. An edited or unreadable cursor gets this answer, with `error.param` set to `cursor`.                                           |

## Example

```bash theme={null}
curl -H "Authorization: Bearer $OXINSIDER_API_KEY" \
  "https://api.0xinsider.com/api/v1/sports/pre-game-side-observations?cohort=wider_holder&category=Tennis&limit=20"
```

## What it does not return

* A funded signal. Every row is a market outside the funded list, and measuring it here changes nothing there.
* A market with zero or missing volume. It is counted as `invalid_market` in the funnel and never becomes a row.
* A game past kickoff that Polymarket has not confirmed is live. `in_play` drops it rather than guessing.
* Arrival history. `emerging_pile` is a view of the current `wider_holder` snapshot, not a log of when wallets arrived.

## Caching

Send the weak `ETag` from a response back in an `If-None-Match` header. If the page has not changed, you get `304 Not Modified` with an empty body. Both the `200` and the `304` carry `Cache-Control: private, no-cache`, so revalidate before you reuse a stored response.


## OpenAPI

````yaml GET /api/v1/sports/pre-game-side-observations
openapi: 3.1.0
info:
  x-generated-rate-limit-policy-from: web/src/lib/rate-limit-facts.ts via web/scripts/generate-api-policy.ts
  title: 0xinsider API
  description: >-
    Follow provider-exposed large-trade activity from Polymarket. Polymarket
    wallet-attributed trades can add grades, P&L, strategy, and diagnostic-score
    context when sufficient source data exists. Fields can be null or
    unavailable. Normal API requests use a 30-second server timeout that returns
    HTTP 408 Request Timeout with the standard error envelope (error.code
    request_timeout) when exceeded. Every /api/v1 failure answers that envelope,
    including a body that is not JSON or does not fit the request schema (400
    invalid_body), a query or path value that does not parse (400 invalid_query,
    invalid_path), a missing Content-Type: application/json (415
    unsupported_media_type), a body over 1048576 bytes (413 payload_too_large)
    and a method the path does not serve (405 method_not_allowed), each with
    error.param naming the field where one is known and meta.request_id equal to
    X-Request-Id. Unknown query names are ignored by default and reported in
    X-Query-Ignored, while X-Effective-Query lists the normalized names and
    values applied using form-urlencoded decoding, where + is a space; strict
    mode returns 400 bad_request with error.reason unknown_query_parameter
    before the handler runs, including for an unknown name with an incomplete
    percent escape. Public REST /api/v1/* endpoints, excluding /api/v1/mcp, use
    Bearer-token based non-credentialed browser CORS: any Origin may call with
    Authorization, Content-Type, If-None-Match, Idempotency-Key, Mcp-Session-Id,
    Mcp-Protocol-Version, Last-Event-Id, and X-Query-Validation request headers.
    X-Query-Validation: strict opts into rejecting unknown query names; the
    default remains compatible. Remote MCP at /api/v1/mcp is non-credentialed,
    but still validates Origin against the 0xinsider/localhost allowlist per MCP
    Streamable HTTP DNS-rebinding guidance. Successful browser CORS preflight
    responses advertise Access-Control-Max-Age: 86400. Browser JavaScript may
    read RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, the legacy
    X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After,
    ETag, X-Request-Id, X-Request-Cost, X-Usage-Accounting,
    X-Batch-RateLimit-Limit, X-Batch-RateLimit-Remaining,
    X-Batch-RateLimit-Reset, Mcp-Session-Id, X-Mcp-Error-Code, X-Query-Ignored,
    and X-Effective-Query response headers. Rate-limit headers describe the
    budget a request was counted against: the API key's per-minute window on an
    authenticated call, and the per-IP budget on a public route or on a refused
    credential (401, 402, 403, 423), so a client looping on a bad or lapsed key
    still sees how much room it has. Conditional GET: these operations return a
    weak ETag and answer If-None-Match with 304 Not Modified and an empty body:
    GET /api/v1/health, GET /api/v1/insider-radar, GET
    /api/v1/insider-radar/{id}, GET /api/v1/large-positions, GET
    /api/v1/leaderboard, GET /api/v1/leaderboard/trending, GET
    /api/v1/market/{condition_id}/candles, GET
    /api/v1/market/{condition_id}/flow, GET /api/v1/market/{condition_id}/intel,
    GET /api/v1/market/{condition_id}/snapshot, GET /api/v1/markets/explore, GET
    /api/v1/markets/sharp-money-flows, GET /api/v1/markets/smart-money-flows,
    GET /api/v1/pick-of-the-day, GET /api/v1/pick-of-the-day/archive, GET
    /api/v1/positions, GET /api/v1/sports-edge-observations, GET
    /api/v1/sports-edge-signals, GET /api/v1/trader/{address}, GET
    /api/v1/trader/{address}/context, GET /api/v1/trader/{address}/pnl, GET
    /api/v1/trader/{address}/position-timeline, GET
    /api/v1/traders/{trader}/position-timeline, GET /api/v1/whale-trades, GET
    /api/v1/whale-trades/history, GET /api/v1/whale-trades/{id}, GET
    /api/v1/whale-trades/{id}/counterparties/executions, GET
    /api/v1/whale-trades/{id}/counterparties/executions/{execution_id}/makers.
    Credentialed first-party routes such as /api/keys, /api/billing, and auth
    endpoints remain restricted to configured 0xinsider origins. Protected V1
    responses, except the zero-cost /api/v1/usage route, after handler execution
    carry X-Usage-Accounting: persisted, failed, or unknown. This reports the
    usage-record write; it does not change the handler result. Do not replay a
    successful mutation to repair an unknown usage record. Before execution,
    unavailable accounting capacity returns HTTP 503 with
    error.reason=request_accounting_unavailable; honor Retry-After. Public API
    responses add the browser-readable Server-Timing header: Processing time in
    milliseconds, for example api;dur=12.345. Includes API authentication, quota
    admission, handler work and response construction. Excludes network transit
    and streamed body or export-file transfer. The engineering budget is
    strictly below 250 ms; this header reports observations, not a latency
    guarantee or a new timeout.
  version: 1.0.0
  contact:
    name: 0xinsider
    email: support@0xinsider.com
    url: https://0xinsider.com
servers:
  - url: https://api.0xinsider.com
    description: >-
      Production (live data). Authenticate with a live key (oxi_sk_live_...);
      requires an active Pro subscription. A sandbox key (oxi_sk_test_...) is
      answered with 401 invalid_api_key and error.reason sandbox_api_key.
  - url: https://0xinsider.com/sandbox
    description: >-
      Sandbox. No credential required and no production data: every documented
      operation answers with its documented example or a deterministic sample of
      its response schema. GET /api/v1/stream is the one exclusion and answers
      400 there, because a Server-Sent Events stream is a live connection rather
      than a body. Add ?sandbox_status=<code> to receive one of the error
      responses the operation documents (for example 429 with Retry-After).
      Documented query parameters and JSON request bodies are checked against
      this document, the two context.md routes answer 200 text/markdown, GET
      /api/v1/trader/{address}/export/download answers its 302 with a Location
      the sandbox serves itself rather than an object store, and nothing is
      stored between requests. A sandbox key (oxi_sk_test_..., issued with no
      account by POST https://api.0xinsider.com/api/v1/agents/register) is
      optional: on an operation that requires a credential, a well-formed key is
      answered with X-Oxi-Sandbox-Key: valid and a malformed one with 401
      invalid_api_key.
security:
  - bearerAuth: []
  - oauth2:
      - read
tags:
  - name: Traders
    description: Traders, batch lookups, timelines, and export readiness.
  - name: Positions
    description: Current prediction-market position snapshots from backend-owned mirrors.
  - name: Large Positions
    description: Largest current open positions from graded traders (Polymarket-only).
  - name: Large trades
    description: Recent and historical large trades.
  - name: Leaderboard
    description: Ranked trader discovery and category/strategy leaderboards.
  - name: Pick of the Day
    description: >-
      One sourced sharp-money call a day: the side profitable wallets are
      backing, with pre-game odds, the holders, and the track record.
  - name: Games
    description: >-
      Sports and esports games: both sides, schedules, provider status and the
      Polymarket markets linked to each game.
  - name: Markets
    description: Market search, discovery, snapshots, and sharp-money flow.
  - name: Content
    description: Search across 0xinsider editorial content.
  - name: Suspicious trades
    description: Trades whose recorded suspicion score meets the live flag threshold.
  - name: Insider Radar
    description: >-
      Deprecated spelling of Suspicious trades; both operations stay live as
      aliases.
  - name: Events
    description: Durable public event replay streams.
  - name: Streaming
    description: Resumable real-time Server-Sent Events stream of live feed envelopes.
  - name: Webhooks
    description: Signed builder webhook destinations and delivery controls.
  - name: Usage
    description: Developer API budget and usage introspection.
  - name: Onboarding
    description: >-
      Self-serve agent registration: a sandbox key with no account, and the path
      to live access.
  - name: System
    description: Health and operational status checks.
  - name: MCP
    description: Remote Model Context Protocol transport.
  - name: Reports
    description: Daily, weekly, monthly, and trader export report snapshots.
  - name: Account
    description: Identify the account and credential authenticated for a paid API request.
externalDocs:
  description: 0xinsider API docs
  url: https://docs.0xinsider.com
paths:
  /api/v1/sports/pre-game-side-observations:
    get:
      tags:
        - Markets
      summary: List observation-only pre-game side cohorts
      description: >-
        Canonical since #16310; GET /api/v1/sports-edge-observations is its
        deprecated alias and serves the same body. Pro-tier. Measures three
        explicitly observation-only Polymarket sports cohorts without changing
        or feeding GET /api/v1/sports/pre-game-sides: wider_holder measures
        pre-game holder piles outside the funded route's exact raw signals
        admission, including recent-flow rows rejected by its event, bucket, or
        total caps; in_play admits provider-confirmed live games and fails
        closed when the provider live-board snapshot is stale or unavailable or
        holder/directional evidence is stale or unavailable; emerging_pile is an
        additive post-compute projection of wider_holder rows with finite
        sharp_pct in [0.75, 0.85), holder_scan_complete=true, and a kickoff
        after its pinned projection cutoff. emerging_pile overlaps wider_holder,
        remains attributed to that source terminal and denominator, and is not
        holder-arrival history. Every row carries observation_only=true,
        provider/holder freshness and completeness fields, and the response
        carries a required snapshot-wide operational/unknown-completeness
        degraded boolean plus an accountable per-sport funnel over a closed
        25-value terminal-reason vocabulary. Omitted or blank category selects
        all 14 registered observation sport buckets, including Table Tennis and
        Pickleball; those two remain outside the funded sports projection. All
        category and all-sports cache scopes share one global observation
        provider-work admission, so distinct scope keys cannot multiply
        concurrent provider fanout. One absolute ~25s compute deadline covers
        cache coordination, board/universe and primary-signals membership reads,
        holder cache/provider work, price/metadata evaluation, and directional
        reads, leaving ~5s below the public router timeout for funnel
        reconciliation, cleanup, and response transport. A shared pre-holder
        stage deadline jointly bounds those database reads and board
        reconciliation to the smaller of 12s or half of the absolute budget
        remaining when that stage starts, preserving holder/post-holder
        opportunity; board reads use bounded fair waves and cold holder
        admission gives each represented canonical sport one row before any
        sport repeats. A request that finds its scope's refresh in flight waits
        for that snapshot, and a scope refresh waits for another scope's global
        provider-work admission while at least ~15s of the absolute deadline
        remain. Absolute deadline exhaustion before a usable cache,
        provider-work admission still held at that ~15s bound, an in-flight
        refresh that ends without publishing while another waiting request takes
        it over, or pre-holder stage expiry before a usable stored universe or
        primary-signals membership result returns 503 with
        error.reason=read_model_warming. Category-resolution SQL errors, Redis
        coordination failures, observation-universe SQL errors, and
        primary-signals membership query failures return 500 internal_error
        instead. Once a usable universe exists, later operational or
        unknown-completeness board, holder, or price/metadata failures are
        retained as explicit terminal reasons in a degraded 200 response with
        degraded=true: board_source_unavailable is a completed board-source
        failure, board_deadline_unavailable means live-board work missed either
        an internal configured-scope deadline or the outer fair-wave deadline;
        both classify only already-started rows, so for the upcoming source read
        funnel.sports[].board_upcoming_status instead; provider_unavailable is
        an attempted holder-provider failure, and holder_deadline_unavailable is
        holder-stage deadline exhaustion. Directional incompleteness is
        cohort-specific: a wider_holder row remains emitted with terminal
        wider_holder_emitted and directional_status=unavailable, while an
        in_play row fails closed with terminal in_play_directional_unavailable.
        capacity_limited records intentional bounded provider-work admission in
        the funnel and does not by itself set degraded=true. Healthy
        wider_holder requests may reuse a snapshot for ~180s; healthy
        emerging_pile requests may use the same snapshot. in_play never serves a
        cached observation snapshot older than ~30s, and degraded snapshots use
        ~30s. The ETag is a weak semantic validator over the stable page
        payload, including next_cursor page position; request-specific meta
        excluded; the opaque emerging_pile projection cutoff in next_cursor is
        not itself a validator input. Rows carry the canonical field names side,
        backing_score and side_share; the older piled_side, conviction_score and
        smart_score keys carry the same values and stay on the wire.
      operationId: listPreGameSideObservations
      parameters:
        - name: X-Query-Validation
          in: header
          required: false
          description: >-
            Opt into strict query-name validation. The default is compatible:
            unknown names are ignored and reported in X-Query-Ignored. With
            strict, an unknown name returns 400 bad_request with error.reason
            unknown_query_parameter before the handler runs, including when its
            percent escape is incomplete.
          schema:
            type: string
            enum:
              - strict
        - name: cohort
          in: query
          required: true
          description: >-
            Observation cohort. wider_holder measures pre-game holder piles
            outside the funded route's exact raw signals admission. in_play
            admits only provider-confirmed live games and excludes stale or
            unavailable provider live-board, holder, or directional evidence.
            emerging_pile is a post-compute wider_holder projection for finite
            sharp_pct in [0.75, 0.85) with holder_scan_complete=true and a
            kickoff after its pinned projection cutoff; it overlaps
            wider_holder, is not an independent denominator, and is not arrival
            history.
          schema:
            type: string
            enum:
              - wider_holder
              - in_play
              - emerging_pile
        - name: category
          in: query
          description: >-
            Optional canonical sport bucket. Omitted or blank selects all
            registered sports. Raw provider categories resolve through the
            canonical taxonomy, including table-tennis or table tennis to Table
            Tennis and pickleball to Pickleball; a non-sport category returns an
            empty list.
          schema:
            type: string
        - name: limit
          in: query
          description: Page size.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: >-
            Server-authenticated opaque seo_v2_ cursor from next_cursor. Pins
            snapshot_as_of, cohort, rank, and condition_id; pre-deploy unsigned
            seo_ cursors are rejected, so clients must request the first page
            after this contract ships; emerging_pile cursors also pin the
            first-page projection_now cutoff so kickoff filtering cannot
            renumber continuation pages. Client edits fail closed; it cannot
            cross cohorts; a refreshed snapshot invalidates it with 400.
          schema:
            type: string
        - name: If-None-Match
          in: header
          required: false
          description: >-
            Conditional GET using a previous weak semantic ETag. Matching stable
            payloads return 304 Not Modified with an empty body;
            request-specific meta and the opaque emerging_pile projection cutoff
            inside next_cursor are excluded from validation.
          schema:
            type: string
      responses:
        '200':
          description: >-
            Observation-only sports cohort with a snapshot-wide
            operational/unknown-completeness degraded verdict and accountable
            funnel
          headers:
            X-Query-Ignored:
              $ref: '#/components/headers/X-Query-Ignored'
            X-Effective-Query:
              $ref: '#/components/headers/X-Effective-Query'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              description: >-
                Client caches may store the private response but must revalidate
                it before every reuse, independently of the server-side snapshot
                TTL.
              schema:
                type: string
                const: private, no-cache
            X-Usage-Accounting:
              $ref: '#/components/headers/X-Usage-Accounting'
            Server-Timing:
              $ref: '#/components/headers/Server-Timing'
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                  - snapshot_as_of
                  - degraded
                  - funnel
                  - meta
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PreGameSideObservation'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                  snapshot_as_of:
                    type: string
                    format: date-time
                    description: >-
                      Completion time of the shared observation snapshot pinned
                      by the cursor.
                  degraded:
                    type: boolean
                    description: >-
                      True when an operational failure or unknown
                      provider-board, holder, directional, reconciliation, or
                      internal completeness state made this snapshot partial.
                      This is snapshot-wide and can retain degradation that the
                      funnel's one-terminal-per-input accounting cannot
                      represent. Intentional bounded capacity_limited rows
                      remain fully accounted in the funnel and do not by
                      themselves set this field.
                  funnel:
                    $ref: '#/components/schemas/PreGameSideFunnelReport'
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '304':
          description: >-
            Not Modified. Returned when If-None-Match matches the current
            sports-edge observation payload.
          headers:
            X-Query-Ignored:
              $ref: '#/components/headers/X-Query-Ignored'
            X-Effective-Query:
              $ref: '#/components/headers/X-Effective-Query'
            ETag:
              $ref: '#/components/headers/ETag'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Cache-Control:
              description: >-
                Client caches may store the private response but must revalidate
                it before every reuse, independently of the server-side snapshot
                TTL.
              schema:
                type: string
                const: private, no-cache
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-Usage-Accounting:
              $ref: '#/components/headers/X-Usage-Accounting'
            Server-Timing:
              $ref: '#/components/headers/Server-Timing'
          x-empty-body: true
        '400':
          description: >-
            Bad request. On this cursor-paginated route a 400 has TWO distinct
            causes; branch on error.reason. (1) error.reason="cursor_expired"
            (with error.param="cursor"): the pagination cursor was invalidated
            by an upstream data change mid-walk (e.g. the ranking snapshot
            behind the page refreshed). It is NOT a malformed parameter and NOT
            a reason to stop: recovery is mechanical -- re-request the first
            page and walk forward again. There is deliberately no Retry-After
            and no error.retry_at, because waiting changes nothing. (2) no
            error.reason: an ordinary invalid request parameter -- check
            error.param when present, otherwise error.message. Both carry
            error.code="bad_request" (a FROZEN contract value), so error.reason
            is the discriminator.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '402':
          description: >-
            Active Pro subscription required. The key is valid but the account
            has no active Pro subscription; error.reason is
            subscription_inactive and error.message names the reactivation URL
            (https://0xinsider.com/billing). Permanent until a person
            reactivates: no Retry-After, never retry on a schedule.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: Account access denied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '408':
          description: >-
            The handler did not answer inside the server's 30-second timeout.
            error.code is request_timeout. On GET and HEAD the response carries
            Retry-After and error.retry_at; on a mutation it carries neither,
            because the request may have completed on the server: check its
            state before repeating it, and reuse its Idempotency-Key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '423':
          description: Account is locked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: >-
            Rate limit exceeded. Three independent budgets. (1) 100
            requests/minute per user (sliding window), on every authenticated
            route. (2) On the BATCH routes only: 2500 batch item units/minute
            per user, reserved before any item is executed. A batch with N
            requested items costs N item units, including duplicate and invalid
            items. 2500 = 100 requests x 25 items per batch, which is the most
            item work a key can buy through the request limiter at all: a caller
            may spend their entire 100-request minute on full 25-item batches
            without the item budget being what stops them. The REQUEST budget is
            the effective ceiling, and batching is never the more expensive
            choice. The item budget can still deny at a sliding-window boundary
            (both counters carry the previous window forward with a floor, and
            the item counter runs 25x the request counter), so honor a 429 from
            either. Over-quota batches return 429 with Retry-After plus
            RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset before any
            item work is done. (3) The monthly quota: Pro includes 250,000
            authenticated requests per UTC calendar month, whatever the billing
            cadence. Over 250,000: with pay as you go on, requests keep
            answering and the excess bills at USD 0.20 per 1,000 on a monthly
            invoice, up to 1,000,000 requests a month; without it, from 1
            October 2026 the next request answers 429 rate_limited with
            error.reason monthly_quota_exceeded and a Retry-After to the month's
            reset, and from the same day a pay-as-you-go account answers the
            same past 1,000,000. A refused request is not counted. Every
            authenticated response carries X-Monthly-Quota-Limit,
            X-Monthly-Quota-Remaining, and X-Monthly-Quota-Reset (unix seconds,
            the first of next month). (4) The per-address budget: 1200
            requests/minute per IP, shared by every caller behind one address
            and counted before authentication, on every route. A 429 from it
            carries error.reason ip_rate_limited and describes that bucket in
            RateLimit-*; a throttled address (sustained over-limit traffic)
            carries error.reason ip_throttled with a Retry-After of minutes to
            days, and a request before it does not shorten the cooldown. Every
            429 is the standard error envelope with meta.request_id equal to
            X-Request-Id.
          headers:
            Retry-After:
              description: Seconds until rate limit resets.
              schema:
                type: integer
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            X-RateLimit-Reset:
              schema:
                type: integer
            X-Request-Id:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '500':
          description: Unexpected server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '503':
          description: >-
            Service unavailable. On this route a 503 has TWO distinct causes;
            branch on error.reason. (1) error.reason="read_model_warming": the
            requested endpoint cannot serve its read model yet. Exact causes are
            endpoint-specific and can include a cold or contended refresh or a
            dependency that prevented refresh; consult that endpoint's contract
            and do not infer dependency health from this shared reason. This is
            endpoint-local unavailability, not rate limiting: retry only this
            route after Retry-After (or error.retry_at), and do not feed it into
            a rate-limit backoff shared with other endpoints. (2) no
            error.reason: the Redis-backed authenticated rate limiter is
            unavailable and the middleware failed closed; Retry-After is the
            seconds until it probes Redis again. Both carry
            error.code="rate_limit_unavailable" (a FROZEN contract value, so it
            cannot be split per cause) and X-Request-Id -- which is why
            error.reason, not error.code, is the discriminator.
          headers:
            Retry-After:
              description: >-
                Seconds until retrying is worth doing. With
                error.reason="read_model_warming" this is the endpoint-local
                read-model retry interval; without a reason it is the
                rate-limiter outage cooldown. Always >= 1.
              schema:
                type: integer
            X-Request-Id:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -sS \
              -H "Authorization: Bearer $OXINSIDER_API_KEY" \
              'https://api.0xinsider.com/api/v1/sports/pre-game-side-observations?cohort=wider_holder&category=Tennis&limit=20'
components:
  headers:
    X-Query-Ignored:
      description: >-
        Comma-separated, percent-encoded query names the operation did not
        publish and therefore ignored in compatible mode. Names are sorted and
        de-duplicated.
      schema:
        type: string
    X-Effective-Query:
      description: >-
        Normalized, percent-encoded query string containing only the recognized
        names and values applied by the operation. Query names and values use
        form-urlencoded decoding, where + is a space. Repeated names are
        retained and sorted by name.
      schema:
        type: string
    RateLimit-Limit:
      description: >-
        Request limit of the budget this request was counted against, for its
        current window: the API key's per-minute sliding window on an
        authenticated call; the per-IP budget on a public route and on a refused
        credential (401, 402, 403, 423), which never reaches the per-key
        limiter. Standard RateLimit header spelling.
      schema:
        type: integer
        example: 100
    RateLimit-Remaining:
      description: >-
        Requests remaining in that budget's current window after this response.
        Standard RateLimit header spelling.
      schema:
        type: integer
        example: 84
    RateLimit-Reset:
      description: >-
        Seconds until that budget's current window resets. Standard RateLimit
        header spelling.
      schema:
        type: integer
        example: 42
    X-RateLimit-Limit:
      description: >-
        Request limit of the budget this request was counted against, for its
        current window: the API key's per-minute sliding window on an
        authenticated call; the per-IP budget on a public route and on a refused
        credential (401, 402, 403, 423).
      schema:
        type: integer
        example: 100
    X-RateLimit-Remaining:
      description: Requests remaining in that budget's current window after this response.
      schema:
        type: integer
        example: 84
    X-RateLimit-Reset:
      description: Unix timestamp when that budget's current window resets.
      schema:
        type: integer
        example: 1710772860
    X-Request-Id:
      description: >-
        Server-generated request identifier for support and tracing. On every
        /api/v1 response, including 304, 408, CORS preflights and every error,
        and always equal to meta.request_id in the body. It is the key of the
        request's usage accounting row and of every log line the request
        emitted, so quote either form to support. A client-supplied X-Request-Id
        request header is ignored: the value is never adopted or echoed.
      schema:
        type: string
        example: req_550e8400
    ETag:
      description: >-
        Weak semantic validator for conditional GET. Request-specific response
        metadata is excluded; send as If-None-Match to receive 304 when the
        stable payload is unchanged.
      schema:
        type: string
        example: W/"8f14e45fceea167a5a36dedd4bea2543"
    X-Usage-Accounting:
      description: >-
        Usage-record persistence for a protected V1 handler response. persisted:
        confirmed row; failed: write failed; unknown: completion could not be
        confirmed. Independent of handler success; do not replay successful
        mutations to repair accounting. Absent before accounting admission and
        on public routes. The zero-cost /api/v1/usage route also omits it.
      schema:
        type: string
        enum:
          - persisted
          - failed
          - unknown
    Server-Timing:
      description: >-
        Processing time in milliseconds, for example api;dur=12.345. Includes
        API authentication, quota admission, handler work and response
        construction. Excludes network transit and streamed body or export-file
        transfer. The engineering budget is strictly below 250 ms; this header
        reports observations, not a latency guarantee or a new timeout.
      schema:
        type: string
      example: api;dur=12.345
  schemas:
    PreGameSideObservation:
      type: object
      description: >-
        One explicitly observation-only holder-pile measurement. It is evidence
        for cohort evaluation, not an execution instruction, and is isolated
        from the funded sports-edge-signals route.
      required:
        - condition_id
        - token_id
        - category
        - raw_category
        - title
        - event_slug
        - event_id
        - parent_event_id
        - game_start_time
        - observed_at
        - cohort
        - observation_only
        - piled_side
        - piled_outcome_index
        - backed_price
        - sharp_pct
        - backed_sharp_usd
        - s_count
        - a_count
        - b_count
        - graded_holders
        - top_grade
        - smart_score
        - volume
        - conviction_score
        - provider_read_source
        - holder_scan_complete
        - holder_snapshot_at
        - directional_status
        - one_way_holder_count
        - hedged_holder_count
        - one_way_graded_usd
        - directional_confidence
        - directional_rank_score
        - rank
        - side
        - backing_score
        - side_share
      properties:
        side:
          type: string
          nullable: true
          description: >-
            The side profitable wallets hold, as a provider-backed display
            label. Canonical spelling of piled_side (#16310), same value: when
            provider group context is unavailable it may remain a bare
            Yes/No/Over/Under, so do not use it alone as participant identity.
        backing_score:
          type: number
          minimum: 0
          description: >-
            Grade-weighted holder-pile score before directional enrichment.
            Canonical spelling of conviction_score (#16310), same value.
        side_share:
          type: number
          minimum: -1
          maximum: 1
          description: >-
            Signed share of graded money on the side, in [-1, 1]. Canonical
            spelling of smart_score (#16309, #16310), same value.
        condition_id:
          type: string
          description: Raw Polymarket condition id.
        token_id:
          type: string
          description: >-
            Provider-backed Polymarket CLOB token id for the piled outcome. Rows
            without a verified token terminate before emission.
        category:
          type: string
          enum:
            - Basketball
            - Football
            - Baseball
            - Hockey
            - MMA
            - Boxing
            - Soccer
            - Cricket
            - Golf
            - Tennis
            - Esports
            - Racing
            - Table Tennis
            - Pickleball
          description: Canonical sport bucket.
        raw_category:
          type: string
          nullable: true
          description: Raw provider category as stored.
        title:
          type: string
          description: Provider-backed market title.
        event_slug:
          type: string
          nullable: true
        event_id:
          type: string
          nullable: true
          description: Provider event id when available.
        parent_event_id:
          type: string
          nullable: true
          description: >-
            Provider parent-event id used as the first event-cap identity when
            available.
        game_start_time:
          type: string
          format: date-time
          description: Provider-backed kickoff time in UTC.
        observed_at:
          type: string
          format: date-time
          description: UTC instant when this row finished provider/holder evaluation.
        cohort:
          type: string
          enum:
            - wider_holder
            - in_play
            - emerging_pile
          description: >-
            Source cohort or additive projection view. emerging_pile is
            projected from wider_holder after source computation, overlaps its
            funnel denominator, and uses the source row's directional evidence.
        observation_only:
          type: boolean
          const: true
          description: Always true. This row must not be routed to an order executor.
        piled_side:
          deprecated: true
          type: string
          nullable: true
          description: >-
            Nullable provider-backed piled-outcome display label. When provider
            group context is unavailable, it may remain a bare
            Yes/No/Over/Under; do not use it alone as participant identity.
            Deprecated (#16310): `side` is the canonical spelling and carries
            the same value; this key stays on the wire.
        piled_outcome_index:
          type: integer
          enum:
            - 0
            - 1
          description: >-
            Provider binary-column selector: 0 selects outcome_yes/token_id_yes;
            1 selects outcome_no/token_id_no. It does not identify home/away or
            a participant. Use piled_side together with title/event context for
            display.
        backed_price:
          type: number
          minimum: 0
          maximum: 1
          description: >-
            Provider-backed implied price for the piled outcome at observation
            time.
        sharp_pct:
          type: number
          minimum: 0
          maximum: 1
          description: Piled-side graded-holder dollar concentration.
        backed_sharp_usd:
          type: number
          minimum: 0
          description: Raw graded-holder USD on the piled outcome.
        s_count:
          type: integer
          minimum: 0
        a_count:
          type: integer
          minimum: 0
        b_count:
          type: integer
          minimum: 0
        graded_holders:
          type: integer
          minimum: 1
          description: Piled-side S/A/B holder count.
        top_grade:
          type: string
          enum:
            - S
            - A
            - B
        smart_score:
          deprecated: true
          type: number
          minimum: -1
          maximum: 1
          description: >-
            Canonical signed holder-pile score. Deprecated (#16310):
            `side_share` is the canonical spelling and carries the same value;
            this key stays on the wire.
        volume:
          type: number
          exclusiveMinimum: 0
          description: >-
            Strictly positive stored market volume in USD. Missing, zero, or
            non-finite volume terminates as invalid_market and is never emitted
            as an observation.
        conviction_score:
          deprecated: true
          type: number
          minimum: 0
          description: >-
            Grade-weighted holder-pile score before directional enrichment.
            Deprecated (#16310): `backing_score` is the canonical spelling and
            carries the same value; this key stays on the wire.
        provider_read_source:
          type: string
          enum:
            - cached
            - live
          description: >-
            Whether the provider holder page came from the shared cache or a
            live provider read.
        holder_scan_complete:
          type: boolean
          description: >-
            True only when neither provider outcome holder page hit the top-100
            scan bound. False means the pile is a positive lower bound and
            cannot satisfy a future capital-promotion gate.
        holder_snapshot_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Proven provider holder observation time. A warm cache hit uses only
            the original provider completion time from its companion metadata,
            never cache-read time. Null, malformed, future, or stale holder time
            fails in-play closed.
        directional_status:
          type: string
          enum:
            - available
            - unknown_ungrouped
            - unknown_stale
            - unavailable
          description: >-
            Truthful state of the directional read, which classifies each graded
            holder by its fresh synced legs across the game's markets and, when
            holder_scan_complete is true, by Polymarket's currentValue on both
            outcomes of this market. A wider_holder row can remain emitted with
            unavailable and terminal wider_holder_emitted; in_play fails closed
            instead and terminates as in_play_directional_unavailable.
        one_way_holder_count:
          type: integer
          nullable: true
          minimum: 0
        hedged_holder_count:
          type: integer
          nullable: true
          minimum: 0
        one_way_graded_usd:
          type: number
          nullable: true
          minimum: 0
        directional_confidence:
          type: number
          nullable: true
          minimum: 0
          maximum: 1
        directional_rank_score:
          type: number
          minimum: 0
          description: >-
            Default cohort ordering key: conviction_score * (1 + 0.25 *
            directional_confidence), or conviction_score when confidence is
            null.
        rank:
          type: integer
          minimum: 1
          description: 1-based rank within this observation cohort and snapshot.
    PreGameSideFunnelReport:
      type: object
      description: >-
        Per-sport accountable funnel for the full observation snapshot, returned
        on every page.
      required:
        - sports
      properties:
        sports:
          type: array
          items:
            $ref: '#/components/schemas/PreGameSideSportFunnelReport'
    ResponseMeta:
      type: object
      required:
        - request_id
        - cached
        - cost
      properties:
        request_id:
          type: string
          description: >-
            Unique request ID (req_ prefix). The same value as the X-Request-Id
            response header, the request's usage accounting row and its log
            lines.
        cached:
          type: boolean
        cache_age_s:
          type: integer
          description: >-
            Cache age in seconds. Omitted when the response was not cached, and
            also when it was cached but its age cannot be established (an entry
            stored before its cache carried a computed instant). Never a
            placeholder: an unknown age is reported as no value rather than as
            the cache TTL.
        cost:
          type: integer
          description: >-
            Advisory request weight (relative compute cost). 1 for simple reads;
            higher for heavier endpoints. Not a credit/price.
        ranking_generation:
          type: integer
          description: >-
            Committed PostgreSQL-owned leaderboard generation for the returned
            rows and cursor. Present on GET /api/v1/leaderboard; omitted on
            endpoints that do not read this ranking.
        ranking_as_of:
          type: string
          format: date-time
          description: >-
            Authoritative RFC3339 timestamp from cache_generations.updated_at
            for ranking_generation. It is read in the same repeatable-read
            snapshot as the leaderboard rows and is not request time, cache
            write time, or row insertion order.
        directional_source:
          type: string
          enum:
            - live
            - degraded
          description: >-
            Which path produced the team-directional read on this response. Only
            present on endpoints that compute one (today: GET
            /api/v1/sports-edge-signals). "live" means the read RAN. "degraded"
            means it FAILED, so nothing was measured and the ranking fell back
            to raw conviction. The flag describes the READ, not its consequence:
            a read that ran and found nothing groupable also leaves the
            directional fields null, and that is honestly "live" -- the
            per-signal nulls already say "nothing to enrich here", so this
            snapshot-level flag carries only what they cannot, namely whether
            the read ran at all. A degraded response is cached on the shorter
            degraded TTL so it self-heals. Reported SEPARATELY from
            ranking_source because the two degradations are independent -- a
            sharp-money DB miss weakens the ranking DATA, a directional failure
            removes a ranking WEIGHT -- and a consumer down-weighting a degraded
            response needs to know which input it lost. Omitted on endpoints
            that compute no directional read.
        ranking_source:
          type: string
          enum:
            - live
            - db_only
          description: >-
            Which ranking-data path produced this response. Only present on
            endpoints that can degrade a ranking (today: GET
            /api/v1/sports-edge-signals). "live" is the normal path (the current
            holder pile from the provider batch); "db_only" is the degraded
            fallback (a truthful but weaker trader_markets ranking) served when
            the live sharp-money ranking batch is unavailable (a sharp-money DB
            read failure, not a Polymarket outage) and cached on a shorter TTL,
            so a consumer can down-weight or skip it. Omitted on endpoints that
            never degrade.
        category_skill_source:
          type: string
          enum:
            - live
            - partial
            - degraded
            - unavailable
          description: >-
            Whole filtered snapshot category-evidence status before pagination.
            Operational live always remains partial source coverage.
        category_skill_model_version:
          type: string
        category_skill_taxonomy_version:
          type: string
        category_skill_platform:
          type: string
          const: polymarket
        category_skill_scope:
          type: string
          const: observed_goldsky_primary_taker_fill
        category_skill_source_coverage:
          type: string
          enum:
            - partial_whale_threshold_fills
            - graded_wallet_fills
        category_skill_observation_started_at:
          type: string
          format: date-time
        category_skill_model_operationally_degraded:
          type: boolean
          description: >-
            Whole-model operational readiness captured with the category model
            snapshot. Present on category-enriched responses even when the
            filtered signal list is empty. When true, category_skill_source is
            degraded and sports-edge-signals uses the shorter degraded cache
            TTL.
        category_skill_status_counts:
          type: object
          required:
            - live
            - insufficient
            - stale
            - unknown
            - degraded
          properties:
            live:
              type: integer
              minimum: 0
            insufficient:
              type: integer
              minimum: 0
            stale:
              type: integer
              minimum: 0
            unknown:
              type: integer
              minimum: 0
            degraded:
              type: integer
              minimum: 0
        category_skill_base_payload_hash:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: >-
            SHA-256 of the funded signal membership/order/rank/cursor vector
            immediately before category-skill enrichment. Sports-edge-signals
            only.
        category_skill_enriched_base_payload_hash:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: >-
            Independent SHA-256 recomputation over the same base fields
            immediately after category-skill enrichment. Equality with
            category_skill_base_payload_hash proves shadow enrichment did not
            change funded inputs. Sports-edge-signals only.
    ApiError:
      type: object
      required:
        - object
        - error
        - meta
      properties:
        object:
          type: string
          const: error
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: >-
                FROZEN: an existing value never changes meaning. request_timeout
                (408, #16146) was added the way insufficient_scope was: the
                handler did not answer inside the server's 30-second timeout.
                Retry-After and retry_at ride on it only for a safe method (GET,
                HEAD); a timed-out mutation may have completed, so check its
                state and reuse its Idempotency-Key.
              enum:
                - bad_request
                - invalid_api_key
                - subscription_required
                - forbidden
                - insufficient_scope
                - not_found
                - account_locked
                - rate_limited
                - rate_limit_unavailable
                - internal_error
                - request_timeout
            message:
              type: string
            doc_url:
              type: string
            param:
              type: string
            retry_at:
              type: string
              format: date-time
              description: >-
                The recommended next request instant (RFC3339), always in the
                future. Present on every retryable error: `pick_not_released`,
                `rate_limited`, `rate_limit_unavailable`, and
                `read_model_warming`. Omitted otherwise. The absolute twin of
                `Retry-After`; prefer the header for the sleep duration. For
                `pick_not_released`, the earliest of the next scheduled release,
                the next automatic selector attempt, the operating-window start,
                or about 60 seconds. See that response.
            freshness:
              $ref: '#/components/schemas/FreshnessFailure'
            reason:
              type: string
              enum:
                - cursor_expired
                - unknown_endpoint
                - pick_not_released
                - trader_not_tracked
                - read_model_warming
                - database_unavailable
                - request_accounting_unavailable
                - idempotency_in_progress
                - webhook_delivery_in_progress
                - webhook_secret_rotation_not_prepared
                - webhook_secret_rotation_overlap_active
                - sandbox_api_key
                - api_key_in_query
                - subscription_inactive
                - monthly_quota_exceeded
                - invalid_query
                - unknown_query_parameter
                - invalid_path
                - invalid_body
                - unsupported_media_type
                - payload_too_large
                - method_not_allowed
                - ip_rate_limited
                - ip_throttled
                - export_expired
                - freshness_ceiling_unsatisfied
              description: >-
                ADDITIVE (#7209). The specific, actionable cause behind `code`,
                when there is one more specific than the code itself. `code`
                keeps its published values, so existing clients are unaffected;
                new clients branch on `reason`. Omitted when the code already
                says everything we know. pick_not_released: no Pick of the Day
                is published for the current product day; schedule one request
                against retry_at instead of polling. unknown_endpoint: the PATH
                is not a route on this API -- read GET /api/v1, do not retry.
                trader_not_tracked: the wallet is real and the URL is right, but
                the trader is outside the HOT/WARM sync tiers -- stop asking for
                this wallet. cursor_expired: pagination went stale mid-walk --
                re-request the first page and continue. read_model_warming: the
                requested endpoint cannot serve its read model yet; exact causes
                are endpoint-specific and can include a cold or contended
                refresh or a dependency that prevented refresh.
                database_unavailable: the API's database or its connection pool
                is temporarily unreachable (a connection-class failure, not a
                query fault); code stays rate_limit_unavailable, nothing is
                rate-limited, retry after Retry-After / retry_at.
                idempotency_in_progress: retain the exact Idempotency-Key and
                request body, then retry shortly. webhook_delivery_in_progress:
                retry the URL or signing-secret configuration change after the
                destination's active request completes.
                request_accounting_unavailable: accounting capacity is
                unavailable before the handler executes; retry after Retry-After
                / retry_at. sandbox_api_key: the credential is a sandbox key
                (oxi_sk_test_) from POST /api/v1/agents/register, which only the
                sandbox server accepts -- call the sandbox base URL with it, or
                get a live key or OAuth access token; do not retry it here.
                api_key_in_query: the key was sent as a ?token= query parameter,
                which no route reads because URLs land in logs and history; the
                key itself was not checked -- resend it as Authorization:
                Bearer. subscription_inactive: the key is valid but the
                account's Pro subscription has lapsed (402
                subscription_required); permanent until a person reactivates at
                https://0xinsider.com/billing, which the message names -- stop
                retrying on a schedule and surface the link. The key owner is
                emailed once per lapse. monthly_quota_exceeded: the account has
                used the requests Pro includes for the UTC calendar month (429
                rate_limited); retry_at and Retry-After name the first of next
                month, the only retry that can succeed, and the message names
                https://0xinsider.com/developers, where pay as you go for
                requests over the quota is turned on. The X-Monthly-Quota-Limit,
                X-Monthly-Quota-Remaining and X-Monthly-Quota-Reset headers on
                every authenticated response say how close the account is.
                invalid_query, invalid_path, invalid_body (400 bad_request,
                #16146): a query parameter, a path segment or the JSON body did
                not parse or does not fit the route's schema, so no handler ran;
                param names the field when the parser named one (a query key, a
                path segment, a JSON path such as traders[0], or body); fix the
                request, never retry it as sent. unsupported_media_type (415
                bad_request, param content-type): send the body with
                Content-Type: application/json. payload_too_large (413
                bad_request, param body): the body is over 1048576 bytes.
                method_not_allowed (405 bad_request): the path is a route but
                not with this method; the Allow header names the methods it
                serves. ip_rate_limited (429 rate_limited, #16380): the
                per-address budget every caller behind one IP shares, counted
                before authentication, is spent; not the key's own window, and
                the RateLimit-* headers describe that bucket. ip_throttled (429
                rate_limited): the address is in a cooldown after sustained
                over-limit traffic; Retry-After is minutes to days, and a
                request before it does not shorten the cooldown.
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    PreGameSideSportFunnelReport:
      type: object
      description: >-
        Independent sports-board supply plus stored-universe terminal accounting
        for one canonical sport.
      required:
        - sport
        - board_input
        - board_live_available
        - board_upcoming_configured
        - board_upcoming_available
        - board_upcoming_status
        - board_upcoming_unavailable_scopes
        - input
        - terminals
        - terminal_total
        - reconciled
      properties:
        sport:
          type: string
          enum:
            - Basketball
            - Football
            - Baseball
            - Hockey
            - MMA
            - Boxing
            - Soccer
            - Cricket
            - Golf
            - Tennis
            - Esports
            - Racing
            - Table Tennis
            - Pickleball
        board_input:
          type: integer
          minimum: 0
          description: >-
            Unique condition ids independently visible on the provider-first
            sports board.
        board_live_available:
          type: boolean
          description: >-
            Whether the always-applicable live-board source completed as
            available. False can mean a completed source failure
            (board_source_unavailable) or live-board work missing an internal
            configured-scope deadline or the outer fair-wave deadline
            (board_deadline_unavailable); inspect terminals to distinguish them.
        board_upcoming_configured:
          type: boolean
          description: >-
            Whether a provider-backed upcoming-board source is configured and
            applicable for this sport. False means not applicable, not provider
            failure.
        board_upcoming_available:
          type: boolean
          description: >-
            Whether every configured upcoming-board scope completed as
            available. False with board_upcoming_configured=false means not
            applicable. When board_upcoming_configured is true and this flag is
            false, read board_upcoming_status for the cause (it reads unknown,
            i.e. no recorded cause, only on a snapshot cached before that field
            existed, which self-clears within one TTL): the
            board_source_unavailable and board_deadline_unavailable terminals
            are assigned only to already-started rows (the live half) and are
            structurally 0 for the upcoming source, so they never explain this
            flag.
        board_upcoming_status:
          type: string
          enum:
            - unknown
            - not_configured
            - available
            - capacity_limited
            - source_unavailable
            - cold_unavailable
            - deadline_unavailable
          description: >-
            Why the upcoming-board source is (un)available. Board supply is one
            canonical-sport union: the bare category owns live truth and every
            configured composed league scope contributes upcoming rows; folded
            leagues without a configured board (currently NCAAB and CFL) are not
            in the upcoming union. available: every configured scope completed
            truthfully (a successful empty schedule still counts).
            capacity_limited: provider pagination or a configured upcoming cache
            published an intentionally bounded complete-event prefix; those rows
            are excluded from diagnostic-universe input and must not be treated
            as a complete upcoming universe. Cache-bound prefixes are limited to
            3,000 rows or an exact 5,000,000-byte final envelope.
            source_unavailable: composition failed before a truthful union;
            resolved scope readers turn half failures into cold_unavailable, so
            fresh producers are whole-union identity reconciliation or a
            registry contract failure and carry zero rows. cold_unavailable:
            every scope completed but at least one reported its upcoming half
            unavailable because no servable entry was inside the stale-serve
            bound and the background warm did not land in time; it is not by
            itself proof of a provider outage. deadline_unavailable: an internal
            configured-scope deadline or the outer bounded fair wave expired. An
            internal deadline may retain healthy bare-category or sibling-scope
            rows; the outer wave records zero rows. These upcoming fields do not
            describe league live-membership availability. If the bare-category
            live scope fails, all live rows are dropped even when a league scope
            completed, because the bare category is the sole live-truth owner.
            not_configured: no upcoming scope applies to the sport. unknown:
            exactly one cause -- a snapshot cached before this field existed
            whose legacy flags recorded an unavailable-but-configured half
            without saying why. Every freshly computed snapshot reports a
            concrete status, and a legacy available or not-configured row is
            reconstructed exactly, so unknown self-clears within one TTL.
            board_upcoming_available is exactly board_upcoming_status ==
            available.
        board_upcoming_unavailable_scopes:
          type: array
          description: >-
            Configured upcoming scopes that did not complete as available.
            Values are category, a provider league tag slug (nfl, cfb, nba,
            wnba, nhl, mls, valorant, league-of-legends, counter-strike-2, or
            dota-2), registry when the compiled scope/projection contract
            drifted, union when cross-scope identity reconciliation failed, or
            wave when the outer fair-wave deadline expired before scope-level
            evidence returned. Empty means no unavailable upcoming scope was
            identified; this includes healthy/not-configured rows and a legacy
            cached row. Observation league scopes are upcoming-only and perform
            no live-membership read.
          items:
            type: string
            enum:
              - category
              - nfl
              - cfb
              - nba
              - wnba
              - nhl
              - mls
              - valorant
              - league-of-legends
              - counter-strike-2
              - dota-2
              - registry
              - union
              - wave
          uniqueItems: true
        input:
          type: integer
          minimum: 0
          description: Stored-universe rows plus provider-board rows missing from storage.
        terminals:
          type: object
          description: >-
            Sparse counts over the closed 25-value terminal vocabulary:
            outside_horizon, resolved, provider_closed, provider_excluded,
            invalid_market, missing_token, missing_stored_market,
            not_provider_live, board_source_unavailable,
            board_deadline_unavailable, primary_slate_candidate,
            zero_indexed_holder_research, capacity_limited,
            provider_unavailable, holder_deadline_unavailable,
            holder_computation_unavailable, holder_scan_incomplete,
            no_current_graded_holder, split_holder_pile, price_unavailable,
            wider_holder_emitted, in_play_emitted, in_play_stale_observed,
            in_play_directional_unavailable, or internal_unclassified.
            primary_slate_candidate means exact admission by the funded route's
            raw shared signals query before provider/holder enrichment;
            recent-flow rows rejected by its event, bucket, or total caps remain
            eligible for wider_holder measurement. capacity_limited is
            intentional bounded provider-work admission, is fully accounted
            here, and does not itself set degraded=true.
            board_source_unavailable means a completed board source was
            unavailable; board_deadline_unavailable means live-board work missed
            either an internal configured-scope deadline or the outer fair-wave
            deadline; provider_unavailable means an attempted holder-provider
            read failed; holder_deadline_unavailable means holder cache/provider
            work missed the absolute request deadline;
            holder_computation_unavailable means post-holder provider or
            DB-backed price/metadata evaluation was unavailable.
          propertyNames:
            $ref: '#/components/schemas/PreGameSideObservationTerminalReason'
          additionalProperties:
            type: integer
            minimum: 0
        terminal_total:
          type: integer
          minimum: 0
          description: Sum of every sparse terminal count.
        reconciled:
          type: boolean
          description: True exactly when input equals terminal_total.
    FreshnessFailure:
      type: object
      required:
        - max_age_s
        - data_quality_status
      properties:
        max_age_s:
          type: integer
          format: int64
          minimum: 0
          description: The caller's requested whole-response freshness ceiling in seconds.
        actual_age_s:
          type: integer
          format: int64
          minimum: 0
          description: >-
            Age in seconds of the oldest stored data_quality.as_of clock, when
            one is available.
        as_of:
          type: string
          format: date-time
          description: >-
            The oldest stored data-quality clock used to calculate actual_age_s,
            when one is available.
        data_quality_status:
          type: string
          enum:
            - fresh
            - partial
            - unknown
            - untracked
            - unavailable
          description: >-
            The trader body's whole-response data-quality status. Only fresh can
            satisfy max_age_s.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Legacy default or named integration API key, or OAuth 2.1 access token,
        in the Authorization header as `Bearer oxi_sk_live_...` or `Bearer
        oxi_at_...`. Default keys retain full access; integration keys are
        limited to their approved read, webhooks, export and usage scopes and
        expire within 90 days. All credentials share the owner's account limits.
        Data calls require an active Pro subscription and return live data. A
        401 carries WWW-Authenticate: Bearer
        resource_metadata="https://api.0xinsider.com/.well-known/oauth-protected-resource"
        (RFC 6750 section 3, RFC 9728).
    oauth2:
      type: oauth2
      description: >-
        OAuth 2.1 authorization code flow with PKCE S256 for apps and MCP
        clients acting for a user. Public clients only (no client secret):
        register with RFC 7591 at https://api.0xinsider.com/oauth/register or
        present an https client ID metadata document URL as client_id.
        Authorization server metadata:
        https://api.0xinsider.com/.well-known/oauth-authorization-server. The
        access token (oxi_at_..., one hour) is sent as `Authorization: Bearer`;
        refresh tokens rotate on every use. A route outside the token's scopes
        answers 403 insufficient_scope. Walkthrough:
        https://0xinsider.com/auth.md.
      flows:
        authorizationCode:
          authorizationUrl: https://0xinsider.com/oauth/authorize
          tokenUrl: https://api.0xinsider.com/oauth/token
          refreshUrl: https://api.0xinsider.com/oauth/token
          scopes:
            read: >-
              Read markets, traders, large trades, positions, reports, search,
              the event stream and every MCP tool
            webhooks: Create, list, verify, rotate and delete webhook endpoints
            export: Start, poll and download trader exports
            usage: Read the caller's API usage counters

````