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

# Building an Odds Screen

> Step-by-step guide to building a real-time odds screen with moneyline, spread, and total columns using TheRundown API and WebSocket.

This guide walks through building a complete odds screen from scratch -- fetching sports, loading events, parsing market data, connecting a WebSocket for live updates, and handling price changes in your UI.

Run the JavaScript walkthrough server-side with Node.js 22+ ESM (`.mjs` or `"type": "module"`). Install `ws` with `npm install ws`; keep `THERUNDOWN_API_KEY` in the server environment and relay browser updates through your authenticated backend.

## Step 1: Fetch the Sports List

Start by loading the list of available sports. This endpoint is public and does not require authentication.

<CodeGroup>
  ```python Python theme={null}
  import os
  import requests

  BASE_URL = "https://therundown.io/api/v2"
  API_KEY = os.environ["THERUNDOWN_API_KEY"]

  sports_response = requests.get(f"{BASE_URL}/sports")
  sports = sports_response.json()["sports"]

  for sport in sports:
      print(f"{sport['sport_id']}: {sport['sport_name']}")
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  import WebSocket from "ws";

  const BASE_URL = "https://therundown.io/api/v2";
  const API_KEY = process.env.THERUNDOWN_API_KEY;
  if (!API_KEY) throw Error('Set THERUNDOWN_API_KEY');

  const sportsResponse = await fetch(`${BASE_URL}/sports`);
  const { sports } = await sportsResponse.json();

  for (const sport of sports) {
    console.log(`${sport.sport_id}: ${sport.sport_name}`);
  }
  ```
</CodeGroup>

Use the sport list to populate a sport selector in your UI. Common sport IDs:

| Sport | ID |
| - | - |
| NFL | 2 |
| MLB | 3 |
| NBA | 4 |
| NCAAB | 5 |
| NHL | 6 |

## Step 2: Fetch Events for a Sport and Date

Once the user selects a sport, fetch events for that sport on a given date. Include `market_ids=1,2,3` for moneyline, spread, and total. Use `main_line=true` to get only the primary line for each market.

<Note>
  Always pass `offset=300` to align the date boundary with US Central Time. Without this, games that tip off late at night may show up under the next day's date.
</Note>

<CodeGroup>
  ```python Python theme={null}
  from datetime import date

  sport_id = 4  # NBA
  today = date.today().isoformat()

  response = requests.get(
      f"{BASE_URL}/sports/{sport_id}/events/{today}",
      headers={"X-TheRundown-Key": API_KEY},
      params={
          "market_ids": "1,2,3",
          "affiliate_ids": "19,23,22",  # DraftKings, FanDuel, BetMGM
          "main_line": "true",
          "offset": "300",             # Central Time
      }
  )

  events = response.json()["events"]
  print(f"Found {len(events)} events")
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  const sportId = 4; // NBA
  const today = new Date().toISOString().split("T")[0];

  const params = new URLSearchParams({
    market_ids: "1,2,3",
    affiliate_ids: "19,23,22", // DraftKings, FanDuel, BetMGM
    main_line: "true",
    offset: "300", // Central Time
  });

  const response = await fetch(
    `${BASE_URL}/sports/${sportId}/events/${today}?${params}`,
    { headers: { "X-TheRundown-Key": API_KEY } }
  );
  const { events } = await response.json();

  console.log(`Found ${events.length} events`);
  ```
</CodeGroup>

## Step 3: Discover Available Markets

Not every sport or event has the same markets. Before building your odds columns, check which markets actually have data. There are two ways to do this:

### By sport and date

Returns all markets with active pricing for a sport on a given date, keyed by sport ID. Use `hide_closed_markets=1` to exclude markets that have been taken off the board.

<CodeGroup>
  ```bash cURL theme={null}
  curl -H "X-TheRundown-Key: $THERUNDOWN_API_KEY" "https://therundown.io/api/v2/sports/4/markets/2026-02-12?offset=300"
  ```

  ```python Python theme={null}
  markets_response = requests.get(
      f"{BASE_URL}/sports/{sport_id}/markets/{today}",
      headers={"X-TheRundown-Key": API_KEY},
      params={"offset": "300", "hide_closed_markets": "1"}
  )

  for m in markets_response.json().get(str(sport_id), []):
      print(f"{m['id']:>4}  {m['name']:<30}  prop={m['proposition']}")
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  const marketsResponse = await fetch(
    `${BASE_URL}/sports/${sportId}/markets/${today}?offset=300&hide_closed_markets=1`,
    { headers: { "X-TheRundown-Key": API_KEY } }
  );
  const marketsBySport = await marketsResponse.json();

  for (const m of marketsBySport[sportId] || []) {
    console.log(`${m.id}  ${m.name}  prop=${m.proposition}`);
  }
  ```
