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

# Get Sports Edge Observations

> Insider-tier. Measures two explicitly observation-only Polymarket sports cohorts without changing or feeding GET /api/v1/sports-edge-signals: wider_holder measures pre-game holder piles outside the funded route's exact raw slate admission, including recent-flow rows rejected by its event, bucket, or total caps; in_play admits only 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. 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-slate 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. Single-flight refresh contention, global provider-work admission contention, absolute deadline exhaustion before a usable cache, or pre-holder stage expiry before a usable stored universe or primary-slate membership result returns 503 with error.reason=read_model_warming. Category-resolution SQL errors, Redis coordination failures, observation-universe SQL errors, and primary-slate 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 is fair-wave deadline exhaustion, 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; 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, with request-specific meta excluded.

Measures sports opportunities that are deliberately excluded from the funded Sports Edge slate. Use this endpoint to collect point-in-time evidence for wider-holder pre-game markets or provider-confirmed in-play markets without changing the funded algorithm.

Pass one required `cohort`:

* `wider_holder` measures current graded-holder consensus without requiring recent graded whale flow.
* `in_play` measures only provider-confirmed live games. Stale or unavailable live-board, holder, or directional evidence fails closed and emits no observation.

Every returned row has `observation_only: true`. Never route these rows to an order executor. This endpoint does not feed [Sports Edge Signals](/api-reference/endpoint/get-sports-edge-signals) or Pick of the Day.

Omit `category` to evaluate all 14 observation sports. The observation registry includes Table Tennis and Pickleball, but both remain outside the 12-sport funded projection. `category=table-tennis`, `category=table tennis`, and `category=pickleball` resolve through the same canonical taxonomy as their title-cased bucket names; a non-sport category returns an empty list.

For an all-sports request, board reads run in bounded fair waves so one slow source cannot block every later sport. Cold holder admission gives each represented canonical sport a first-candidate attempt before any sport repeats. This preserves evaluation breadth; it does not guarantee an emitted row when that sport's candidates fail a terminal gate.

All category and all-sports cache scopes share one global observation provider-work admission. Independent scope keys cannot multiply concurrent holder and price fan-out against funded provider traffic.

For `in_play`, provider `event_status` is authoritative when present: `Live` and `Paused` count as in progress, while `Ended` does not. The legacy score overlay is consulted only when `event_status` is absent.

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

Every `200` response includes a required top-level `degraded` boolean. It is `true` when an operational failure or unknown completeness state made the snapshot partial. Treat it as the snapshot-wide verdict: it can remain `true` even when the per-sport funnel cannot show the same failure because each input receives exactly one terminal reason.

The response also includes that per-sport `funnel`. For each evaluated sport, `input` must equal `terminal_total`; sparse terminal reason counts explain why candidates were emitted or excluded. `capacity_limited` is intentional bounded admission: it remains fully accounted in the funnel and does not by itself set `degraded: true`. `board_upcoming_configured` distinguishes a sport with no applicable upcoming-board source from a configured source that is unavailable.

A healthy `wider_holder` request can reuse a snapshot for about 180 seconds. An `in_play` request never serves a cached observation snapshot older than about 30 seconds. Degraded snapshots use the shorter, about-30-second lifetime for both cohorts.

Directional degradation is cohort-specific. A `wider_holder` candidate can remain emitted with `directional_status: "unavailable"` and terminal reason `wider_holder_emitted`. An `in_play` candidate fails closed instead and is counted under `in_play_directional_unavailable`.

The route reserves about 25 seconds for one compute attempt. Single-flight refresh or global provider-work admission contention, or the deadline elapsing before a usable cache, universe, or exact raw funded-slate membership result exists, returns JSON `503 rate_limit_unavailable` with `error.reason: "read_model_warming"`. Category-resolution SQL errors, Redis coordination failures, observation-universe SQL errors, and primary-slate membership SQL errors return JSON `500 internal_error`. Once a usable universe exists, later unfinished board, holder, price, or metadata work is represented in a degraded `200` funnel instead of becoming a route-wide error.

Pagination uses the endpoint-specific `seo_` cursor. Cursors are pinned to `snapshot_as_of` and the selected cohort, so you cannot reuse one across cohorts or after its snapshot refreshes.

Successful responses return a weak semantic `ETag`. It covers the stable page payload, including `next_cursor`, but excludes request-specific `meta`. Send it back as `If-None-Match` with the same query to receive `304 Not Modified` with an empty body when that stable payload is unchanged.

