Skip to content

VALUEODDSAPI / DEVELOPER RESOURCES

How to build an odds comparison: a developer guide

Showing the best price across bookmakers looks simple, until the same match has five different names and the best price is fourteen minutes old. This guide walks through the data model, the matching problems, working Python code and the checks to run before launch.

Published by ValueOddsAPI · 2 November 2026

Illustration: many thin data streams merging into one sorted green stream, like odds being matched and normalised
Illustration generated with AI.

In 30 seconds

New to odds data? Five terms first

Odds comparison
A page or app that shows the best available price for each outcome across bookmakers.
Normalise
Convert every bookmaker’s naming into one shared format.
Line
The number in a market, such as 2.5 in Over/Under 2.5 goals.
Stale price
A price that has not been confirmed recently and may no longer be available.
Arbitrage
Best prices that together guarantee a profit. Rare, short-lived and often a data error.

Start with one row per price

Whatever your source, flatten it into one row per price. Every later step, from matching to display, becomes a simple group-by on these fields.

Fields for one normalised price row
FieldExampleWhy it matters
event_idars-cheOne ID for the same match at every bookmaker. Getting this right is most of the work.
market1x2, total, handicapA shared market name, regardless of what each bookmaker calls it.
line2.5, −1, noneOver 2.5 and Over 3.5 are different markets and must never be compared.
selectionhome, overA shared outcome name, from the same perspective at every bookmaker.
bookmakerBookmaker AWhere the price comes from.
odds2.18Decimal odds. Convert fractional or American odds on the way in.
updated_at18:00:15 UTCWhen the price was last confirmed. Without it you cannot hide stale prices.

Step 1: match events across bookmakers

Bookmakers write the same match differently: “Man Utd” and “Manchester United”, reversed word order, missing accents, “FC” prefixes, kick-off times a few minutes apart. Matching on raw names silently splits one match into several, or worse, merges two different ones.

Normalise names: lower-case, strip accents and words such as FC or SK. Keep an alias table for names that still differ. Require more than a name: the same competition, both teams and a kick-off within a tolerance. Watch the traps: women’s, U21 and reserve teams share names with the first team.

Do not auto-merge on a fuzzy score alone. Send uncertain matches to a review queue. A wrong match shows users a “best price” for a different game.

Step 2: normalise markets and lines

One bookmaker’s “Total goals” is another’s “Over/Under” or “Goals”. Map every raw market name to your shared list, and keep the raw name too so you can remap later.

Lines must match exactly: compare Over 2.5 only with Over 2.5. Handicaps need one sign convention, for example always from the home team’s side. Settlement rules differ: 90 minutes or including extra time, and how a non-starting player is treated in player markets. Prices with different rules are not comparable.

Some bookmakers also list the same label twice in one market. Keep both with an index rather than letting one overwrite the other.

Step 3: pick the best fresh price

With clean rows, the comparison itself is short. This example uses illustrative data, not a live feed. It drops prices older than five minutes, then keeps the highest price per selection.

from collections import defaultdict
from datetime import datetime, timedelta, timezone

# One row per price: illustrative data in a normalised shape
now = datetime(2026, 11, 2, 18, 0, tzinfo=timezone.utc)
rows = [
    # event_id, market, line, selection, bookmaker, odds, updated_at
    ("ars-che", "1x2", None, "home", "Bookmaker A", 2.10, now - timedelta(seconds=40)),
    ("ars-che", "1x2", None, "home", "Bookmaker B", 2.18, now - timedelta(seconds=75)),
    ("ars-che", "1x2", None, "home", "Bookmaker C", 2.25, now - timedelta(minutes=14)),
    ("ars-che", "1x2", None, "draw", "Bookmaker A", 3.50, now - timedelta(seconds=40)),
    ("ars-che", "1x2", None, "draw", "Bookmaker B", 3.40, now - timedelta(seconds=75)),
    ("ars-che", "1x2", None, "away", "Bookmaker A", 3.60, now - timedelta(seconds=40)),
    ("ars-che", "1x2", None, "away", "Bookmaker B", 3.75, now - timedelta(seconds=75)),
    ("ars-che", "total", 2.5, "over", "Bookmaker A", 1.95, now - timedelta(seconds=40)),
    ("ars-che", "total", 2.5, "over", "Bookmaker B", 1.91, now - timedelta(seconds=75)),
    ("ars-che", "total", 2.5, "under", "Bookmaker A", 1.87, now - timedelta(seconds=40)),
    ("ars-che", "total", 2.5, "under", "Bookmaker B", 1.93, now - timedelta(seconds=75)),
]