</CodeGroup>

### By event ID

Returns only the markets available for a specific event. Useful when building a detail view for a single game.

<CodeGroup>
  ```bash cURL theme={null}
  curl -H "X-TheRundown-Key: $THERUNDOWN_API_KEY" "https://therundown.io/api/v2/events/EVENT_ID/markets"
  ```

  ```python Python theme={null}
  event_markets = requests.get(
      f"{BASE_URL}/events/{event_id}/markets",
      headers={"X-TheRundown-Key": API_KEY}
  ).json()

  for m in event_markets:
      print(f"{m['id']:>4}  {m['name']}")
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  const eventId = "EVENT_ID";
  const eventMarkets = await fetch(
    `${BASE_URL}/events/${eventId}/markets`,
    { headers: { "X-TheRundown-Key": API_KEY } }
  ).then((r) => r.json());

  for (const m of eventMarkets) {
    console.log(`${m.id}  ${m.name}`);
  }
  ```
</CodeGroup>

Use the `event_id` value returned by `GET /api/v2/sports/{sportID}/events/{date}` when calling per-event V2 endpoints.

Each market object includes:

| Field | Description |
| - | - |
| `id` | Numeric market ID — pass these as `market_ids` when fetching event odds |
| `name` | Human-readable name (e.g., "Moneyline", "Player Points") |
| `proposition` | `true` for player prop markets, `false` for game-level markets |
| `period_id` | Period this market applies to (full game, first half, etc.) |
| `live_variant_id` | If set, the corresponding live/in-play market ID |
| `description` | Longer description of the market |

The three core markets for an odds screen are **1** (Moneyline), **2** (Spread), and **3** (Total). See [Market IDs](/reference/markets) for the full list including player props and live markets.

## Step 4: Parse Markets for Display

Each event contains a `markets` array. Index it by `market_id` to pull moneyline (1), spread (2), and total (3) for each sportsbook.

