---
title: "Building a Sports Market Detail Page"
description: "Compose the discovery, orderbook, and chart endpoints into a cross-venue game detail page"
---

> **For AI agents:** the complete documentation index is at [llms.txt](/llms.txt). Append `.md` to any page URL for its markdown version.

A sports game's markets are spread across companion events (moneyline, spreads, totals, props)
and venues, all sharing one canonical game key (`aggKey`, e.g. `agg_fifwc_bih_che_260618`).
Build the detail page with three steps and no client-side matching.

## Listing the games (one tile per game)

`GET /venue-events?grouped=true` returns **one tile per game**: a venue's companion events
(`… - More Markets`, `… - Exact Score`, `… : Spread`) fold under their base, and cross-venue
siblings collapse to the cluster anchor. Each tile carries the game `aggKey` and
`groupMarketCount` (open markets across the whole game). Tap a tile, take its `id`, and load the
detail page below.

<Warning>
`grouped` is **opt-in** — only pass `grouped=true` once you have the game-detail page below,
because grouping hides the companion events from the listing and the detail page is what
re-surfaces their markets (spreads, exact score, props) via `aggKey`. Omit `grouped` (the
default) and companion events list as separate tiles.
</Warning>

## 1. Resolve the game (any member event → the same page)

`GET /venue-events/:id` → read `structureType` (`sport` → render the game page) and `aggKey`
(the game handle).

<Warning>
**The page is keyed on the game (`aggKey`), not on the event you opened.** A game's companion
events (`… - Halftime Result`, `… - Exact Score`, `… : Spread`) each have their own `id` but share
the same `aggKey`. Opening **any** of them must render the **same** game page. Derive the tabs and
markets below from the `aggKey` — NOT from the opened event's own `venueMarkets`. If you key off the
opened event, the base event shows only game lines and the "Halftime Result" companion shows only
halves, even though both are the same game.
</Warning>

## 2. Build the tabs from the game `aggKey`

Fetch the game's markets by `aggKey` and group them client-side by `marketGroup`:

`GET /venue-markets?aggKey={gameAggKey}&status=open&limit=100&cursor=…`

- `aggKey` is **game-scoped**: you get every market of the game across companion events + venues
  (moneyline, spreads, totals, props), deduped to one row per logical market (cross-venue siblings
  on `matchedVenueMarkets[]`).
- **The visible tab set is the `marketGroup`s present in this result** — not the groups of the
  opened event. Hide groups with no markets. Paginate large games with `cursor`/`limit`.
- To load tabs lazily instead of all at once, add a `marketGroup` (+ `period`) filter per tab — but
  the **visible tab set still comes from the game**, so probe which groups exist up front (one
  unfiltered `aggKey` page is usually enough; a game is dozens of markets, not hundreds).
- Other enums for sub-grouping/ordering within a tab: `sportsMarketType`
  (`moneyline`/`spread`/`total`/`prop`/`to_advance`/`completion`/`other`), `period` (see the full
  vocabulary below — `full`/`1h`/`2h`/`q1`..`q4`/`set_N`/`map_N`/`inning_N`/…),
  `lineValue` (number), `sectionRank` (order).

### Game Lines moneyline — source it from the game **root**

The aggKey set carries moneyline rows from every sibling venue-event, so the raw set can show
duplicate "Draw"s or drop an outcome. For the match-result N-way, take the moneyline markets from
the **opened game event's own markets** (`marketGroup=game_lines` on the event you navigated to)
and dedupe; keep the spread/total ladders from the full aggKey set. Listing endpoints already
surface group roots (`?grouped=true`), so the event you open IS the root in the normal flow.

### Spread / total ladders — group by the `aggKey` line-family, NOT the question

A spread or total is one card with a line selector (O/U 0.5 / 1.5 / 2.5 …), and the same line is
offered by several venues. **Do not group these by `question` or `marketSubtype`** — each venue
phrases and labels them differently for the *same* market (e.g. `"Argentina O/U 0.5"` /
`"Will over 0.5 goals be scored?"` / `null`), which fragments one ladder into separate cards.

