# Build a Python odds comparison that compares the same market

The highest number in an odds response is not automatically the best price. A full-game moneyline, a first-half line, and a point spread answer different questions. Even within one market, a suspended or stale row should not win a comparison.

This tutorial builds a small comparison from a single event's [Odds API snapshot](https://odds-api.net/docs). It uses Python's standard library, so the grouping rules are visible rather than hidden in an SDK call.

## What counts as the same selection?

For this example, we compare **full-time moneyline** prices for the same event and side. We request only the `moneyline` market, then check `market_key`, `bet_type`, `period`, and `side` again in the returned rows. The `event_id` is fixed by the URL. The bookmaker is *not* part of the grouping key; it is what we want to compare.

For a spread or total, add the exact point value and any other selection dimensions to the key. Never group all “home” selections together across different periods or lines.

## Fetch every snapshot page

Get a current `event_id` from `GET /v1/events`. Put your API key in an environment variable on the server. The snapshot can be paginated, so collect pages before ranking anything.

```python
import json
import os
import sys
import time
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

BASE = "https://api.odds-api.net/v1"
API_KEY = os.environ["ODDS_API_KEY"]
EVENT_ID = sys.argv[1]


def get_json(url):
    request = Request(url, headers={"X-API-Key": API_KEY})
    with urlopen(request, timeout=20) as response:
        return json.load(response)


def load_snapshot(event_id):
    path = f"/events/{quote(event_id, safe='')}/odds/snapshot"
    filters = {"market_keys": "moneyline"}
    items = []
    bookmaker_times = {}
    cursor = None

    while True:
        query = dict(filters)
        if cursor:
            query["cursor"] = cursor
        page = get_json(f"{BASE}{path}?{urlencode(query)}")
        items.extend(page["items"])
        bookmaker_times.update(page.get("bookmaker_as_of_ts_ms", {}))
        if page["complete"]:
            return bookmaker_times, items
        cursor = page.get("next_cursor")
        if not cursor:
            raise RuntimeError("Incomplete snapshot without next_cursor")


bookmaker_times, items = load_snapshot(EVENT_ID)
```

The API keeps a bookmaker's matching markets together on a page when no bookmaker filter is set. `complete` and `next_cursor` still decide whether you have all pages. For a production cache, retain the top-level freshness fields and `resume` alongside the fetched items.

## Filter before sorting

This example applies a **client policy** of at most 120 seconds since each bookmaker's accepted snapshot. Adjust that threshold to suit your application and compare it with the response's `target_refresh_interval_seconds` and `ttl_seconds`. It is a display rule, not an API guarantee.

```python
MAX_AGE_MS = 120_000
now_ms = int(time.time() * 1000)
best = {}

for row in items:
    if row.get("market_key") != "moneyline":
        continue
    if row.get("bet_type") != "moneyline":
        continue
    if row.get("period") != "full time":
        continue
    if row.get("side") not in {"home", "away"}:
        continue
    if not row.get("is_available"):
        continue

    price = row.get("odds")
    bookmaker = row.get("bookmaker")
    observed_ms = bookmaker_times.get(bookmaker)
    if not isinstance(price, (int, float)) or price <= 1:
        continue
    if not isinstance(observed_ms, (int, float)):
        continue
    if now_ms - observed_ms > MAX_AGE_MS:
        continue

    key = (row["market_key"], row["bet_type"], row["period"], row["side"])
    previous = best.get(key)
    if previous is None or price > previous["odds"]:
        best[key] = row

for key, row in sorted(best.items()):
    print(f"{key[3]:>4}: {row['odds']:.2f} at {row['bookmaker']}")
```

Run both blocks in one file with `python compare.py EVENT_ID`. A row can disappear from the output because it is unavailable, lacks bookmaker freshness metadata, or exceeds the chosen age threshold. That is preferable to presenting it as a current best price.

One subtlety: the code combines per-bookmaker timestamps across pages. If you need a strict point-in-time comparison across a large paginated response, verify the snapshot and freshness metadata stay consistent as you page. If they do not, retry the snapshot before showing a ranking.

## Use the result as a display candidate

The printed price is the highest **eligible snapshot price** for that exact selection. It is not a promise that a bookmaker still offers it when a user opens the market. Refresh the snapshot or subscribe to the event's odds stream when you need a hotter view, and recheck availability before any downstream action.

The [Odds API documentation](https://odds-api.net/docs) defines the snapshot response, pagination, bookmaker freshness map, and streaming route used in this example.
