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

# Sports

> Live sports events, markets, cross-provider matching, brackets, and reference metadata

Live and upcoming sports events, per-event markets, cross-platform matching, brackets, and reference data. Reach for this page when you are building a scoreboard, a game page, a parlay builder, or a cross-venue price comparison.

Full parameter reference and a live tester: [API Reference](/api-reference).

## Base URL

```
https://data.kairos.trade
```

All paths on this page are relative to that host.

## Authentication

Every sports endpoint on this page is public and requires no credentials. Valid API-key or session credentials are accepted when supplied, but they are optional.

**Rate limits.** `/sports/tournament-bracket` is 10 requests/minute (the `heavy` group) and `/sports/game-markets` is 60/minute (the `sports` group). Every other route on this page has no explicit decorator and falls under the service-wide default of 100/minute. Anonymous limits are keyed by client IP; authenticated requests can use the credential identity. Group defaults are adjustable at runtime by an operator.

## Live events

```
GET /sports/live-events
```

Currently-active games with real-time scores, an additive urgency score, and cross-provider matched markets. Call it to drive a live scoreboard.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=10, s-maxage=10`.

Predict.fun fixtures are included only when the provider **indexing gate** lists `predictfun` as enabled. The gate is re-checked on every request (including cache hits). If the gate lookup fails, Predict.fun is **excluded** (fail closed) — Polymarket/Kalshi live rows still return.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `include_markets` | boolean | No | `false` | Include the full markets list (not just the primary market) per event |

```bash theme={null}
curl "https://data.kairos.trade/sports/live-events?include_markets=true"
```

Each event includes `sportFamily`, the canonical category id from `/sports/catalog`
(for example, `cricket`). The existing `sport` field remains the source league code
(for example, `cricbbl`).

**Falls back to a live provider lookup when the per-game market cache is cold**, and, when no matched entry carries a Kalshi side, synthesizes one from a team-name lookup.

### Response

The response shape is described by the field table below rather than by a JSON example.

| Field | Type | Description |
| - | - | - |
| `events[].sportFamily` | string | Canonical category id from `/sports/catalog`, such as `cricket`; `sport` remains the source league code |
| `events[].hasScore` | boolean | True only when the source reported a safely oriented score pair; schedule-only cricket rows are false |
| `events[].scoreUnavailable` | string \| absent | `no_live_score_feed` when a schedule-backed row has no trustworthy scoreboard source |
| `events[].urgencyScore` | integer | Ranking heuristic for fresh games explicitly in play: +100 live, +60 overtime, +40 late period (margin ≤5), +20 (margin ≤3), +10 (state updated ≤10s ago). Zero for stale, stopped, ended, unknown-status, or schedule-only games. |
| `events[].stale` | boolean | True when the game state is older than 120s, its timestamp is invalid/timezone-free/in the future, or no live score feed exists |
| `events[].primaryMarket` | object \| null | Normalized primary market — `question`, `outcomePrices`, `volumeNum`, `liquidityNum`, `bestBid`/`bestAsk`/`spread`, etc. |
| `events[].markets` | array | Full normalized market list; present only when `include_markets=true` |
| `events[].ts` | string | ISO 8601 timestamp of the underlying game state, not a Unix epoch |
| `matchingMarkets` | object | Keyed by `events[].matchingSlug` — one entry per event that resolved a matching slug, not per event |

Every event row also carries `gameId`, `sport`, `sportFamily`, `homeTeam`, `awayTeam`, `homeScore`, `awayScore`, `status`, `period`, `elapsed`, `live`, `ended`, `slug`, `matchingSlug`, and `marketsCount`.

`events` is ordered by `urgencyScore` descending, then by `ts` descending.

### Soccer and NFL fixture timing

`GET /txodds/fixtures/{fixture_id}/timing` returns timing evidence for NFL
regulation and second-half EPL / senior World Cup fixtures. The
[OpenAPI reference](/openapi/data-api.yaml) defines the response fields.

`usableForLateGame` indicates whether the observation supports its named
`lateGameBasis`; it does not indicate that a game is already late. Soccer's
`periodRemainingSeconds` describes the nominal half clock, excluding unknown
added time. NFL also exposes `regulationRemainingSeconds`.
`gameRemainingSeconds` remains null: neither predicts the final whistle.

Stale, missing, or unsupported evidence returns `usableForLateGame=false` with
an `unavailableReason`. Consumers must check observation age and verify the
fixture's market mapping and settlement scope before using timing.

### Timing and late-game strategies

Each event includes a `timing` object. `elapsed` remains an opaque, nullable
upstream display string. Do not subtract it from a regulation duration or use
it as seconds elapsed. A value such as `05:12` does not by itself establish
clock direction, period length, or whether the clock is running.

| `timing` field | Meaning |
| - | - |
| `source` | `polymarket` for scoreboard rows, `predictfun` for Predict.fun schedule rows, or `schedule` for Polymarket schedule-only rows |
| `status` | Normalized lifecycle: `scheduled`, `live`, `break`, `overtime`, `shootout`, `suspended`, `delayed`, `postponed`, `cancelled`, `final`, or `unknown` |
| `clock` | Raw display clock, or null |
| `clockDirection` | Currently `unknown`; this feed has no verified sport/league clock interpretation |
| `clockRunning` | Currently null; absence is not equivalent to a stopped clock |
| `periodRemainingSeconds` | Currently null; no authoritative period countdown is derived from `elapsed` |
| `gameRemainingSeconds` | Currently null; regulation, overtime, and eventual game completion are not interchangeable |
| `overtime` | True for explicit OT/extra-time evidence, false for `FT` (regulation final), otherwise null; true can describe an already finished game |
| `stateAgeSeconds` | Age of the cached game-state timestamp at evaluation, or null when invalid/unavailable |
| `evaluatedAt` | UTC evaluation timestamp; consumers must account for additional time since evaluation |
| `staleAfterSeconds` | 120; stale means strictly older than this threshold |
| `stale` | Same value as the event's top-level `stale` |
| `usableForLateGame` | Currently false for these sources; no authoritative remaining-time contract has been established |
| `unavailableReason` | `unknown_clock_semantics`, `missing_clock`, `game_not_in_play`, `stale_state`, `invalid_timestamp`, or `no_live_score_feed` |

Explicit suspended/delayed/cancelled/final statuses take precedence over the
broad `live` flag. Unrecognized statuses stay unknown. `FT OT` and `F/OT`
indicate completion after overtime, not active overtime. No countdown is
extrapolated during breaks, suspension, or silence. `urgencyScore` remains a
candidate-ranking heuristic, not permission to trigger a strategy.

`ts` records when the game state was observed. Depending on the available
feed data, it can represent the source observation or the time Kairos received
it. Freshness therefore measures the age of our observation, not a guarantee that the upstream score is current.
The response can also be cached; neither reading it again nor receiving a
WebSocket heartbeat proves that a particular game's clock has advanced.

Predict.fun and cricket schedule rows are inferred from scheduled start/end
windows, not a live scoreboard. Their legacy `live` field can be true, but
`timing.status` is `unknown`, `stale` is true, and `unavailableReason` is
`no_live_score_feed`.

The [upstream sports documentation](https://docs.polymarket.com/market-data/realtime-data#sports-stream)
lists periods and lifecycle statuses, but does not establish the complete
sport-specific `elapsed` contract needed to enable these remaining-time fields.

<Note>
  **Gotcha:** `matchingMarkets` entries are backfilled to `{"providers": []}` for any event that resolved a matching slug but has no cross-provider data. A present key does **not** imply a match was found — check `providers`. When the Predict.fun gate is off, `predictfun` entries are stripped from every `providers` list.
</Note>

## Event markets

```
GET /sports/event-markets
```

All markets for a single sports event, resolved by `game_id` or by `slug` (which is first resolved to a `gameId`).

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=10, s-maxage=10`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `game_id` | string | No\* | — | Gamma/sports game ID, looked up directly |
| `slug` | string | No\* | — | Market slug used to discover the event's `gameId` when `game_id` is absent |

