18+ Only  |  Gambling can be addictive — please gamble responsibly  |  Gambling Help: 1800 858 858  |  GambleAware

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

(18+, gamble responsibly, Gambling Help 1800 858 858.)

Get a free API key →

This site contains wagering-related analysis and is intended for Australian users aged 18+. Gambling involves risk. Please gamble responsibly.