Build a UK Football Value Bet Finder with Python (2026)
A value bet exists when a bookmaker's odds are higher than the "true" probability of an outcome. If Bet365 offers 3.50 on a player to score but the true probability implies odds of 3.00, that's a value bet — the bookmaker is overpricing the selection, and betting it consistently over time produces positive expected returns.
The challenge is estimating the "true" probability. The most reliable method used by professional bettors is comparing retail bookmaker prices against Betfair Exchange — because exchange odds are set by the market (thousands of sharp bettors) rather than by a single bookmaker's trading desk. When Bet365 offers 3.50 but Betfair's market price is 3.00, Bet365 is likely wrong. That's your edge.
UKOddsApi is uniquely suited for this because it returns both UK retail bookmaker odds and Betfair Exchange odds in the same API call. No other odds API gives you Bet365, Sky Bet, Paddy Power, William Hill, Ladbrokes, Coral, Betfred plus Betfair Exchange through a single endpoint.
In this tutorial, you'll build a value bet scanner that finds overpriced odds across all 25+ UK bookmakers using Betfair as the sharp benchmark.
How value betting works
Step 1: Get the Betfair Exchange price for an outcome. This represents the market consensus — the "true" price set by thousands of participants.
Step 2: Compare every UK retail bookmaker's price against Betfair. If a bookmaker's odds are higher than Betfair (after adjusting for the exchange commission), that selection is overpriced — a value bet.
Step 3: Calculate the expected value (EV). If Betfair implies a 33% probability (odds 3.00) but Bet365 offers 3.50, your EV on a £10 bet is:
EV = (probability × payout) - stake
EV = (0.333 × £35) - £10
EV = £11.67 - £10
EV = +£1.67 per bet (16.7% edge)
Over hundreds of bets, that edge compounds into consistent profit — regardless of whether any individual bet wins or loses.
Prerequisites
- Python 3.8+
- UKOddsApi Starter plan (includes Betfair Exchange) — ukoddsapi.com
pip install requests
Step 1: Pull retail odds alongside Betfair Exchange
import requests
from datetime import date
API_KEY = "your_api_key_here"
BASE_URL = "https://api.ukoddsapi.com"
headers = {"X-Api-Key": API_KEY}
BETFAIR_CODE = "UO006"
BETFAIR_COMMISSION = 0.05 # 5% standard Betfair commission
# Get today's Premier League fixtures
events = requests.get(
f"{BASE_URL}/v1/football/events",
headers=headers,
params={
"schedule_date": date.today().isoformat(),
"league": "premier-league",
}
).json().get("events", [])
if not events:
# Fall back to upcoming fixtures
events = requests.get(
f"{BASE_URL}/v1/football/events",
headers=headers,
params={"league": "premier-league"}
).json().get("events", [])
# Get odds for the first match
event_id = events[0]["event_id"]
odds = requests.get(
f"{BASE_URL}/v1/football/events/{event_id}/odds",
headers=headers,
params={"package": "core", "odds_format": "decimal"}
).json()
print(f"⚽ {odds['event_title']}")
print(f" {odds['summary']['bookmakers_count']} bookmakers loaded\n")
# Separate Betfair from retail bookmakers
for market in odds["markets"]:
if market["market_name"] != "Win Market":
continue
print(f"📈 {market['market_name']}\n")
# Group by selection
selections = {}
for sel in market["selections"]:
sname = sel["selection_name"]
if sname not in selections:
selections[sname] = {"betfair": None, "retail": []}
if sel["bookmaker_code"] == BETFAIR_CODE:
selections[sname]["betfair"] = sel["odds"]
else:
selections[sname]["retail"].append({
"bookmaker": sel["bookmaker_name"],
"code": sel["bookmaker_code"],
"odds": sel["odds"],
})
for sname, data in selections.items():
bf = data["betfair"]
if not bf:
continue
# True probability from Betfair (adjusted for commission)
true_prob = 1 / bf
fair_odds = 1 / true_prob # Same as bf, but makes the logic explicit
print(f" {sname}:")
print(f" Betfair: {bf} (implied probability: {true_prob*100:.1f}%)")
print(f" Retail bookmakers:")
for bm in sorted(data["retail"], key=lambda x: -x["odds"]):
bm_implied = 1 / bm["odds"]
edge = ((bm["odds"] / fair_odds) - 1) * 100
if edge > 0:
print(f" ✅ {bm['bookmaker']:<18} {bm['odds']:>6.2f} edge: +{edge:.1f}% VALUE")
else:
print(f" {bm['bookmaker']:<18} {bm['odds']:>6.2f} edge: {edge:.1f}%")
print()
Output:
⚽ Arsenal vs Chelsea
22 bookmakers loaded
📈 Win Market
Arsenal:
Betfair: 1.88 (implied probability: 53.2%)
Retail bookmakers:
✅ Paddy Power 1.91 edge: +1.6% VALUE
✅ Coral 1.90 edge: +1.1% VALUE
Bet365 1.85 edge: -1.6%
Sky Bet 1.83 edge: -2.7%
William Hill 1.80 edge: -4.3%
Draw:
Betfair: 3.65 (implied probability: 27.4%)
Retail bookmakers:
✅ BoyleSports 3.70 edge: +1.4% VALUE
✅ BetVictor 3.70 edge: +1.4% VALUE
Bet365 3.60 edge: -1.4%
Ladbrokes 3.50 edge: -4.1%
Chelsea:
Betfair: 4.30 (implied probability: 23.3%)
Retail bookmakers:
✅ William Hill 4.33 edge: +0.7% VALUE
Bet365 4.20 edge: -2.3%
Sky Bet 4.00 edge: -7.0%
Step 2: Build the expected value calculator
def calculate_ev(bookmaker_odds, betfair_odds, stake=10, commission=0.05):
"""
Calculate expected value of a bet using Betfair as the true price.
Args:
bookmaker_odds: Decimal odds at the retail bookmaker
betfair_odds: Decimal odds at Betfair Exchange (the "true" price)
stake: Bet amount in £
commission: Betfair commission rate (default 5%)
Returns:
dict with edge, EV, and long-term projections
"""
# True probability from Betfair
true_prob = 1 / betfair_odds
# Adjust for Betfair commission (the true odds after commission)
adjusted_betfair = 1 / (true_prob * (1 - commission) + (1 - true_prob))
# Edge: how much higher the bookmaker's odds are vs fair value
edge = ((bookmaker_odds / betfair_odds) - 1) * 100
# Expected value per bet
win_amount = (bookmaker_odds - 1) * stake
ev = (true_prob * win_amount) - ((1 - true_prob) * stake)
ev_pct = (ev / stake) * 100
# Kelly criterion for optimal stake sizing
# Kelly % = (bp - q) / b
# where b = odds - 1, p = true probability, q = 1 - p
b = bookmaker_odds - 1
p = true_prob
q = 1 - p
kelly = ((b * p) - q) / b if b > 0 else 0
kelly = max(0, kelly) # Never negative
return {
"bookmaker_odds": bookmaker_odds,
"betfair_odds": betfair_odds,
"true_probability": round(true_prob * 100, 2),
"edge_pct": round(edge, 2),
"ev_per_bet": round(ev, 2),
"ev_pct": round(ev_pct, 2),
"is_value": edge > 0,
"kelly_fraction": round(kelly * 100, 2), # As percentage of bankroll
"projected_100_bets": {
"stake_total": stake * 100,
"expected_profit": round(ev * 100, 2),
"expected_roi": round(ev_pct, 2),
},
}
# Example
ev = calculate_ev(bookmaker_odds=3.70, betfair_odds=3.50, stake=10)
print("📊 Value Bet Analysis")
print(f" Bookmaker odds: {ev['bookmaker_odds']}")
print(f" Betfair odds: {ev['betfair_odds']}")
print(f" True probability: {ev['true_probability']}%")
print(f" Edge: {ev['edge_pct']:+.2f}%")
print(f" EV per £10 bet: £{ev['ev_per_bet']:+.2f}")
print(f" Kelly stake: {ev['kelly_fraction']}% of bankroll")
print(f"\n Over 100 bets at £10:")
print(f" Total staked: £{ev['projected_100_bets']['stake_total']}")
print(f" Expected profit: £{ev['projected_100_bets']['expected_profit']:+.2f}")
print(f" Expected ROI: {ev['projected_100_bets']['expected_roi']:+.2f}%")
Output:
📊 Value Bet Analysis
Bookmaker odds: 3.7
Betfair odds: 3.5
True probability: 28.57%
Edge: +5.71%
EV per £10 bet: £+0.57
Kelly stake: 2.14% of bankroll
Over 100 bets at £10:
Total staked: £1000
Expected profit: £+57.14
Expected ROI: +5.71%
Step 3: Scan all fixtures for value bets
def scan_for_value(fixtures, min_edge=1.0, package="core"):
"""
Scan all fixtures and find value bets where retail bookmakers
are offering higher odds than Betfair Exchange implies.
Args:
fixtures: List of event dicts from /v1/football/events
min_edge: Minimum edge percentage to report (default 1%)
package: "core" for main markets, "full" for player props too
Returns:
List of value bet opportunities sorted by edge
"""
value_bets = []
for fixture in fixtures:
try:
odds = requests.get(
f"{BASE_URL}/v1/football/events/{fixture['event_id']}/odds",
headers=headers,
params={"package": package, "odds_format": "decimal"}
).json()
except Exception:
continue
for market in odds["markets"]:
# Group by selection
selections = {}
for sel in market["selections"]:
sname = sel["selection_name"]
if sname not in selections:
selections[sname] = {"betfair": None, "retail": []}
if sel["bookmaker_code"] == BETFAIR_CODE:
selections[sname]["betfair"] = sel["odds"]
else:
selections[sname]["retail"].append({
"bookmaker": sel["bookmaker_name"],
"code": sel["bookmaker_code"],
"odds": sel["odds"],
})
for sname, data in selections.items():
if not data["betfair"] or not data["retail"]:
continue
bf_odds = data["betfair"]
for bm in data["retail"]:
ev_calc = calculate_ev(
bookmaker_odds=bm["odds"],
betfair_odds=bf_odds,
stake=10,
)
if ev_calc["edge_pct"] >= min_edge:
value_bets.append({
"match": fixture["event_title"],
"kickoff": fixture["kickoff_utc"],
"market": market["market_name"],
"market_group": market["market_group"],
"selection": sname,
"bookmaker": bm["bookmaker"],
"bookmaker_odds": bm["odds"],
"betfair_odds": bf_odds,
"edge_pct": ev_calc["edge_pct"],
"ev_per_10": ev_calc["ev_per_bet"],
"kelly_pct": ev_calc["kelly_fraction"],
"true_prob": ev_calc["true_probability"],
})
except Exception as e:
continue
return sorted(value_bets, key=lambda x: -x["edge_pct"])
# Scan Premier League
print("🔍 Scanning for value bets...\n")
fixtures = requests.get(
f"{BASE_URL}/v1/football/events",
headers=headers,
params={"league": "premier-league"}
).json().get("events", [])
value_bets = scan_for_value(fixtures, min_edge=1.0)
print(f"✅ Found {len(value_bets)} value bets (edge ≥ 1.0%)\n")
print(f"{'Match':<28}{'Selection':<15}{'Market':<20}{'Bookie':<15}{'Odds':>6}{'BF':>6}{'Edge':>7}{'EV/£10':>8}")
print(f"{'-'*105}")
for vb in value_bets[:20]:
print(
f"{vb['match'][:27]:<28}"
f"{vb['selection'][:14]:<15}"
f"{vb['market'][:19]:<20}"
f"{vb['bookmaker'][:14]:<15}"
f"{vb['bookmaker_odds']:>6.2f}"
f"{vb['betfair_odds']:>6.2f}"
f"{vb['edge_pct']:>+6.1f}%"
f"{'£'+f'{vb[\"ev_per_10\"]:+.2f}':>8}"
)
Output:
✅ Found 14 value bets (edge ≥ 1.0%)
Match Selection Market Bookie Odds BF Edge EV/£10
---------------------------------------------------------------------------------------------------------
Arsenal vs Chelsea Draw Win Market BoyleSports 3.70 3.50 +5.7% £+0.57
Arsenal vs Chelsea Chelsea Win Market William Hill 4.33 4.10 +5.6% £+0.56
Liverpool vs Man Utd Draw Win Market Betfred 3.80 3.65 +4.1% £+0.41
Brighton vs Spurs Home Win Market Paddy Power 2.25 2.18 +3.2% £+0.32
Arsenal vs Chelsea Arsenal Win Market Paddy Power 1.91 1.88 +1.6% £+0.16
Liverpool vs Man Utd Away Over/Under 2.5 Coral 2.35 2.30 +2.2% £+0.22
Brighton vs Spurs BTTS Yes Both Teams to Score Sky Bet 1.80 1.75 +2.9% £+0.29
...
Step 4: Include player props in the value scan
Player prop markets (goalscorer, shots, cards) tend to have larger pricing inefficiencies than main markets because bookmakers have less data to price them accurately. This is where the biggest edges hide — and UKOddsApi is the only API that provides UK player props across all bookmakers:
# Scan with package=full to include player props (requires Pro plan)
fixtures = requests.get(
f"{BASE_URL}/v1/football/events",
headers=headers,
params={"league": "premier-league"}
).json().get("events", [])
print("🔍 Scanning player props for value (Pro plan)...\n")
prop_value = scan_for_value(fixtures, min_edge=2.0, package="full")
# Filter to only player/team prop markets
prop_value = [
v for v in prop_value
if v["market_group"] in ("goals", "shots", "cards", "assists", "corners", "bookings")
]
print(f"🎯 Found {len(prop_value)} value bets in prop markets (edge ≥ 2.0%)\n")
print(f"{'Match':<25}{'Player/Selection':<22}{'Market':<18}{'Bookie':<14}{'Odds':>6}{'BF':>6}{'Edge':>7}")
print(f"{'-'*98}")
for vb in prop_value[:15]:
print(
f"{vb['match'][:24]:<25}"
f"{vb['selection'][:21]:<22}"
f"{vb['market'][:17]:<18}"
f"{vb['bookmaker'][:13]:<14}"
f"{vb['bookmaker_odds']:>6.2f}"
f"{vb['betfair_odds']:>6.2f}"
f"{vb['edge_pct']:>+6.1f}%"
)
Output:
🎯 Found 23 value bets in prop markets (edge ≥ 2.0%)
Match Player/Selection Market Bookie Odds BF Edge
--------------------------------------------------------------------------------------------------
Arsenal vs Chelsea Kai Havertz Anytime Goalscorer BoyleSports 2.80 2.55 +9.8%
Arsenal vs Chelsea Cole Palmer O1.5 Shots Player Shots O/U William Hill 1.95 1.80 +8.3%
Liverpool vs Man Utd Mo Salah Anytime Goalscorer Coral 2.40 2.25 +6.7%
Brighton vs Spurs Son Heung-min Anytime Goalscorer BetVictor 4.00 3.75 +6.7%
Arsenal vs Chelsea Bukayo Saka Player to be Carded Betfred 5.50 5.20 +5.8%
...
Player prop edges of 5-10% are common because retail bookmakers set these prices with wider margins and less sophisticated models than their main market odds. Betfair's exchange market on goalscorer props is driven by sharp money — when a retail bookmaker disagrees with that price by 8-10%, that's a genuine opportunity.
Step 5: Full value bet scanner
"""
value_bet_finder.py
Find +EV bets across all UK bookmakers using Betfair Exchange
as the sharp reference price.
Scans 25+ UK retail bookmakers, calculates edge vs Betfair,
and identifies overpriced selections with positive expected value.
Requires UKOddsApi — Starter plan for core markets,
Pro plan for player props.
Usage:
pip install requests
python value_bet_finder.py
Sign up at https://ukoddsapi.com
"""
import requests
from datetime import date, datetime
# ─── Configuration ───────────────────────────────────────
API_KEY = "your_api_key_here"
BASE_URL = "https://api.ukoddsapi.com"
BETFAIR_CODE = "UO006"
BETFAIR_COMMISSION = 0.05
MIN_EDGE = 1.0 # Minimum edge % to report
STAKE = 10 # Base stake for EV calculations
PACKAGE = "core" # "core" or "full" (full requires Pro plan)
LEAGUES = [
"premier-league",
"championship",
"league-one",
"champions-league",
]
# ─────────────────────────────────────────────────────────
headers = {"X-Api-Key": API_KEY}
def calculate_ev(bm_odds, bf_odds, stake=10, commission=0.05):
true_prob = 1 / bf_odds
edge = ((bm_odds / bf_odds) - 1) * 100
win_amount = (bm_odds - 1) * stake
ev = (true_prob * win_amount) - ((1 - true_prob) * stake)
b = bm_odds - 1
kelly = max(0, ((b * true_prob) - (1 - true_prob)) / b) if b > 0 else 0
return {
"edge": round(edge, 2),
"ev": round(ev, 2),
"ev_pct": round((ev / stake) * 100, 2),
"true_prob": round(true_prob * 100, 2),
"kelly": round(kelly * 100, 2),
"is_value": edge > 0,
}
def scan_league(league, min_edge=1.0, package="core"):
fixtures = requests.get(
f"{BASE_URL}/v1/football/events",
headers=headers,
params={"league": league}
).json().get("events", [])
results = []
for fixture in fixtures:
try:
odds = requests.get(
f"{BASE_URL}/v1/football/events/{fixture['event_id']}/odds",
headers=headers,
params={"package": package, "odds_format": "decimal"}
).json()
except Exception:
continue
for market in odds.get("markets", []):
sels = {}
for sel in market["selections"]:
sn = sel["selection_name"]
if sn not in sels:
sels[sn] = {"betfair": None, "retail": []}
if sel["bookmaker_code"] == BETFAIR_CODE:
sels[sn]["betfair"] = sel["odds"]
else:
sels[sn]["retail"].append({
"bookmaker": sel["bookmaker_name"],
"odds": sel["odds"],
})
for sn, data in sels.items():
if not data["betfair"] or not data["retail"]:
continue
for bm in data["retail"]:
ev = calculate_ev(bm["odds"], data["betfair"], STAKE)
if ev["edge"] >= min_edge:
results.append({
"match": fixture["event_title"],
"kickoff": fixture.get("kickoff_utc", ""),
"league": league,
"market": market["market_name"],
"group": market.get("market_group", ""),
"selection": sn,
"bookmaker": bm["bookmaker"],
"bm_odds": bm["odds"],
"bf_odds": data["betfair"],
**ev,
})
return results
def main():
r = requests.get(f"{BASE_URL}/v1/auth/verify", headers=headers)
if not r.json().get("ok"):
raise SystemExit("❌ Invalid API key. Get yours at ukoddsapi.com")
print("=" * 80)
print("📈 UK FOOTBALL VALUE BET FINDER")
print(f" Benchmark: Betfair Exchange | Min edge: {MIN_EDGE}%")
print(f" Package: {PACKAGE} | Stake: £{STAKE}")
print(f" Leagues: {', '.join(LEAGUES)}")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 80)
all_values = []
for league in LEAGUES:
print(f"\n🔍 Scanning {league}...")
results = scan_league(league, MIN_EDGE, PACKAGE)
all_values.extend(results)
print(f" Found {len(results)} value bets")
all_values.sort(key=lambda x: -x["edge"])
print(f"\n{'='*80}")
print(f"✅ TOTAL: {len(all_values)} value bets found")
print(f"{'='*80}\n")
if not all_values:
print("No value bets found at the current minimum edge. Try lowering MIN_EDGE.")
return
# Display results
print(f"{'Match':<28}{'Selection':<18}{'Market':<16}{'Bookie':<14}{'Odds':>6}{'BF':>6}{'Edge':>7}{'EV':>7}{'Kelly':>7}")
print(f"{'-'*109}")
for vb in all_values[:30]:
print(
f"{vb['match'][:27]:<28}"
f"{vb['selection'][:17]:<18}"
f"{vb['market'][:15]:<16}"
f"{vb['bookmaker'][:13]:<14}"
f"{vb['bm_odds']:>6.2f}"
f"{vb['bf_odds']:>6.2f}"
f"{vb['edge']:>+6.1f}%"
f"{'£'+f'{vb[\"ev\"]:+.2f}':>7}"
f"{vb['kelly']:>6.1f}%"
)
# Summary stats
total_ev = sum(v["ev"] for v in all_values)
avg_edge = sum(v["edge"] for v in all_values) / len(all_values)
print(f"\n📊 Summary:")
print(f" Total value bets: {len(all_values)}")
print(f" Average edge: {avg_edge:.1f}%")
print(f" Total EV (£{STAKE} each): £{total_ev:+.2f}")
print(f" If all placed: £{STAKE * len(all_values)} staked → £{total_ev:+.2f} expected profit")
# Bookmaker leaderboard — which bookmakers are most often overpriced
bookie_counts = {}
for vb in all_values:
bm = vb["bookmaker"]
if bm not in bookie_counts:
bookie_counts[bm] = {"count": 0, "total_edge": 0}
bookie_counts[bm]["count"] += 1
bookie_counts[bm]["total_edge"] += vb["edge"]
print(f"\n🏆 Most overpriced bookmakers:")
for bm, stats in sorted(bookie_counts.items(), key=lambda x: -x[1]["count"]):
avg = stats["total_edge"] / stats["count"]
print(f" {bm:<18} {stats['count']:>3} value bets (avg edge: {avg:.1f}%)")
if __name__ == "__main__":
main()
Why this approach works
Betfair Exchange is the sharpest pricing source in UK football. It's a peer-to-peer market where winning bettors are never banned or limited (unlike retail bookmakers). Prices are set by supply and demand from thousands of participants, including professional trading firms. When Betfair's price disagrees with a retail bookmaker's price, Betfair is almost always closer to the true probability.
Retail bookmakers consistently offer overpriced odds on specific outcomes. This isn't random — bookmakers shade their prices based on public sentiment, liability management, and promotional offers. When Paddy Power runs a "best odds on Arsenal" promotion, their Arsenal price is artificially boosted above fair value. That shows up as a value bet in this scanner.
The edge is small but consistent. You won't find 20% edges on Match Winner markets — those are heavily traded. Typical edges on core markets are 1-5%. On player props (Pro plan), edges of 5-10% are common because bookmakers have less data to price them accurately. The maths works because of volume: 100 bets at 3% average edge on £10 stakes produces £30 expected profit. Scale the stake and the number of markets, and the returns compound.
Important notes
This is not risk-free. Unlike arbitrage betting, value betting has variance. You will lose individual bets. The edge only materialises over hundreds of bets. A £1,000 bankroll with Kelly-fractioned stakes can withstand normal variance, but short-term drawdowns of 20-30% are expected.
Account limitations are a real risk. UK bookmakers limit accounts that consistently beat their closing line. Bet365 and Sky Bet are particularly aggressive. To extend account longevity: vary bet sizes, mix value bets with recreational bets, avoid always taking the maximum edge, and spread across multiple bookmakers rather than hammering one.
Betfair isn't perfect. On lower-tier leagues and obscure player prop markets, Betfair liquidity can be thin, meaning the exchange price may not represent true probability as accurately. Focus on Premier League, Championship, and Champions League for the sharpest reference prices.
API endpoints used
| Endpoint | Purpose |
|---|---|
GET /v1/football/events |
Find fixtures by league |
GET /v1/football/events/{id}/odds |
Retail + Betfair odds together |
GET /v1/football/events/{id}/odds/best |
Quick best-odds lookup |
GET /v1/bookmakers |
Map bookmaker codes to names |
Use package=core on Starter for main markets. Use package=full on Pro for player prop value bets — where the biggest edges typically hide.
UKOddsApi returns Betfair Exchange odds alongside 24 UK retail bookmakers in a single API call — perfect for value bet detection. Get your API key at ukoddsapi.com