<CodeGroup>
  ```python Python theme={null}
  MARKET_MONEYLINE = 1
  MARKET_SPREAD = 2
  MARKET_TOTAL = 3


  def get_price(participant, affiliate_id):
      """Extract the price from a participant's first line for a given book."""
      for line in participant.get("lines", []):
          p = line.get("prices", {}).get(affiliate_id, {}).get("price")
          if p == 0.0001:
              return None, line.get("value")  # Off the board
          return p, line.get("value")
      return None, None


  def fmt(price):
      """Format American odds for display."""
      if price is None:
          return "N/A"
      return f"+{int(price)}" if price > 0 else str(int(price))


  # Build a lookup: market_id -> market object
  for event in events:
      away = event["teams"][0]["name"]
      home = event["teams"][1]["name"]
      markets = {m["market_id"]: m for m in event.get("markets", [])}

      # Moneyline
      ml = markets.get(MARKET_MONEYLINE, {})
      ml_prices = {}
      for p in ml.get("participants", []):
          price, _ = get_price(p, "19")
          ml_prices[p["name"]] = fmt(price)

      # Spread
      sp = markets.get(MARKET_SPREAD, {})
      sp_prices = {}
      for p in sp.get("participants", []):
          price, value = get_price(p, "19")
          sp_prices[p["name"]] = f"{value} ({fmt(price)})"

      # Total
      tot = markets.get(MARKET_TOTAL, {})
      tot_prices = {}
      for p in tot.get("participants", []):
          price, value = get_price(p, "19")
          side = "O" if p.get("type") == "TYPE_OVER" else "U"
          tot_prices[side] = f"{value} {fmt(price)}"

      print(f"\n{away} @ {home}")
      print(f"  ML:     {ml_prices.get(away, 'N/A')} / {ml_prices.get(home, 'N/A')}")
      print(f"  Spread: {sp_prices.get(away, 'N/A')} / {sp_prices.get(home, 'N/A')}")
      print(f"  Total:  {tot_prices.get('O', 'N/A')} / {tot_prices.get('U', 'N/A')}")
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  const MARKET_MONEYLINE = 1;
  const MARKET_SPREAD = 2;
  const MARKET_TOTAL = 3;

  function getPrice(participant, affiliateId) {
    const line = participant.lines?.[0];
    if (!line) return { price: null, value: null };
    const raw = line.prices?.[affiliateId]?.price ?? null;
    return { price: raw === 0.0001 ? null : raw, value: line.value };
  }

  function fmt(price) {
    if (price == null) return "N/A";
    return price > 0 ? `+${Math.round(price)}` : String(Math.round(price));
  }

  for (const event of events) {
    const [away, home] = event.teams.map((t) => t.name);

    // Index markets by ID for direct lookup
    const markets = Object.fromEntries(
      (event.markets || []).map((m) => [m.market_id, m])
    );

    // Moneyline
    const ml = {};
    for (const p of markets[MARKET_MONEYLINE]?.participants || []) {
      ml[p.name] = fmt(getPrice(p, "19").price);
    }

    // Spread
    const sp = {};
    for (const p of markets[MARKET_SPREAD]?.participants || []) {
      const { price, value } = getPrice(p, "19");
      sp[p.name] = `${value} (${fmt(price)})`;
    }

    // Total
    const tot = {};
    for (const p of markets[MARKET_TOTAL]?.participants || []) {
      const { price, value } = getPrice(p, "19");
      const side = p.type === "TYPE_OVER" ? "O" : "U";
      tot[side] = `${value} ${fmt(price)}`;
    }

    console.log(`\n${away} @ ${home}`);
    console.log(`  ML:     ${ml[away] || "N/A"} / ${ml[home] || "N/A"}`);
    console.log(`  Spread: ${sp[away] || "N/A"} / ${sp[home] || "N/A"}`);
    console.log(`  Total:  ${tot.O || "N/A"} / ${tot.U || "N/A"}`);
  }
  ```
</CodeGroup>

## Step 5: Connect WebSocket for Real-Time Updates

Once your initial data is loaded, connect a server-side V2 Markets WebSocket client to receive live price updates. WebSocket access requires an Ultra plan or higher. Native browser WebSocket clients cannot set the authentication header, so browser consumers should connect through an authenticated backend relay. Filter by sport to reduce traffic.

