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

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

  BASE_URL = "https://therundown.io/api/v2"
  API_KEY = "YOUR_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 JavaScript theme={null}
  const BASE_URL = "https://therundown.io/api/v2";
  const API_KEY = "YOUR_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}",
      params={
          "key": API_KEY,
          "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 JavaScript theme={null}
  const sportId = 4; // NBA
  const today = new Date().toISOString().split("T")[0];

  const params = new URLSearchParams({
    key: API_KEY,
    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}`
  );
  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 "https://therundown.io/api/v2/sports/4/markets/2026-02-12?key=YOUR_API_KEY&offset=300"
  ```

  ```python Python theme={null}
  markets_response = requests.get(
      f"{BASE_URL}/sports/{sport_id}/markets/{today}",
      params={"key": API_KEY, "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 JavaScript theme={null}
  const marketsResponse = await fetch(
    `${BASE_URL}/sports/${sportId}/markets/${today}?key=${API_KEY}&offset=300&hide_closed_markets=1`
  );
  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 "https://therundown.io/api/v2/events/EVENT_ID/markets?key=YOUR_API_KEY"
  ```

  ```python Python theme={null}
  event_markets = requests.get(
      f"{BASE_URL}/events/{event_id}/markets",
      params={"key": API_KEY}
  ).json()

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

  ```javascript JavaScript theme={null}
  const eventMarkets = await fetch(
    `${BASE_URL}/events/${eventId}/markets?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 JavaScript 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 the V2 Markets WebSocket to receive live price updates. Filter by sport to reduce traffic.

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

  WS_URL = f"wss://therundown.io/api/v2/ws/markets?key={API_KEY}&sport_ids=4"


  async def listen_for_updates():
      async with websockets.connect(WS_URL) 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 JavaScript theme={null}
  const WS_URL = `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4`;

  const ws = new WebSocket(WS_URL);

  ws.onopen = () => {
    console.log("WebSocket connected");
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);

    // 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.onerror = (error) => {
    console.error("WebSocket error:", error);
  };

  ws.onclose = () => {
    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 JavaScript 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.onmessage = (rawMsg) => {
    const msg = JSON.parse(rawMsg.data);
    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>