\* At least one should be supplied — both are optional, unvalidated strings at the request layer.

```bash theme={null}
curl "https://data.kairos.trade/sports/event-markets?slug=ucl-din-vf-2026-08-18"
```

**Returns `200` with `{"gameId": "<value or \"unknown\">", "markets": []}` when nothing resolves — it never 404s.**

### Response

Array of normalized markets, same shape as `events[].primaryMarket` on `/sports/live-events` (`id`, `question`, `conditionId`, `tokenId`, `outcomePrices`, `outcomes`, `volumeNum`, `liquidityNum`, `acceptingOrders`, `sportsMarketType`, `provider`, ...).

## Game markets

```
GET /sports/game-markets?game_id={gameId}
GET /sports/game-markets?slug={eventSlug}
```

The full market catalog for one game, grouped for game pages and parlay builders. The response combines Polymarket's base event and linked child events (such as corners, halves, and player props) with matched Kalshi, Predict.fun, and Hyperliquid markets when available.

**Auth:** none — public. **Rate limit:** 60 requests/minute. **Cache-Control:** `public, max-age=30`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `game_id` | string | Conditional | — | 1–64 letters, digits, `_`, or `-`. Provider game ID; preferred when discovery returns one |
| `slug` | string | Conditional | — | 1–200 characters. Game event slug, such as the `eventSlug` returned by sports discovery routes |

