Build a UK Football Arbitrage Scanner in Python (2026)
Arbitrage betting — placing bets on every outcome of an event across different bookmakers to guarantee a profit regardless of the result — relies on one thing: comprehensive, real-time odds data from multiple bookmakers. The more bookmakers you cover, the more arbs you find.
Most odds APIs cover 5-15 bookmakers, mostly US-focused. UKOddsApi covers 25+ UK bookmakers including Bet365, Sky Bet, Paddy Power, William Hill, Ladbrokes, Coral, Betfred, BetVictor, BoyleSports, and UK exchanges like Betfair and Matchbook. More UK bookmakers means more pricing disagreements, which means more arbitrage opportunities.
Better still, UKOddsApi has a built-in arbitrage endpoint that does the heavy lifting for you — no need to build your own normalisation and comparison engine.
In this tutorial, you'll build a complete UK football arbitrage scanner that finds guaranteed-profit opportunities across every major UK bookmaker.
What is arbitrage betting?
Arbitrage works when bookmakers disagree on the probability of outcomes enough that you can back every possible result and still profit. Here's a simplified example:
Cardiff City vs Northampton — Total Away Corners (Over/Under 1.5)
- William Hill: Over 1.5 at 1.50
- BoyleSports: Under 1.5 at 3.80
Implied probabilities:
- Over 1.5: 1/1.50 = 66.7%
- Under 1.5: 1/3.80 = 26.3%
- Total: 93.0% (below 100% = arbitrage exists)
Profit margin: 7.5% — meaning a £300 total stake guarantees ~£22.50 profit no matter what happens.
Prerequisites
- Python 3.8+
- A UKOddsApi Scale plan (the arbitrage endpoint is available on Scale)
pip install requests
Step 1: Find arbitrage opportunities
UKOddsApi's arbitrage endpoint scans all 25+ UK bookmakers and returns pre-calculated arb opportunities with profit percentages and stake plans:
import requests
from datetime import date
API_KEY = "your_api_key_here"
BASE_URL = "https://api.ukoddsapi.com"
headers = {"X-Api-Key": API_KEY}
# Find today's arbitrage opportunities
response = requests.get(
f"{BASE_URL}/v1/football/arbitrage",
headers=headers,
params={
"date": date.today().isoformat(),
"min_profit": 0.5, # Minimum 0.5% profit
}
)
arb_data = response.json()
print(f"🔍 Found {arb_data['count']} arbitrage opportunities today\n")
Step 2: Display arbs with full details
Each arb response includes the event, market, bookmakers, odds, and a calculated stake plan:
for arb in arb_data["arbitrages"]:
profit = arb["profit_percentage"]
# Colour-code by profit level
if profit >= 5:
flag = "🔥"
elif profit >= 2:
flag = "✅"
else:
flag = "💡"
print(f"{flag} {arb['event_title']}")
print(f" Competition: {arb['competition']}")
print(f" Market: {arb['market_name']} ({arb['market_group']})")
print(f" Type: {arb['description']}")
print(f" Profit: {profit:.2f}%")
print(f" Implied probability: {arb['implied_probability']:.2f}%")
print()
# Show each leg
for leg in arb["legs"]:
print(f" 📗 {leg['selection']} @ {leg['odds']}")
print(f" Bookmaker: {leg['bookmaker_name']}")
print(f" Stake: {leg['stake_percentage']:.1f}% of bankroll")
print()
print(f" {'-'*50}")
print()
Output:
🔥 Cardiff City vs Northampton
Competition: English League One
Market: Total Away Corners (corners)
Type: Over 1.5 vs Under 1.5
Profit: 7.55%
Implied probability: 92.98%
📗 Over 1.5 @ 1.5
Bookmaker: William Hill
Stake: 71.7% of bankroll
📗 Under 1.5 @ 3.8
Bookmaker: BoyleSports
Stake: 28.3% of bankroll
--------------------------------------------------
Step 3: Calculate exact stakes for your bankroll
The arbitrage endpoint accepts a total_stake parameter that returns a ready-to-use stake plan with exact amounts, returns, and guaranteed profit:
# Get arbs with a £500 stake plan
response = requests.get(
f"{BASE_URL}/v1/football/arbitrage",
headers=headers,
params={
"date": date.today().isoformat(),
"min_profit": 0.5,
"total_stake": 500, # Calculate stakes for £500 total
"round_to": 1, # Round to nearest £1
}
)
arb_data = response.json()
for arb in arb_data["arbitrages"]:
plan = arb["stake_plan"]
print(f"\n⚽ {arb['event_title']}")
print(f" {arb['market_name']}: {arb['description']}")
print(f"\n 💰 Stake Plan (£{plan['total_stake']:.0f} total)")
print(f" {'Bookmaker':<18}{'Selection':<16}{'Odds':>6}{'Stake':>10}{'Returns':>10}")
print(f" {'-'*60}")
for leg in plan["legs"]:
print(
f" {leg['bookmaker_name']:<18}"
f"{leg['selection']:<16}"
f"{leg['odds']:>6.2f}"
f"{'£'+str(leg['stake_amount_final']):>10}"
f"{'£'+f'{leg[\"return_amount\"]:.2f}':>10}"
)
print(f"\n Guaranteed return: £{plan['sure_return']:.2f}")
print(f" Guaranteed profit: £{plan['sure_profit']:.2f}")
print(f" ROI: {plan['roi_percent']:.1f}%")
Output:
⚽ Cardiff City vs Northampton
Total Away Corners: Over 1.5 vs Under 1.5
💰 Stake Plan (£500 total)
Bookmaker Selection Odds Stake Returns
------------------------------------------------------------
William Hill Over 1.5 1.50 £358 £537.00
BoyleSports Under 1.5 3.80 £142 £539.60
Guaranteed return: £537.00
Guaranteed profit: £37.00
ROI: 7.4%
Step 4: Build a real-time arb scanner
Let's put it all together into a scanner that checks for arbs continuously and alerts you when opportunities appear:
"""
uk_arb_scanner.py
Real-time UK football arbitrage scanner.
Checks all 25+ UK bookmakers for guaranteed-profit opportunities.
Uses UKOddsApi — Scale plan required for arbitrage endpoint.
Sign up at https://ukoddsapi.com
Usage:
pip install requests
python uk_arb_scanner.py
"""
import requests
import time
from datetime import date, datetime
# ─── Configuration ───────────────────────────────────────
API_KEY = "your_api_key_here"
BASE_URL = "https://api.ukoddsapi.com"
BANKROLL = 500 # Total stake per arb in £
MIN_PROFIT = 0.5 # Minimum profit percentage
SCAN_INTERVAL = 60 # Seconds between scans
ROUND_STAKES = 1 # Round stakes to nearest £1
# ─────────────────────────────────────────────────────────
headers = {"X-Api-Key": API_KEY}
seen_arbs = set() # Track already-seen opportunities
def scan_arbitrage():
"""Fetch current arbitrage opportunities."""
try:
response = requests.get(
f"{BASE_URL}/v1/football/arbitrage",
headers=headers,
params={
"date": date.today().isoformat(),
"min_profit": MIN_PROFIT,
"total_stake": BANKROLL,
"round_to": ROUND_STAKES,
}
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
print(f"⏳ Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
return []
response.raise_for_status()
return response.json().get("arbitrages", [])
except requests.exceptions.RequestException as e:
print(f"❌ API error: {e}")
return []
def arb_key(arb):
"""Create a unique key for deduplication."""
legs = tuple(
(leg["bookmaker_code"], leg["selection"], leg["odds"])
for leg in arb["legs"]
)
return (arb["event_id"], arb["market_id"], legs)
def display_arb(arb):
"""Pretty-print an arbitrage opportunity."""
profit = arb["profit_percentage"]
plan = arb.get("stake_plan", {})
# Severity indicator
if profit >= 5:
icon = "🔥🔥🔥"
elif profit >= 3:
icon = "🔥🔥"
elif profit >= 1:
icon = "🔥"
else:
icon = "💡"
print(f"\n{'='*65}")
print(f"{icon} NEW ARB: {profit:.2f}% profit")
print(f"{'='*65}")
print(f"⚽ {arb['event_title']}")
print(f" {arb['competition']}")
print(f" Kick-off: {arb['kickoff_utc']}")
print(f" Market: {arb['market_name']} — {arb['description']}")
print()
if plan and "legs" in plan:
print(f" 💰 Stake Plan (£{plan['total_stake']:.0f})")
print(f" {'Bookmaker':<18}{'Bet':<20}{'Odds':>6}{'Stake':>10}{'Return':>10}")
print(f" {'-'*64}")
for leg in plan["legs"]:
print(
f" {leg['bookmaker_name']:<18}"
f"{leg['selection']:<20}"
f"{leg['odds']:>6.2f}"
f"{'£'+str(leg['stake_amount_final']):>10}"
f"{'£'+f'{leg[\"return_amount\"]:.2f}':>10}"
)
print()
print(f" ✅ Guaranteed profit: £{plan['sure_profit']:.2f} ({plan['roi_percent']:.1f}% ROI)")
else:
for leg in arb["legs"]:
print(f" 📗 {leg['selection']} @ {leg['odds']} — {leg['bookmaker_name']} ({leg['stake_percentage']:.1f}%)")
print()
def main():
# Verify API key
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("=" * 65)
print("🇬🇧 UK FOOTBALL ARBITRAGE SCANNER")
print(f" Scanning 25+ UK bookmakers every {SCAN_INTERVAL}s")
print(f" Bankroll: £{BANKROLL} | Min profit: {MIN_PROFIT}%")
print("=" * 65)
print()
scan_count = 0
total_arbs_found = 0
while True:
scan_count += 1
now = datetime.now().strftime("%H:%M:%S")
arbs = scan_arbitrage()
new_arbs = []
for arb in arbs:
key = arb_key(arb)
if key not in seen_arbs:
seen_arbs.add(key)
new_arbs.append(arb)
if new_arbs:
total_arbs_found += len(new_arbs)
print(f"\n🔔 [{now}] Scan #{scan_count}: {len(new_arbs)} NEW arb(s) found!")
for arb in sorted(new_arbs, key=lambda a: -a["profit_percentage"]):
display_arb(arb)
else:
print(f"[{now}] Scan #{scan_count}: No new arbs. "
f"({len(seen_arbs)} tracked, {total_arbs_found} total found)")
time.sleep(SCAN_INTERVAL)
if __name__ == "__main__":
main()
Step 5: Check arbs on a specific match
If you're interested in a particular fixture, use the event-specific arbitrage endpoint:
event_id = "evt_arsenal_chelsea_2026_04_25"
response = requests.get(
f"{BASE_URL}/v1/football/events/{event_id}/arbitrage",
headers=headers,
params={
"min_profit": 0.5,
"total_stake": 300,
}
)
arbs = response.json()
print(f"⚽ {arbs.get('event_title', event_id)}")
print(f" Found {arbs['count']} arb(s) on this match\n")
for arb in arbs["arbitrages"]:
print(f" 📈 {arb['market_name']}: {arb['description']}")
print(f" Profit: {arb['profit_percentage']:.2f}%")
for leg in arb["legs"]:
print(f" {leg['selection']} @ {leg['odds']} — {leg['bookmaker_name']}")
print()
Why UK bookmaker coverage matters for arb detection
The number of arbitrage opportunities scales with the number of bookmakers you monitor. Here's the maths:
- 5 bookmakers: 10 possible pairs to compare
- 10 bookmakers: 45 possible pairs
- 25 bookmakers: 300 possible pairs
More pairs = more pricing disagreements = more arbs. That's why UKOddsApi's coverage of 25+ UK bookmakers finds arbs that other APIs miss entirely. If you're only scanning Bet365, Unibet, and William Hill (what most global APIs offer for the UK), you're checking 3 pairs. With UKOddsApi, you're checking 300+.
And because UKOddsApi covers 100+ markets including corners, cards, and player props, you're not just finding arbs on Match Result and Over/Under — you're finding arbs on Total Corners, Booking Points, Anytime Goalscorer, and dozens of other markets that other APIs don't even carry.
Important notes on UK arbitrage betting
Account limitations: UK bookmakers — especially Bet365, Sky Bet, and BetVictor — actively limit accounts that display arbing behaviour. To extend account longevity: vary bet sizes, mix arb bets with recreational bets, avoid round numbers, and don't always take the maximum stake.
Bet placement speed: UK arbs typically last minutes, not seconds. You don't need millisecond execution for most opportunities. The built-in arbitrage endpoint refreshes frequently, giving you a comfortable window to place bets manually or via a semi-automated workflow.
Exchanges for arb: Betfair Exchange (included in UKOddsApi) is particularly valuable for arb because exchange accounts are rarely limited. An arb between a retail bookmaker and Betfair Exchange is more sustainable long-term than arbs between two retail bookmakers.
What to build next
- Telegram/Discord notifications — send arb alerts to your phone the moment they're detected
- Web dashboard — build a React frontend showing live arbs with one-click links to bookmakers
- Profit tracker — log completed arbs and track cumulative profit over time
- Filter by bookmaker — only show arbs involving bookmakers where you have active accounts
API endpoints used
| Endpoint | Description |
|---|---|
GET /v1/football/arbitrage?date=YYYY-MM-DD |
Find all arbs on a given date |
GET /v1/football/events/{id}/arbitrage |
Find arbs on a specific match |
GET /v1/football/events |
List fixtures by date and league |
GET /v1/bookmakers |
List all 25+ UK bookmakers |
Add total_stake and round_to parameters to get calculated stake plans with exact bet amounts.
UKOddsApi scans 25+ UK bookmakers across 100+ markets to find arbitrage opportunities that other APIs miss. The arbitrage endpoint is available on the Scale plan. Get started at ukoddsapi.com