Build a betting model data pipeline in Python (free odds API)
A betting model is only as good as the data feeding it. Before any clever modelling, you need clean, consistent odds features: current prices, the market consensus, the de-vigged (margin-removed) probabilities, and how the line has moved. In this tutorial we'll build that data pipeline in Python with the free PuntersEdge AU Odds API — no per-bookmaker scraping.
⚠️ 18+ only. This is a data-engineering tutorial. Models are uncertain; past performance doesn't predict results. Bet only what you can afford to lose. Not financial advice. Gambling Help: 1800 858 858.
Get a free key
Free key at puntersedge.online/api-platform, passed in X-API-Key.
import requests
BASE = "https://api.puntersedge.online/v1"
HEADERS = {"X-API-Key": "YOUR_FREE_KEY"}
Step 1 — current odds → implied probability
/v1/best-odds/{sport} gives the best price per selection plus every book's price in all_prices. Implied probability is 1 / decimal_odds:
def best_odds(sport):
r = requests.get(f"{BASE}/best-odds/{sport}", headers=HEADERS, timeout=15)
r.raise_for_status()
return r.json()
for ev in best_odds("afl")[:3]:
print(ev["home_team"], "v", ev["away_team"])
for s in ev["selections"]:
imp = 1 / s["best_price"]
print(f" {s['name']:<22} {s['best_price']} implied {imp:.1%}")
Step 2 — market consensus and de-vigging
The best price overstates the true chance; the *average* across books is a better consensus, and removing the bookmaker margin (the "vig") gives a fair probability. For a two-way market:
def features(event):
sels = event["selections"]
# consensus = mean of each book's price per selection
cons = []
for s in sels:
prices = [p["price"] for p in s["all_prices"]]
cons.append(sum(prices) / len(prices))
raw = [1 / c for c in cons] # implied probs (still include vig)
overround = sum(raw)
fair = [p / overround for p in raw] # de-vigged: sums to 1.0
return [{
"selection": s["name"],
"best": s["best_price"],
"consensus": round(c, 3),
"fair_prob": round(f, 4),
"edge_vs_best": round((1/s["best_price"]) - f, 4), # best - fair
} for s, c, f in zip(sels, cons, fair)]
import json
print(json.dumps(features(best_odds("afl")[0]), indent=2))
edge_vs_best is the gap between the best available implied probability and the fair probability — a first-pass value signal you can feed a model or rank on.
Step 3 — add line-movement features from history
/v1/sports/{sport}/odds/history gives every snapshot, so you can add opening price and drift as features:
def history(sport):
return requests.get(f"{BASE}/sports/{sport}/odds/history", headers=HEADERS, timeout=15).json()
ev = history("afl")[0]
snaps = sorted(ev["snapshots"], key=lambda s: s["recorded_at"])
opens = {s["bookmaker"]: s["home_price"] for s in snaps if s["bookmaker"] not in {}} # first seen
print(ev["home_team"], "open->latest home price:",
snaps[0]["home_price"], "->", snaps[-1]["home_price"])
Step 4 — assemble a model-ready frame
Stitch it into one row per selection — drop it straight into pandas:
rows = []
for ev in best_odds("afl"):
for f in features(ev):
rows.append({"match": f"{ev['home_team']} v {ev['away_team']}", **f})
import pandas as pd
df = pd.DataFrame(rows).sort_values("edge_vs_best", ascending=False)
print(df.head(10).to_string(index=False))
That's a reusable feature pipeline: best price, consensus, de-vigged fair probability, an edge signal, and movement — ready for your model or a simple value filter.
Wrap up
- Free key + docs: https://puntersedge.online/api-platform
- Also on RapidAPI: https://rapidapi.com/Propertyscout001/api/puntersedge-au-odds
- Endpoints:
/v1/best-odds/{sport},/v1/sports/{sport}/odds/history
(18+, gamble responsibly, Gambling Help 1800 858 858.)