Supply exactly one identifier. Sending neither or both returns `400 {"error":"exactly one of game_id or slug is required"}`. Use the game-level event slug, not an individual market ID. A valid identifier that does not resolve returns `200`: team and sport metadata are `null`, and sections with no matched supplemental markets are empty.

```bash theme={null}
curl "https://data.kairos.trade/sports/game-markets?slug=uel-rso-bou-2026-09-17"
```

### Response

```json theme={null}
{
  "slug": "ucl-din-vf-2026-08-18",
  "teamA": "Dinamo Zagreb",
  "teamB": "Viktoria Plzen",
  "teamALogo": "https://...",
  "teamBLogo": "https://...",
  "sport": "ucl",
  "sections": {
    "gameLines": [
      {
        "key": "moneyline",
        "title": "Moneyline",
        "section": "gameLines",
        "layout": "pills",
        "provider": "polymarket",
        "mergeKey": "moneyline",
        "volume": 15420.5,
        "legs": [
          {
            "label": "Dinamo Zagreb",
            "marketId": "poly-home-win",
            "price": 0.54,
            "side": "yes",
            "outcomeKey": "team:dinamo zagreb",
            "comboEligible": true,
            "yesPositionId": "0x...",
            "noPositionId": "0x..."
          }
        ]
      },
      {
        "key": "predictfun:pf-home-win",
        "title": "Will Dinamo Zagreb win on 2026-08-18?",
        "section": "gameLines",
        "layout": "pills",
        "provider": "predictfun",
        "mergeKey": "moneyline",
        "volume": 820,
        "legs": [
          {
            "label": "Yes",
            "marketId": "pf-home-win",
            "price": 0.52,
            "side": "yes",
            "outcomeKey": "team:dinamo zagreb",
            "comboEligible": false,
            "yesPositionId": null,
            "noPositionId": null
          }
        ]
      }
    ],
    "halves": [],
    "playerProps": [],
    "moreMarkets": [],
    "exactScore": [],
    "corners": []
  }
}
```

| Field | Type | Description |
| - | - | - |
| `sections` | object | Always contains `gameLines`, `halves`, `playerProps`, `moreMarkets`, `exactScore`, and `corners` arrays |
| `sections.<name>[].layout` | `pills` \| `ladder` | `pills` families have a flat `legs` array; spreads and totals use `rungs`, each with a numeric `line` and `legs` |
| `sections.<name>[].provider` | string | Venue for every market in the family: `polymarket`, `kalshi`, `predictfun`, or `hyperliquid` |
| `mergeKey` / `outcomeKey` | string | Opaque server-assigned cross-venue identity. Group `pills` families by `mergeKey`, then legs by `outcomeKey`; do not infer identity from titles |
| `legs[].side` | `yes` \| `no` | Contract side represented by the leg. The same `marketId` can back both sides; for example, a total's Under is commonly the `no` side |
| `legs[].comboEligible` | boolean | `true` means the leg has Polymarket combo position IDs; `false` means it cannot be used in an atomic combo |
| `legs[].yesPositionId` / `noPositionId` | string \| null | Combo position IDs. Select the ID matching `side` |