The Python example uses `websockets>=14`; the Node.js example requires `ws` (`npm install ws`).

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import json
  import os
  import websockets

  API_KEY = os.environ["THERUNDOWN_API_KEY"]
  WS_URL = "wss://therundown.io/api/v2/ws/markets?sport_ids=4"


  async def listen_for_updates():
      async with websockets.connect(
          WS_URL, additional_headers={"X-TheRundown-Key": API_KEY}
      ) as ws:
          async for message in ws:
              msg = json.loads(message)

              # Skip heartbeat messages
              if msg.get("meta", {}).get("type") == "heartbeat":
                  continue

              d = msg["data"]
              print(f"Update: event={d['event_id']} market={d['market_id']} aff={d['affiliate_id']}")
              print(f"  line={d['line']} price={d['price']} (was {d['previous_price']})")


  asyncio.run(listen_for_updates())
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  const WS_URL = "wss://therundown.io/api/v2/ws/markets?sport_ids=4";
  const ws = new WebSocket(WS_URL, {
    headers: { "X-TheRundown-Key": API_KEY },
  });

  ws.on("open", () => {
    console.log("WebSocket connected");
  });

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());

    // Skip heartbeat messages
    if (msg.meta?.type === "heartbeat") return;

    const d = msg.data;
    console.log(`Update: event=${d.event_id} market=${d.market_id} aff=${d.affiliate_id}`);
    console.log(`  line=${d.line} price=${d.price} (was ${d.previous_price})`);
  });

  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });

  ws.on("close", () => {
    console.log("WebSocket closed, reconnecting...");
    // Implement reconnection logic here
  });
  ```
</CodeGroup>

## Step 6: Handle Price Updates in the UI

When a WebSocket message arrives, merge the individual price update into your local state. Each message contains a single price change — find the matching event, market, participant, and affiliate, then update the price.

<CodeGroup>
  ```python Python theme={null}
  # In-memory state: event_id -> full event dict from the REST API
  event_state = {e["event_id"]: e for e in events}


  def apply_price_update(update):
      """Merge a single WebSocket price update into the local event state."""
      event = event_state.get(update["event_id"])
      if not event:
          return

      market_id = update["market_id"]
      aff_id = str(update["affiliate_id"])
      participant_id = update["normalized_market_participant_id"]
      new_price = float(update["price"])

      for market in event.get("markets", []):
          if market["market_id"] != market_id:
              continue

          for participant in market.get("participants", []):
              if participant["id"] != participant_id:
                  continue

              for line in participant.get("lines", []):
                  prices = line.setdefault("prices", {})
                  prices[aff_id] = {
                      "price": new_price,
                      "is_main_line": update["is_main_line"],
                      "updated_at": update["updated_at"],
                  }
                  print(f"Updated: market={market_id} participant={participant_id} aff={aff_id} price={new_price}")
                  return
  ```

  ```javascript Node.js 22+ (ESM) theme={null}
  // In-memory state: eventId -> full event object from the REST API
  const eventState = new Map(events.map((e) => [e.event_id, e]));

  function applyPriceUpdate(update) {
    const event = eventState.get(update.event_id);
    if (!event) return;

    const marketId = update.market_id;
    const affId = String(update.affiliate_id);
    const participantId = update.normalized_market_participant_id;

    for (const market of event.markets || []) {
      if (market.market_id !== marketId) continue;

      for (const participant of market.participants || []) {
        if (participant.id !== participantId) continue;

        for (const line of participant.lines || []) {
          line.prices[affId] = {
            price: parseFloat(update.price),
            is_main_line: update.is_main_line,
            updated_at: update.updated_at,
          };
          // Re-render the event row in your UI here
          return;
        }
      }
    }
  }

  // Wire up to WebSocket
  ws.on("message", (rawMsg) => {
    const msg = JSON.parse(rawMsg.toString());
    if (msg.meta?.type === "heartbeat") return;
    applyPriceUpdate(msg.data);
  });
  ```
</CodeGroup>

## Tips for Production

<AccordionGroup>
  <Accordion title="Use delta endpoints for efficient polling as a fallback">
    If the WebSocket disconnects, use `GET /api/v2/delta` to fetch event deltas or `GET /api/v2/markets/delta` to fetch market price deltas since your last request. This is much more efficient than refetching the full event list.
  </Accordion>

  <Accordion title="Animate price changes">
    When a price moves, briefly highlight the cell green (price improved for the bettor) or red (price worsened). This visual cue helps users notice live movement.
  </Accordion>

  <Accordion title="Handle the 0.0001 sentinel value">
    Always check for `0.0001` before displaying a price. Show "Off Board" or "N/A" instead. See [Sentinel Values](/reference/sentinel-values) for details.
  </Accordion>

  <Accordion title="Cache the sports and affiliates lists">
    The sports and affiliates endpoints return reference data that rarely changes. Cache these responses and refresh once per day to avoid unnecessary API calls.
  </Accordion>

  <Accordion title="Filter aggressively">
    Use `affiliate_ids`, `market_ids`, and `main_line=true` to reduce payload size. Only request the data your UI actually displays.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="WebSocket Streaming" icon="bolt" href="/guides/websocket-streaming">
    Deep dive into WebSocket configuration
  </Card>

  <Card title="Efficient Polling" icon="rotate" href="/guides/efficient-polling">
    Delta endpoints and cache strategies for when WebSocket isn't available
  </Card>

  <Card title="Player Props" icon="user" href="/guides/player-props">
    Add player prop markets to your screen
  </Card>

  <Card title="Historical Odds" icon="chart-line" href="/guides/historical-odds">
    Track line movement over time
  </Card>

  <Card title="Data Model" icon="diagram-project" href="/reference/data-model">
    How events, markets, lines, and prices relate
  </Card>

  <Card title="Sportsbook IDs" icon="list" href="/reference/sportsbooks">
    Full list of affiliate IDs
  </Card>
</CardGroup>


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