Malformed typed query values, such as `limit=abc`, return the standard JSON `400 bad_request` envelope. Read `error.param` when it is present; otherwise use `error.message`.

Like other public API routes, this endpoint is also subject to the 30-second transport timeout. If that outer timeout is reached, it returns `408 Request Timeout` with an empty body. Check the status before parsing JSON.


## OpenAPI

````yaml GET /api/v1/sports-edge-observations
openapi: 3.1.0
info:
  title: 0xinsider API
  description: >-
    Find your edge on Polymarket and Kalshi. Every wallet graded, every trade
    scored, every outlier flagged. API exposes trader grades, whale trades,
    smart money signals, and insider detection for AI agents, trading bots, and
    research tools. Normal API requests use a 30-second server timeout that
    returns HTTP 408 Request Timeout with an empty body when exceeded. 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, and Mcp-Session-Id request
    headers. 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
    X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After,
    ETag, X-Request-Id, X-Request-Cost, X-Batch-RateLimit-Limit,
    X-Batch-RateLimit-Remaining, X-Batch-RateLimit-Reset, Mcp-Session-Id, and
    X-Mcp-Error-Code response headers. Credentialed first-party routes such as
    /api/keys, /api/billing, and auth endpoints remain restricted to configured
    0xinsider origins.
  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 Insider subscription.
security:
  - bearerAuth: []
tags:
  - name: Traders
    description: Trader intelligence, 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: Whale Trades
    description: Recent and historical large trade intelligence.
  - 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 proven smart money is
      backing, with pre-game odds, the proven holders, and the track record.
  - name: Markets
    description: Market search, discovery, snapshots, and smart-score flow.
  - name: Insider Radar
    description: Suspicious trading pattern detection.
  - 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: 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.
paths:
  /api/v1/sports-edge-observations:
    get:
      tags:
        - Markets
      summary: List observation-only sports-edge cohorts
      description: >-
        Insider-tier. Measures two explicitly observation-only Polymarket sports
        cohorts without changing or feeding GET /api/v1/sports-edge-signals:
        wider_holder measures pre-game holder piles outside the funded route's
        exact raw slate admission, including recent-flow rows rejected by its
        event, bucket, or total caps; in_play admits only 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. 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-slate 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. Single-flight refresh contention, global
        provider-work admission contention, absolute deadline exhaustion before
        a usable cache, or pre-holder stage expiry before a usable stored
        universe or primary-slate membership result returns 503 with
        error.reason=read_model_warming. Category-resolution SQL errors, Redis
        coordination failures, observation-universe SQL errors, and
        primary-slate 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 is fair-wave deadline exhaustion,
        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; 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,
        with request-specific meta excluded.
      operationId: listSportsEdgeObservations
      parameters:
        - name: cohort
          in: query
          required: true
          description: >-
            Observation cohort. wider_holder measures pre-game holder piles
            outside the funded route's exact raw slate admission. in_play admits
            only provider-confirmed live games and excludes stale or unavailable
            provider live-board, holder, or directional evidence.
          schema:
            type: string
            enum:
              - wider_holder
              - in_play
        - 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: >-
            Opaque seo_ cursor from next_cursor. Pins snapshot_as_of, cohort,
            rank, and condition_id. 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 is 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-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'
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                  - next_cursor
                  - snapshot_as_of
                  - degraded
                  - funnel
                  - meta
                properties:
                  object:
                    type: string
                    const: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SportsEdgeObservation'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
                  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/SportsEdgeFunnelReport'
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '304':
          description: >-
            Not Modified. Returned when If-None-Match matches the current
            payload.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
        '400':
          $ref: '#/components/responses/CursorExpired'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/SubscriptionRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '408':
          $ref: '#/components/responses/RequestTimeout'
        '423':
          $ref: '#/components/responses/Locked'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ReadModelWarming'
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -sS \
              -H 'Authorization: Bearer $OXI_SK' \
              'https://api.0xinsider.com/api/v1/sports-edge-observations?cohort=wider_holder&category=Tennis&limit=20'