<Note>
  **Gotcha:** using the YES position ID for a `no` leg buys the opposite outcome. Always pick the position ID that matches `legs[].side`.
</Note>

`comboEligible` and both position-ID fields can be **absent** when the combo catalog is unavailable. Absence means eligibility is unknown, not false. Supplemental venue legs explicitly return `comboEligible: false` because atomic combos — single positions built from several legs — are Polymarket-only.

Supplemental venue enrichment is best-effort: unavailable supplemental data does not remove the Polymarket catalog.

<Note>
  **Gotcha:** an open market without a usable quote remains in the response with `price: 0`. Treat that as unavailable pricing, not an executable 0% price.
</Note>

## Matching markets

```
GET /sports/matching-markets
```

Cross-platform price matches for a set of market slugs — the same market on different providers, for side-by-side comparison. Entries may include Polymarket, Kalshi, Predict.fun, and Hyperliquid.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=30, s-maxage=60`, or `max-age=3` / `max-age=5` when `live=true`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `slugs` | string | Yes | — | Non-empty, comma-separated market or matching slugs. Send `matchingSlug`, `primaryMarket.slug`, and `eventSlug` when present; zero-length entries (`a,,b`) are dropped, while whitespace-only entries are looked up verbatim |
| `live` | string | No | — | Literal `"true"` or `"false"`. Refresh the Kalshi side from the live discover cache before returning |

```bash theme={null}
curl "https://data.kairos.trade/sports/matching-markets?slugs=uel-rso-bou-2026-09-17-bou,uel-rso-bou-2026-09-17&live=true"
```

No auth headers — this route is public.

<Note>
  **Gotcha:** `live` is compared as a raw string, not parsed as a boolean — anything other than `"true"` is treated as false. `live=1` and `live=TRUE` do nothing.
</Note>

**Slugs with no cross-venue match are silently omitted** from the response object rather than returned as null. Event slugs and per-outcome market slugs are different namespaces; include all keys carried by a discovery card. When `live=true`, a correction is applied if a stale Kalshi price appears to be on the wrong side of a binary flip.

### Response

```json theme={null}
{
  "nba-lal-bos-2026-01-15": {
    "title": "Lakers vs Celtics",
    "tokenId": "10897234...",
    "providers": [
      { "provider": "polymarket", "marketId": "nba-lal-bos-2026-01-15", "price": 0.62 },
      { "provider": "kalshi", "marketId": "KXNBAGAME-26JAN15LALBOS-LAL", "price": 0.6, "eventTicker": "KXNBAGAME-26JAN15LALBOS" },
      { "provider": "predictfun", "marketId": "1976281", "price": 0.61 },
      { "provider": "hyperliquid", "marketId": "3413", "price": null, "source": "market_matcher" }
    ]
  }
}
```

| Field | Type | Description |
| - | - | - |
| `<slug>.providers[].price` | number \| null | 0–1 scale probability/price; `null` when identity is known but no cached quote is available |
| `<slug>.providers[].eventTicker` | string \| null | Kalshi event ticker when available |
| `<slug>.providers[].source` | string | `market_matcher` when cross-venue identity matching supplied the provider; otherwise absent |

Returns `{}` if `slugs` parses to no non-empty entries.

## Trending matched

```
GET /sports/trending-matched
```

Top sports markets available on both Polymarket and Kalshi, ranked by live Kalshi volume.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=30, s-maxage=60`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `limit` | integer | No | `5` | 1–20. Max results |

```bash theme={null}
curl "https://data.kairos.trade/sports/trending-matched?limit=5"
```

No auth headers — this route is public.

