<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Odds API]]></title><description><![CDATA[Odds API]]></description><link>https://odds-api-net.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Odds API</title><link>https://odds-api-net.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 06 Oct 2026 05:41:02 GMT</lastBuildDate><atom:link href="https://odds-api-net.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a Python odds comparison that compares the same market]]></title><description><![CDATA[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]]></description><link>https://odds-api-net.hashnode.dev/build-a-python-odds-comparison-that-compares-the-same-market</link><guid isPermaLink="true">https://odds-api-net.hashnode.dev/build-a-python-odds-comparison-that-compares-the-same-market</guid><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[sports betting]]></category><dc:creator><![CDATA[Odds API]]></dc:creator><pubDate>Mon, 28 Sep 2026 04:51:56 GMT</pubDate><content:encoded><![CDATA[<p>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.</p>
<p>This tutorial builds a small comparison from a single event's <a href="https://odds-api.net/docs">Odds API snapshot</a>. It uses Python's standard library, so the grouping rules are visible rather than hidden in an SDK call.</p>
<h2>What counts as the same selection?</h2>
<p>For this example, we compare <strong>full-time moneyline</strong> prices for the same event and side. We request only the <code>moneyline</code> market, then check <code>market_key</code>, <code>bet_type</code>, <code>period</code>, and <code>side</code> again in the returned rows. The <code>event_id</code> is fixed by the URL. The bookmaker is <em>not</em> part of the grouping key; it is what we want to compare.</p>
<p>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.</p>
<h2>Fetch every snapshot page</h2>
<p>Get a current <code>event_id</code> from <code>GET /v1/events</code>. Put your API key in an environment variable on the server. The snapshot can be paginated, so collect pages before ranking anything.</p>
<pre><code class="language-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)
</code></pre>
<p>The API keeps a bookmaker's matching markets together on a page when no bookmaker filter is set. <code>complete</code> and <code>next_cursor</code> still decide whether you have all pages. For a production cache, retain the top-level freshness fields and <code>resume</code> alongside the fetched items.</p>
<h2>Filter before sorting</h2>
<p>This example applies a <strong>client policy</strong> 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 <code>target_refresh_interval_seconds</code> and <code>ttl_seconds</code>. It is a display rule, not an API guarantee.</p>
<pre><code class="language-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 &lt;= 1:
        continue
    if not isinstance(observed_ms, (int, float)):
        continue
    if now_ms - observed_ms &gt; 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 &gt; previous["odds"]:
        best[key] = row

for key, row in sorted(best.items()):
    print(f"{key[3]:&gt;4}: {row['odds']:.2f} at {row['bookmaker']}")
</code></pre>
<p>Run both blocks in one file with <code>python compare.py EVENT_ID</code>. 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.</p>
<p>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.</p>
<h2>Use the result as a display candidate</h2>
<p>The printed price is the highest <strong>eligible snapshot price</strong> 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.</p>
<p>The <a href="https://odds-api.net/docs">Odds API documentation</a> defines the snapshot response, pagination, bookmaker freshness map, and streaming route used in this example.</p>
]]></content:encoded></item></channel></rss>