components:
  headers:
    X-RateLimit-Limit:
      description: Authenticated V1 per-user request limit for the current sliding window.
      schema:
        type: integer
        example: 100
    X-RateLimit-Remaining:
      description: >-
        Authenticated V1 requests remaining in the current sliding window after
        this response.
      schema:
        type: integer
        example: 84
    X-RateLimit-Reset:
      description: Unix timestamp when the authenticated V1 request window resets.
      schema:
        type: integer
        example: 1710772860
    X-Request-Id:
      description: Server-generated request identifier for support and tracing.
      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"
  schemas:
    SportsEdgeObservation:
      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
      properties:
        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
        observation_only:
          type: boolean
          const: true
          description: Always true. This row must not be routed to an order executor.
        piled_side:
          type: string
          nullable: true
          description: Human provider-backed label for the piled outcome.
        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 for participant identity.
        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:
          type: number
          minimum: -1
          maximum: 1
          description: Canonical signed holder-pile score.
        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:
          type: number
          minimum: 0
          description: Grade-weighted holder-pile score before directional enrichment.
        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 cross-market directional read. 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
          minimum: 0
          nullable: true
        directional_confidence:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
        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.
    SportsEdgeFunnelReport:
      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/SportsEdgeSportFunnelReport'
    ResponseMeta:
      type: object
      required:
        - request_id
        - cached
        - cost
      properties:
        request_id:
          type: string
          description: Unique request ID (req_ prefix).
        cached:
          type: boolean
        cache_age_s:
          type: integer
          nullable: true
          description: Cache age in seconds, null if not cached.
        cost:
          type: integer
          description: >-
            Advisory request weight (relative compute cost). 1 for simple reads;
            higher for heavier endpoints. Not a credit/price.
        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
            smart-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 smart-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.
    SportsEdgeSportFunnelReport:
      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
        - 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 fair-wave deadline exhaustion
            (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 the configured upcoming-board source completed as available.
            False with board_upcoming_configured=false means not applicable;
            false with it true can mean a completed source failure or fair-wave
            deadline exhaustion, distinguished by board_source_unavailable
            versus board_deadline_unavailable terminals.
        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 slate 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 its fair wave missed
            the bounded deadline inside the shared pre-holder stage;
            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/SportsEdgeObservationTerminalReason'
          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.
    ApiError:
      type: object
      required:
        - object
        - error
        - meta
      properties:
        object:
          type: string
          const: error
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - bad_request
                - invalid_api_key
                - subscription_required
                - forbidden
                - not_found
                - account_locked
                - rate_limited
                - rate_limit_unavailable
                - internal_error
            message:
              type: string
            doc_url:
              type: string
              nullable: true
            param:
              type: string
              nullable: true
            retry_at:
              type: string
              format: date-time
              nullable: true
              description: >-
                The recommended next retry instant (RFC3339). Present on every
                retryable error (reason=pick_not_released, code=rate_limited,
                code=rate_limit_unavailable, reason=read_model_warming) and
                omitted otherwise. Always in the future. For pick_not_released
                it has two regimes: before the 11:00 UTC operating-window start,
                before a selected pick's stored release, or after a terminal
                skipped day, it is an exact floor and nothing can publish first.
                When a publish is overdue or the selector is still hunting
                inside the operating window, it is a bounded cadence (~60s), not
                a lower-bound promise; a pick may publish before it. Schedule
                one request and do not poll. Prefer Retry-After for the duration
                because it is immune to client clock skew.
            reason:
              type: string
              enum:
                - cursor_expired
                - unknown_endpoint
                - pick_not_released
                - trader_not_tracked
                - read_model_warming
              nullable: true
              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. Consult that
                endpoint's contract, retry only this route after the interval,
                and do not infer dependency health from this reason.
        meta:
          $ref: '#/components/schemas/ResponseMeta'
  responses:
    CursorExpired:
      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'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    SubscriptionRequired:
      description: Active Insider subscription required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Forbidden:
      description: Account access denied
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    RequestTimeout:
      description: >-
        Request exceeded the server's 30-second transport timeout. The timeout
        response has an empty body because it is generated before handler-level
        JSON error shaping.
    Locked:
      description: Account is locked
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    RateLimited:
      description: >-
        Rate limit exceeded. Two 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 still denies 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
        before any item work is done.
      headers:
        Retry-After:
          description: Seconds until rate limit resets.
          schema:
            type: integer
        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'
    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    ReadModelWarming:
      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'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key authentication. Send your key in the Authorization header as
        `Bearer oxi_sk_live_...`. Live keys require an active Insider
        subscription and return live data.

````