Scans the matching cache for games with **both** a Polymarket and a Kalshi side, drops anything dated before yesterday (UTC), enriches Kalshi volume from the live discover cache (`volume_1h` falling back to total), and sorts descending.

### Response

```json theme={null}
{
  "matches": [
    {
      "slug": "nba-lal-bos-2026-01-15",
      "title": "Lakers vs Celtics",
      "image": null,
      "icon": null,
      "volume1h": 48213.5,
      "polymarket": { "marketId": "nba-lal-bos-2026-01-15", "tokenId": "10897234...", "price": 0.62 },
      "kalshi": { "marketId": "KXNBAGAME-26JAN15LALBOS-LAL", "eventTicker": "KXNBAGAME-26JAN15LALBOS", "price": 0.6 }
    }
  ],
  "count": 1
}
```

| Field | Type | Description |
| - | - | - |
| `matches[].image` / `icon` | string \| null | Always `null` — not populated by this endpoint. Do not use them as an image source |
| `count` | integer | Number of items in `matches`, after truncation to `limit` |

Returns `{"matches": [], "count": 0}` immediately if the matching cache is empty.

## Upcoming events

```
GET /sports/upcoming-events
```

The response also includes `startingEvents`: fixtures with a known kickoff in the
past ten minutes, retained while a live score may still be pending. These carry
`awaitingLive: true`; they are not confirmed live games and contain no scores.
Scheduled Polymarket fixtures with a known kickoff carry `startingEligible: true`
so a cached schedule can make the same transition. Consumers should replace a
starting fixture when its venue market ID appears in the live feed, and expire it
after ten minutes.

Not-yet-live sports events within a lookahead window, with matched cross-provider markets attached.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=30, s-maxage=60`, or `public, max-age=10, s-maxage=30` when a stale cached body is served while it refreshes in the background.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `windowHours` | integer | No | `24` | 1–720. Look-ahead window for game start times |
| `category` | string | No | — | Catalog category id. Expands to every league in that category; takes priority over `league`/`series` |
| `league` | string | No | — | Any string, lower-cased. Single league filter, used only when `category` is absent or doesn't resolve. Not validated against the catalog — an unknown slug simply resolves nothing |
| `series` | string | No | — | Comma-separated slugs. Used only when neither `category` nor `league` applies; entries are trimmed and lower-cased, blanks dropped |
| `limitPerSeries` | integer | No | `12` | 1–200. Max events fetched per resolved series |
| `startFrom` | string | No | — | ISO 8601 instant with a UTC offset. Only events starting at or after it |
| `startTo` | string | No | — | ISO 8601 instant with a UTC offset, after `startFrom`. Only events starting before it |

```bash theme={null}
curl "https://data.kairos.trade/sports/upcoming-events?windowHours=24&league=uel&limitPerSeries=12"
```

With none of `category`/`league`/`series` supplied, the query covers **every** league in the sports catalog, including UEFA Europa League (`uel`).

**Served from a shared cache**: the window is rebuilt in the background about once a minute. The default windows (`windowHours=720&limitPerSeries=200` and `windowHours=168&limitPerSeries=12`, every league) are kept warm and a copy is never served more than two hours old; any other window is served for at most ten minutes and kept warm only while it is being requested. The `Age` header gives the seconds since it was built. Only a request with no copy at all waits for a build — returning `502 {"detail": "All upstream fetches failed"}` if every upstream fetch fails, or `504` if the build runs out of time. A partial upstream failure still returns `200` with whatever resolved.

**Loading a few days at a time:** `startFrom`/`startTo` cut one slice of the window by `startTime`, so a schedule can fetch its next days as it scrolls. Adjacent slices never overlap. `startingEvents` appear only in the slice that covers the current time, and `matchingMarkets` holds only the entries the returned events reference.

### Response

The response shape is described by the field table below rather than by a JSON example.

| Field | Type | Description |
| - | - | - |
| `events[].startTime` | string | Normalized `gameStartTime` (falls back to the event's `endDate` when missing). ISO 8601 with an explicit offset, e.g. `2026-05-25T18:10:00+00:00` — not a `Z` suffix, and not a Unix epoch |
| `events[].drawPrice` / `teamBPrice` | number \| null | Present only for soccer 3-way events |
| `matchingMarkets` | object | Same four-provider matching-entry shape as `/sports/matching-markets`, keyed by every available card slug |

Every event row also carries `eventId`, `title`, `sport`, `sportFamily`, `league`, `teamA`, `teamB`, `image`, `primaryMarket`, `marketsCount`, and `eventSlug`. `sportFamily` is the canonical category id from `/sports/catalog` and is equal to `sport` on this route. `events` is sorted by `startTime` ascending.

Returns `200` with `{"events": [], "startingEvents": [], "matchingMarkets": {}}` when the filters resolve to no leagues (for example `series=,,`), and on a genuine no-data result.

## Metadata

```
GET /sports/metadata
```

Every synced team and league — logos, abbreviations, aliases, colors — for frontend lookups.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=3600, s-maxage=3600`.