Group by the **line-family key**: the market `aggKey` with the line token removed (`_<line>`, e.g.
`_2p5`). All lines of one family share it, and it is identical across venues, while still keeping
distinct subjects apart — a match total (`…_tot`), a team total (`…_tot_team_arg`), and corners
(`…_tot_corners`) each get their own family. Within a family, order the ladder by `lineValue` and
union the venues per line. (This is the one supported structural read of `aggKey`; see the
[aggKey recipe](/recipes/agg-key) — everything else stays opaque.)

### Tab → filter mapping

- **Game Lines** — `marketGroup ∈ {game_lines, spreads, totals}` (moneyline from the root member,
  above) **plus `sportsMarketType=to_advance`** (see "Team to Advance" below — it carries
  `marketGroup=specials` but belongs here).
- **Halves** — `marketGroup=period_lines`, `period ∈ {1h, 2h}` (basketball / american_football also
  expose quarters `q1`–`q4`, hockey `period_1`–`period_3` — same `period_lines` group, sub-grouped
  by `period`). Do **NOT** also filter on `sportsMarketType`: half-result legs ("France leading at
  halftime?") are `sportsMarketType=other` and an AND'd type filter drops them.
- **Exact Score** — `marketGroup=exact_score`
- **Props** — `marketGroup ∈ {team_props, player_props, specials, completion}`, **excluding
  `sportsMarketType=to_advance`** (those render under Game Lines).
- **Other** — `marketGroup=other`

### Team to Advance — a Game Lines market, above the moneyline

Knockout games (World Cup, playoff series, etc.) carry a **"Team to Advance"** market:
`sportsMarketType=to_advance`. It is a primary game line — venues render it **above** the moneyline
— but its `marketGroup` is `specials`, so filter it in **by `sportsMarketType`, not `marketGroup`**:
source `to_advance` from the game set alongside the moneyline and place it first in the Game Lines
tab. For a venue whose *only* market on a game is the advance single (it lists nothing else), this
is what makes the Game Lines tab non-empty.

Some venues decompose it into per-team Yes/No legs (`…_adv_{team}`) while others list one 2-outcome
single (`…_adv`, team-name pills). Group the whole family with the line-family key (below); anchor
the card on the bare-`_adv` single when present.

### Prop tabs via `marketCategory` (deprecated — use `marketGroup` instead)

<Warning>
`marketCategory` is **deprecated** in favor of `marketGroup` (see the Fields reference below).
It remains populated for back-compat — the soccer-only prop tabs (`corners`, `goals`, `assists`,
`shots`) still appear there — but new code should filter by `marketGroup`.
</Warning>

Each market carries a normalized `marketCategory` — the subject grouping for the prop tabs,
orthogonal to `sportsMarketType`:

- `game_lines` — moneyline / spreads / totals (sub-group with `sportsMarketType` + `period`)
- `exact_score`, `corners`, `goals`, `assists`, `shots` — the dedicated prop tabs (soccer only)
- `other` — recognized sports markets without a dedicated tab; `null` for non-sports markets

Load one prop tab the same way you load Game Lines:

`GET /venue-markets?aggKey={gameAggKey}&marketCategory=corners&limit=50`

As with every other enum, the API exposes the category; your UI owns the tab label.

- Compose your own tabs/labels from these enums — the API exposes data, not presentation. Group by
  `marketGroup` (see the Tab → filter mapping in step 2); use `sportsMarketType`/`period` only for
  sub-grouping **within** a tab, never to gate a tab (e.g. a `period_lines` "Halves" tab must not
  filter `sportsMarketType`, or half-result legs typed `other` vanish).
- Paginate large sections with `cursor`/`limit`.

## 3. Prices, order ticket, chart

- Live cross-venue prices / best price: `GET /orderbook/midpoints?venueMarketIds=…` (the market
  list carries no prices; midpoints are the price source).
- Order ticket / smart routing (split across venues): `GET /orderbook/{venueMarketOutcomeId}/route?maxSpend=…&compareVenues=true` → per-venue `fills[]` + `venueSoloQuotes[]`.
- Price chart: `GET /charts/bars?venueMarketOutcomeId=…&resolution=5`.

<Note>
Tab labels and grouping are yours to define. The API gives you normalized enums
(`sportsMarketType`, `marketGroup`, `period`, `lineValue`, `sectionRank`); map them to whatever
tabs your UI needs. The reference mapping lives in our demo app.
</Note>

## Fields reference

The following fields are available on `/venue-events` and `/venue-markets` for sports content.

### `sport` (on VenueEvent)

Normalized cross-venue sport classification. `null` for non-sports events.

Closed vocabulary:

| Value | Description |
| --- | --- |
| `soccer` | Association football |
| `basketball` | Basketball (NBA, WNBA, college, international) |
| `baseball` | Baseball (MLB, international) |
| `american_football` | American football (NFL, college) |
| `hockey` | Ice hockey (NHL, international) |
| `tennis` | Tennis (ATP, WTA, Grand Slams) |
| `cricket` | Cricket (Test, ODI, T20) |
| `golf` | Golf (PGA, LIV, majors) |
| `mma` | Mixed martial arts (UFC, Bellator, etc.) |
| `boxing` | Boxing |
| `motorsport` | Racing (F1, NASCAR, IndyCar, etc.) |
| `esports` | Competitive gaming (CS2, Dota 2, LoL, Valorant, Honor of Kings, etc.) |
| `table_tennis` | Table tennis / ping pong |
| `rugby` | Rugby (union & league — Top 14, NRL, Six Nations, etc.) |
| `volleyball` | Volleyball |
| `pickleball` | Pickleball (MLP, PPA) |
| `lacrosse` | Lacrosse (PLL, NLL) |
| `aussie_rules` | Australian rules football (AFL/AFLW) |
| `other_sport` | Any sport not in the list above |

### `marketGroup` (on VenueMarket)

Sport-aware FE grouping. Use this to build per-sport tab sets. **Supersedes `marketCategory`**
for sports tabs (see note above). The `marketGroup` query parameter on `/venue-markets` accepts
any value from this set.

Closed vocabulary:

| Value | Description |
| --- | --- |
| `game_lines` | Full-game moneyline / winner markets |
| `spreads` | Handicap / point-spread markets |
| `totals` | Over/under total-points or total-goals markets |
| `period_lines` | Intra-game moneyline, spread, or total scoped to a period (quarter, half, inning, set, map) |
| `exact_score` | Exact final score markets |
| `team_props` | Team-level proposition markets (NRFI, first-5-innings winner, both-teams-to-score, etc.) |
| `player_props` | Player-level proposition markets (points, rebounds, strikeouts, etc.) |
| `specials` | Novelty / event-level specials (toss winner, method of victory, championship outright, **team to advance** — the last is `sportsMarketType=to_advance` and belongs in the Game Lines tab, not Props; see above) |
| `completion` | "Will this match complete?" propositions (tennis, cricket) |
| `other` | Sports markets that don't fit the groups above |

### `marketSubtype` (on VenueMarket)

Lossless raw venue market type string preserved from discovery. Examples:
`cricket_toss_winner`, `nrfi`, `tennis_set_winner`, `cs2_odd_even_total_kills`,
`baseball_team_first_five_winner`, `soccer_halftime_result`, `map_handicap`.

Use `marketSubtype` to label or disambiguate rows within a `marketGroup` (e.g. render the
`nrfi` row with its proper label rather than a generic "Team Props" heading). `null` when the
venue supplies no subtype.

### `period` (on VenueMarket)

Temporal scope of the market. Expanded from the original set to cover all sports:

| Tokens | Description |
| --- | --- |
| `full` | Full game / match |
| `match` | Alias for full match (tennis, cricket) |
| `series` | Multi-game series result |
| `1h` / `2h` | First / second half (soccer, basketball, american_football) |
| `q1` / `q2` / `q3` / `q4` | Quarters (basketball, american_football) |
| `period_1` / `period_2` / `period_3` | Periods (hockey) |
| `inning_1` … `inning_9` | Individual innings (baseball) |
| `first_5_innings` | First-five-innings scope (baseball) |
| `first_3_innings` / `first_7_innings` | First-three / first-seven-innings scope (baseball) |
| `set_1` … `set_5` | Individual sets (tennis) |
| `map_1` … `map_5` | Individual maps (esports — CS2, Dota 2, etc.) |
| `game_1` … `game_5` | Individual games within a series (esports, some tennis) |
| `ot` | Overtime / extra time |

## Per-sport tab matrix

Use this table as the starting point for building per-sport tab UIs. Render only the
`marketGroup` values that appear for a given `sport`; hide empty tabs.

<Note>
Coverage reflects what venues currently list. `other` and `other_sport` are catch-alls. The set
is venue-driven and grows as new market types are listed.
</Note>

| Sport | marketGroups present | Notable subtypes / periods |
| --- | --- | --- |
| `soccer` | `game_lines`, `spreads`, `totals`, `period_lines`, `exact_score`, `team_props`, `specials` | `soccer_halftime_result` (periods `1h`/`2h`), `soccer_exact_score`, `both_teams_to_score`; knockout `to_advance` (in `specials`, rendered under Game Lines); legacy corners/goals/assists/shots via `marketCategory` |
| `basketball` | `game_lines`, `spreads`, `totals`, `period_lines`, `player_props` | quarters `q1`–`q4`; player points / rebounds / assists props |
| `baseball` | `game_lines`, `spreads`, `totals`, `team_props`, `player_props` | `nrfi` (`inning_1`), `baseball_team_first_five_winner` (`first_5_innings`), strikeout / home-run player props |
| `american_football` | `game_lines`, `spreads`, `totals`, `period_lines` | quarters `q1`–`q4`, halves `1h`/`2h` |
| `hockey` | `game_lines`, `spreads`, `totals`, `period_lines` | `period_1`–`period_3` |
| `tennis` | `game_lines`, `totals`, `spreads`, `completion` | `tennis_set_winner` (`set_1`–`set_5`), `tennis_first_set_totals` (`set_1`), `tennis_match_totals` (`match`), `tennis_completed_match` |
| `cricket` | `game_lines`, `totals`, `team_props`, `player_props`, `specials`, `completion` | `cricket_toss_winner` (`specials`), `cricket_team_top_batter` / `cricket_most_sixes` (`player_props`), `cricket_completed_match` |
| `golf` | `game_lines`, `specials` | Outright winner, matchup markets, MVP / championship specials |
| `mma` | `game_lines`, `specials` | Fight winner, method-of-victory / round specials |
| `boxing` | `game_lines`, `specials` | Fight winner, method / round specials |
| `motorsport` | `game_lines`, `specials` | Race winner, podium specials |
| `esports` | `game_lines`, `spreads`, `totals`, `player_props`, `specials` | `map_handicap` (`spreads`), `cs2_odd_even_total_kills` / `kill_over_under_game` (`player_props`), `first_blood_game` / `dota2_rampage` (`specials`); periods `map_1`–`map_5` / `game_1`–`game_5` |
| `rugby` | `game_lines`, `totals`, `specials` | Match winner, draw, handicap; mostly moneyline today |
| `volleyball` | `game_lines`, `totals`, `specials` | Match/set winner, set totals |
| `pickleball` | `game_lines`, `specials` | Match winner; mostly moneyline today |
| `lacrosse` | `game_lines`, `spreads`, `totals` | Moneyline / spread / total |
| `aussie_rules` | `game_lines`, `spreads`, `totals` | Moneyline / line / total (AFL) |

<Note>
`sport` is resolved from the venue's league code first, then its **tags** (which carry a clean
sport name like "Rugby" or "Volleyball"), then the title. Sports a venue lists but that aren't yet
in the vocabulary fall to `other_sport` (still classified into `marketGroup`s) until added.
</Note>

## Filter examples

### Baseball — team props for a specific game

```
GET /venue-markets?aggKey=agg_mlb_nyy_bos_260618&marketGroup=team_props
```

Returns the team-prop markets for that game: NRFI (`nrfi`, `period=inning_1`), first-5-innings
winner (`baseball_team_first_five_winner`, `period=first_5_innings`), and any other team-level
propositions available across venues. Each row's `matchedVenueMarkets[]` contains the cross-venue
pairings so you can show the best price without a second request.

### Tennis — set and match totals

```
GET /venue-markets?aggKey=agg_atp_djokovic_sinner_260618&marketGroup=totals
```

Returns all over/under totals for the match: per-set totals (`period=set_1`, `period=set_2`, …)
and full-match totals (`period=match`). Use `marketSubtype` to distinguish the rendering:
`tennis_first_set_totals` gets a "First Set" label; `tennis_match_totals` gets "Match Total".
