Build an odds comparison screen in Python and HTML
Show every bookmaker's price for each game side by side and highlight the best one, so you always take the top available price.
Guide ยท Updated June 2026
An odds comparison screen, or line shopping grid, lists every bookmaker's price for a game side by side and highlights the best price on each side. It is the simplest betting tool to build and one of the most useful, because always taking the top price adds up over time. This guide builds one: a small Python backend pulls head to head odds from the RapidOddsAPI REST endpoint and shapes them into a grid, and a plain HTML and CSS page renders it with the best price marked.
There is no clever maths here. The value is in seeing every book at once and never leaving money on the table by betting at a worse price than you had to. We will build it in two halves: a backend that fetches and shapes the data, and a front end that draws the grid. Keeping the fetch on the backend also keeps your API key off the browser, which matters, since a key in client side code is visible to anyone.
Step 1: pull the odds and build a grid
Start in Python. Pull head to head odds, group the per book entries for each game, then build a grid: for each game, every book's price on each team, plus the best price on each side.
import requests
API_KEY = "your_api_key"
SPORT = "MLB"
BOOKMAKERS = [
"Bet365", "Sportsbet", "TAB", "Pointsbet",
"Ladbrokes", "Unibet", "Dabble",
]
def group_by_matchup(games):
merged = {}
for g in games:
key = (g["game"]["home_team"], g["game"]["away_team"])
if key not in merged:
merged[key] = {"game": g["game"], "bookmakers": []}
merged[key]["bookmakers"].extend(g["bookmakers"])
return list(merged.values())
def build_grid(game):
away = game["game"]["away_team"]
home = game["game"]["home_team"]
rows = {} # {book: {team: price}}
best = {away: 0.0, home: 0.0} # best price on each side
for book in game["bookmakers"]:
for market in book["markets"]:
if market["key"] != "head_to_head":
continue
for o in market["outcomes"]:
team, price = o["name"], o["price"]
rows.setdefault(book["name"], {})[team] = price
if price > best.get(team, 0):
best[team] = price
return {"away": away, "home": home, "rows": rows, "best": best}
See it in the terminal first
Before wiring up a page, print the grid to check the shape. Marking the best price with a star makes it obvious.
def main():
resp = requests.get(
f"https://api.rapidoddsapi.com/sports/{SPORT}/markets",
params={
"api_key": API_KEY,
"market_type": ["head_to_head"],
"bookmaker": BOOKMAKERS,
},
timeout=30,
)
resp.raise_for_status()
for game in group_by_matchup(resp.json()["games"]):
grid = build_grid(game)
away, home, rows, best = grid["away"], grid["home"], grid["rows"], grid["best"]
print(f"{away} at {home}")
for book, prices in rows.items():
a, h = prices.get(away), prices.get(home)
a = f"{a}{' *' if a == best[away] else ''}" if a else "-"
h = f"{h}{' *' if h == best[home] else ''}" if h else "-"
print(f" {book:<11} {away}: {a:<10} {home}: {h}")
print()
if __name__ == "__main__":
main()
Running it prints a grid like this. The star is the best price on each side, which is the one to take.
Kansas City Royals at Washington Nationals
Sportsbet Kansas City Royals: 2.0 Washington Nationals: 1.81 *
Dabble Kansas City Royals: 2.08 Washington Nationals: 1.75
Pointsbet Kansas City Royals: 2.1 Washington Nationals: 1.77
Ladbrokes Kansas City Royals: 2.1 Washington Nationals: 1.75
Bet365 Kansas City Royals: 2.15 * Washington Nationals: 1.74
Unibet Kansas City Royals: 2.07 Washington Nationals: 1.78
TAB Kansas City Royals: 2.1 Washington Nationals: 1.75
This is line shopping in one screen. The Royals range from 2.00 at Sportsbet to 2.15 at Bet365. Same bet, but 2.15 pays 7.5 percent more than 2.00 when it wins. Take that better price on every bet and it compounds into a real difference over a season.
Step 2: serve the grid as JSON
To draw the grid in a browser, the page needs the data. Serve it from a small backend so the API key stays on the server. Python's built in HTTP server is enough for a local tool. This returns the list of game grids as JSON.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
def get_grids():
resp = requests.get(
f"https://api.rapidoddsapi.com/sports/{SPORT}/markets",
params={
"api_key": API_KEY,
"market_type": ["head_to_head"],
"bookmaker": BOOKMAKERS,
},
timeout=30,
)
resp.raise_for_status()
return [build_grid(g) for g in group_by_matchup(resp.json()["games"])]
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/odds":
data = json.dumps(get_grids()).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(data)
else: # serve the page at /
with open("screen.html", "rb") as f:
body = f.read()
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(body)
HTTPServer(("localhost", 8000), Handler).serve_forever()
Run it, then open http://localhost:8000. The page is served at the root, and it fetches the data from /odds.
Step 3: the screen, in plain HTML and CSS
Save this as screen.html next to the script. It fetches /odds, builds one table per game, and highlights the best price in each column. No framework, no build step, just a file you can open.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Odds Screen</title>
<style>
body { font-family: system-ui, sans-serif; background: #0f172a;
color: #e2e8f0; padding: 24px; }
h2 { font-size: 18px; margin: 28px 0 10px; }
table { border-collapse: collapse; width: 100%; max-width: 560px; }
th, td { text-align: left; padding: 8px 14px; border-bottom: 1px solid #1e293b; }
th { color: #94a3b8; font-weight: 600; }
.book { color: #94a3b8; }
.best { color: #4ade80; font-weight: 700; }
</style>
</head>
<body>
<div id="screen">Loading...</div>
<script>
async function load() {
const games = await (await fetch("/odds")).json()
const root = document.getElementById("screen")
root.innerHTML = ""
for (const g of games) {
const h = document.createElement("div")
let rows = ""
for (const [book, prices] of Object.entries(g.rows)) {
const a = prices[g.away], hm = prices[g.home]
const aCls = a === g.best[g.away] ? "best" : ""
const hCls = hm === g.best[g.home] ? "best" : ""
rows += `<tr><td class="book">${book}</td>
<td class="${aCls}">${a ?? "-"}</td>
<td class="${hCls}">${hm ?? "-"}</td></tr>`
}
h.innerHTML = `<h2>${g.away} at ${g.home}</h2>
<table>
<tr><th>Book</th><th>${g.away}</th><th>${g.home}</th></tr>
${rows}
</table>`
root.appendChild(h)
}
}
load()
</script>
</body>
</html>
Open the page and you get a clean board per game, with the best price on each team in green. That is a working odds comparison screen, and the whole thing is a Python file and an HTML file.
Taking it further
- More markets. The grid is built on head to head, but the same shape works for totals and spreads once you group by the point line. See the coverage page for the market keys.
- More sports. Change SPORT to show NBA, NFL, AFL, and the rest. The grid code does not change.
- Live updates. A screen is most useful when it never shows a stale price. Stream the feed over WebSocket and update the cells as prices move, with no polling loop.
- Add a click out. The API can include a deep link to each book's market, so the best price can become a button that takes you straight there.
Next steps
You have a working odds comparison screen. For tools that find an edge in the same data, see the positive EV scanner, the arbitrage scanner, and the middles scanner. For the full picture, see what you can build with an odds API, or read the API documentation.
Start building with RapidOddsAPI
Real-time, standardised odds from 100+ bookmakers over REST and WebSocket. Start free with 250 credits, no credit card required.