### Request

No parameters.

```bash theme={null}
curl "https://data.kairos.trade/sports/metadata"
```

**Served from a shared cache** refreshed every 15 minutes and never more than 24 hours old (teams and leagues sync every 12 hours); the `Age` header gives the seconds since the body was built.

### Response

```json theme={null}
{
  "teams": [
    { "id": "142", "name": "Boston Celtics", "league": "NBA", "logoUrl": null, "abbreviation": "BOS", "alias": "Celtics", "color": "#007A33" }
  ],
  "leagues": [
    { "id": "8", "sport": "basketball", "imageUrl": null }
  ]
}
```

## Catalog

```
GET /sports/catalog
```

The structured sports catalog — categories and their leagues, enriched with resolved Polymarket series IDs. Use it to populate league pickers and to discover the `league` values the other routes accept.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=3600, s-maxage=3600`.

### Request

No parameters.

```bash theme={null}
curl "https://data.kairos.trade/sports/catalog"
```

### Response

```json theme={null}
{
  "categories": [
    {
      "id": "basketball",
      "label": "Basketball",
      "leagues": [
        { "slug": "nba", "label": "NBA", "tagId": 100639, "seriesId": 123 }
      ]
    }
  ]
}
```

<Note>
  **Gotcha:** `tagId` is a hardcoded constant (100639) shared by every league — it is not league-specific, so it cannot be used to tell leagues apart.
</Note>

`seriesId` is resolved via a cached lookup and may be `null` if unresolved.

The soccer category includes UEFA Europa League as `{ "slug": "uel", "label": "UEFA Europa League" }`.

## Kalshi live games

```
GET /sports/kalshi-live-games
```

Every currently-live Kalshi sports milestone (hockey, basketball, baseball, football/UFL, soccer, esports), with moneyline/spread/total market blocks attached.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=240`.

### Request

No parameters.

```bash theme={null}
curl "https://data.kairos.trade/sports/kalshi-live-games"
```

Building each game's market blocks from Kalshi's authenticated API is comparatively slow on a cold cache — caching keeps steady-state requests fast.

### Response

```json theme={null}
{
  "games": [ { "...": "self-contained Kalshi game card payload" } ]
}
```

## Kalshi filters

```
GET /sports/kalshi-filters
```