MAX_AGE = timedelta(minutes=5)

def best_prices(rows, now):
    """Best fresh price per selection, grouped by event, market and line."""
    markets = defaultdict(dict)
    for event, market, line, selection, book, odds, updated in rows:
        if now - updated > MAX_AGE:
            continue                       # stale price: never show it as best
        key = (event, market, line)
        best = markets[key].get(selection)
        if best is None or odds > best[0]:
            markets[key][selection] = (odds, book)
    return markets

for (event, market, line), sels in best_prices(rows, now).items():
    book_sum = sum(1 / odds for odds, _ in sels.values())
    label = market if line is None else f"{market} {line}"
    print(f"{event} {label}  (best-price book: {book_sum:.1%})")
    for sel, (odds, book) in sels.items():
        print(f"  {sel:6} {odds:.2f}  {book}")

Output:

ars-che 1x2  (best-price book: 101.1%)
  home   2.18  Bookmaker B
  draw   3.50  Bookmaker A
  away   3.75  Bookmaker B
ars-che total 2.5  (best-price book: 103.1%)
  over   1.95  Bookmaker A
  under  1.93  Bookmaker B

Bookmaker C’s 2.25 on the home win would have been the best price, but it was 14 minutes old. A stale best price is the most common way an odds comparison misleads its users: the price is gone when they click.

Step 4: sanity-check the result

Add up 1 ÷ odds for the best prices in a market. A normal result is slightly above 100%: 101.1% for the 1X2 above. Below 100% means the best prices together guarantee a profit, which is rare and short-lived.

When you see it, check first for a data problem: mismatched events, a different line, different settlement rules or a stale price. Most “arbitrage” in a new comparison product is a matching bug. Read more about the margin in how to remove the bookmaker margin.

Launch checklist

Checklist before launching an odds comparison
CheckWhyPriority
Hide prices older than your limitA stale best price is gone when users click.Must
Show when each price was updatedUsers trust a comparison they can verify.Must
Review uncertain event matchesA wrong match shows a best price for the wrong game.Must
Only show bookmakers allowed in each marketMany countries restrict promoting unlicensed gambling operators. This is not legal advice.Must
Alert on best prices adding up to under 100%Usually a data bug, sometimes arbitrage.Should
Estimate refresh needs and request volumeDrives cost; try the request calculator.Should

Using ValueOddsAPI for this

ValueOddsAPI delivers odds already normalised into events, markets and selections, including bookmakers that many global feeds miss, such as Danske Spil, Bet25 and NordicBet. Business plans start at $2,000/month (Pro, 20 bookmakers). See odds data for publishers.

Frequently asked questions

What is the hardest part of building an odds comparison?

Matching the same event and market across bookmakers. Names, market labels, lines and settlement rules all differ, and a single wrong match shows users a best price for the wrong game or the wrong line.

How fresh do the prices need to be?

It depends on the product. For pre-match comparison a few minutes is often acceptable if you show the update time; for live betting content you need seconds. Always hide prices older than your limit.

Why do my best prices add up to less than 100%?

Either you found a genuine arbitrage, which is rare and short-lived, or the data is mismatched: a different event, line or settlement rule, or a stale price. Check the data first.

Can I show any bookmaker to any user?

No. Many countries restrict which gambling operators may be promoted to their residents. Check the rules for each market you serve before displaying or linking to a bookmaker.

Related reading

ValueOddsAPI provides odds data, not betting advice. Betting involves risk of loss and is for adults (18+) only. If gambling stops being fun, contact your local support service, such as StopSpillet in Denmark.

Get the data into your product

Solo $999/month (personal use), Pro $2,000, Scale $4,500, Elite $9,000 and Enterprise from $15,000/month, excluding VAT. Coverage and usage rights are confirmed before activation. Missing a bookmaker? We add the sources you need, scoped and priced per project.