The Kalshi sports taxonomy — Sport → Competition → Scope — backing category-filter UI.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=21600` (6 hours, in-process and Redis).

### Request

No parameters.

```bash theme={null}
curl "https://data.kairos.trade/sports/kalshi-filters"
```

No auth headers — this route is public.

### Response

The upstream Kalshi payload is passed through **verbatim**, with no normalization:

```json theme={null}
{
  "filters_by_sports": {
    "All sports": { "competitions": {}, "scopes": [] },
    "Baseball": { "competitions": { "MLB": { "scopes": [] } }, "scopes": [] }
  },
  "sport_ordering": ["All sports", "Basketball"]
}
```

A response missing `filters_by_sports`, or a non-200 from upstream, is treated as a fetch failure and surfaces as a `500` on a cold cache.

## Futures

```
GET /sports/futures
```

Sports futures — championship and award outright markets — grouped by event, across Polymarket, Predict.fun, and Hyperliquid.

**Auth:** none — public. **Rate limit:** 100/minute. **Cache-Control:** `public, max-age=60, s-maxage=300`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `mode` | string | No | — | `sports` leaves out esports and the `other` bucket unless `category` names one; `esports` keeps only esports futures |
| `category` | string | No | `all` | `all`, a catalog category id (e.g. `basketball`, `other`), or with `mode=esports` an esports title slug or `__esports_other` for titles outside the catalog |
| `providers` | string | No | — | Comma-separated provider names to keep, e.g. `polymarket,predictfun` |
| `q` | string | No | — | Space-separated terms that must all appear in the title or a contender label |
| `offset` | integer | No | `0` | Where the page starts; use the previous page's `nextOffset` |
| `limit` | integer | No | — | 1–100. Page size; when set, the response adds `total` and `nextOffset` |

With no parameters the whole list is returned, as before.

```bash theme={null}
curl "https://data.kairos.trade/sports/futures"
curl "https://data.kairos.trade/sports/futures?mode=sports&category=basketball&limit=24"
```

**Stale-while-revalidate:** any cached body is served immediately, with a single background rebuild kicked off once it is past the soft TTL. Only a cold cache builds synchronously, and concurrent cold builds share one in-flight build.

### Response

```json theme={null}
{
  "futures": [
    {
      "eventId": "123456",
      "title": "2026 NBA Champion",
      "image": null,
      "sport": "basketball",
      "sportFamily": "basketball",
      "league": "nba",
      "provider": "polymarket",
      "volume": 8412330,
      "marketsCount": 2,
      "outcomes": [
        {
          "label": "Boston Celtics",
          "price": 0.21,
          "marketId": "will-the-celtics-win-the-2026-nba-finals",
          "tokenId": "10897234...",
          "conditionId": "0x..."
        }
      ]
    }
  ]
}
```

| Field | Type | Description |
| - | - | - |
| `futures[].sportFamily` | string | Canonical category id from `/sports/catalog`; equal to `sport` on this route |
| `futures[].provider` | string | `polymarket`, `predictfun`, or `hyperliquid` |
| `futures[].volume` | number | Summed market volume across the event's contenders, rounded to a whole number |
| `futures[].marketsCount` | integer | Number of contenders **before** the per-event cap is applied to `outcomes` |
| `outcomes[].label` | string \| null | Venue-supplied contender label. `null` when the source row lacks explicit outcome identity; clients must not infer one from the question |
| `outcomes[].price` | number \| null | Yes-side price on the **0–1** scale (Polymarket `outcomePrices`, Predict.fun best-bid/best-ask mid rounded to 4 dp). `null` when the contender is unpriced |
| `outcomes[].tokenId` / `conditionId` | string \| null | Present when the underlying market carries them |

`outcomes` is sorted favorites-first with unpriced contenders last, and is capped per event; events are returned richest-first by contender count. Per-game props and game lines are filtered out, and the unrecognised-sport bucket is additionally capped.

## Tournament bracket

```
GET /sports/tournament-bracket
```

A structured tournament bracket for a league, enriched with live Polymarket/Kalshi/Predict.fun prices per tie.

**Auth:** none — public. **Rate limit:** 10 requests/minute. **Cache-Control:** `public, max-age=60, stale-while-revalidate=3600`.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `league` | string | Yes | — | Not validated at the request layer. League slug (e.g. `ucl`, `nba`, `ncaab`) |
| `format` | string | No | `symmetric` | Not validated at the request layer; `symmetric` \| `left-to-right` \| `groups-then-knockout` are the meaningful values. Bracket layout |

```bash theme={null}
curl "https://data.kairos.trade/sports/tournament-bracket?league=ucl&format=symmetric"
```

**An unrecognised `league` returns `200`**, not `404` — the body is `{"leftRounds": [], "rightRounds": [], "final": null, "winner": null}`. An unrecognised `format` is not rejected either; anything other than `left-to-right` renders the symmetric layout, and only the literal `symmetric` selects two-legged tie merging.

`format=left-to-right` returns all rounds in `leftRounds` with `rightRounds` empty. `format=groups-then-knockout` adds a `groups` array with standings tables. `thirdPlace` is present only for football-data competitions that have a third-place fixture.

`league` and `format` are both part of the cache key, so a novel pair pays a full synchronous build.

### Response

```json theme={null}
{
  "leftRounds": [ { "name": "Quarter-finals", "shortName": "QF", "matches": [ { "...": "..." } ] } ],
  "rightRounds": [ ],
  "final": { "id": "final-tbd", "teams": [null, null], "status": "scheduled", "subtitle": "Final" },
  "winner": null
}
```

<Warning>
  **Gotcha:** `winner` is always `null` — this endpoint never resolves a champion. Read the final tie from `final` instead.
</Warning>

## Errors

Errors are returned as `{ "detail": "<message>" }`, with two exceptions: `429` uses an `error` key, and `422` uses FastAPI's standard validation body. The Code column below holds the exact message string the service returns; `—` means the response carries no fixed string.

| Status | Code | When it happens | What to do |
| - | - | - | - |
| 400 | `exactly one of game_id or slug is required` | `/sports/game-markets` received neither identifier or both identifiers | Resend with exactly one identifier. |
| 422 | — | FastAPI parameter validation — missing or empty `slugs`, missing `league`, malformed or over-long `slug`/`game_id` on `/sports/game-markets`, non-integer or out-of-range `limit`/`offset`/`windowHours`/`limitPerSeries`, an unknown futures `mode`, or a `startFrom`/`startTo` that is not an ISO instant with an offset or is out of order | Fix the parameter and resend — deterministic; retrying unchanged returns the same error. The FastAPI validation body names the offending field. |
| 429 | `Rate limit exceeded: 100 per 1 minute` | Rate limit exceeded | Back off and retry; `Retry-After` and `X-RateLimit-*` say how long. A 429 does not consume quota. |
| 429 | `API key data rate limit exceeded` | Per-credential data-API ceiling exceeded (only when the API credential carries a `data` rate-limit override) | Slow down on that credential, or have its `data` override raised. |
| 500 | `An internal error occurred. Please try again later.` | Any unhandled error — e.g. Redis or ClickHouse unavailable on `/sports/metadata`, `/sports/catalog`, or `/sports/futures`, or a cold-cache upstream failure on `/sports/kalshi-live-games` or `/sports/kalshi-filters` | Retry once the backing store or upstream recovers; report to support with the response body if it persists. |
| 502 | `All upstream fetches failed` | `/sports/upcoming-events` only — every upstream event source failed while building a cold cache entry | Retry; a partial upstream recovery returns `200` with whatever resolved. |
| 504 | `Upcoming events build timed out` | `/sports/upcoming-events` only — a request with no cached copy waited 90 seconds without a build finishing | Retry shortly; the background warmer rebuilds requested windows, and later requests are served from its copy. |

### No route on this page returns 404

Valid but unresolvable input degrades to a `200`. Do not branch on `404` — branch on the empty body:

* `/sports/event-markets` → `{"gameId": "<value or \"unknown\">", "markets": []}`
* `/sports/matching-markets` → `{}` when none of the requested slugs have a cross-venue match
* `/sports/trending-matched` → `{"matches": [], "count": 0}`
* `/sports/upcoming-events` → `{"events": [], "startingEvents": [], "matchingMarkets": {}}`
* `/sports/tournament-bracket` → `{"leftRounds": [], "rightRounds": [], "final": null, "winner": null}` for an unknown league
* `/sports/game-markets` → the catalog with `null` team/sport metadata and empty sections when the Polymarket event is missing

Best-effort enrichment never fails a request: cross-venue supplementation on `/sports/game-markets` and `/sports/upcoming-events`, the Predict.fun gate on `/sports/live-events`, combo-eligibility annotation, and background refreshes all fail open and degrade the payload rather than erroring.

`GET /sports/poly-kalshi-pairings` and `GET /sports/combo-markets` are documented on the [matched markets](/rest/matched-markets) page.


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