How to Build an Arbitrage Betting Bot with Python (Free API)
What Is Arbitrage Betting?
Arbitrage betting (“arbing”) is the practice of betting on every possible outcome of an event across different bookmakers, where the combined implied probability is less than 100%. The result: guaranteed profit regardless of who wins.
Here is a quick example. A soccer match has three outcomes (Home, Draw, Away):
| Outcome | Bookmaker | Odds | Implied Prob |
|---|---|---|---|
| Home Win | Pinnacle | 2.55 | 39.2% |
| Draw | Bet365 | 3.80 | 26.3% |
| Away Win | 1xBet | 3.20 | 31.3% |
Total implied probability: 96.8%. That is under 100%, which means you can stake proportionally on all three outcomes and lock in a 3.2% profit no matter what happens.
The catch? To find arbs, you need odds from as many bookmakers as possible. The more books you cover, the more arbs you find. That is why most arb scanners fail — they only check 20–40 soft bookmakers.
Why Most Arbitrage Bots Fail
Before we build anything, let us talk about why most DIY arb bots produce zero results.
1. Not Enough Bookmakers
The Odds API covers roughly 40 soft bookmakers. Arbs between soft books are razor-thin and vanish in seconds. Real, actionable arbs live in the spread between sharp bookmakers (Pinnacle, Singbet, SBOBet) and softs (Bet365, DraftKings, FanDuel). If your API does not carry sharps, you are scanning a puddle instead of an ocean.
2. No Sharp Bookmakers
Pinnacle, Singbet, and SBOBet set the “true line.” When a soft bookmaker is slow to adjust, the gap between their price and the sharp line creates an arb. Most odds APIs do not carry these books at all. OddsPapi carries all three, plus 350+ total bookmakers including crypto and niche books like 1xBet and GG.BET.
3. Polling Latency
If you poll every 30 seconds, the arb is gone before your code even prints it. The best arbs last seconds, not minutes. This is where WebSocket streaming changes the game — OddsPapi pushes odds changes to you in real-time instead of waiting for you to ask.
Building an Arb Bot: The Odds API vs OddsPapi
| Feature | The Odds API | OddsPapi |
|---|---|---|
| Bookmakers | ~40 (soft only) | 350+ (sharps + softs) |
| Sharp Books (Pinnacle, Singbet) | No | Yes |
| Crypto/Niche Books | No | Yes (1xBet, GG.BET) |
| Real-Time WebSocket | No | Yes |
| Free Historical Data | No | Yes |
| Free Tier | 500 req/month | 250 req/month |
| Arb Detection Potential | Low (limited coverage) | High (sharp + soft spread) |
The math is simple: more bookmakers = more price discrepancies = more arbs. With 350+ books including sharps, OddsPapi gives your scanner the coverage it needs to actually find opportunities.
Build an Arbitrage Scanner in Python
Let us build a working arb scanner from scratch. This code is tested against the live OddsPapi API and ready to run.
Step 1: Install Dependencies & Set Up
import requests
from datetime import datetime, timedelta
API_KEY = "YOUR_API_KEY" # Get free at oddspapi.io
BASE_URL = "https://api.oddspapi.io/v4"
No extra libraries needed. Just requests and the standard library. Grab your free API key and drop it in.
Step 2: Get Today’s Fixtures
def get_fixtures(sport_id=10):
"""Fetch today's fixtures. Sport 10 = Soccer."""
today = datetime.now().strftime("%Y-%m-%d")
tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
response = requests.get(f"{BASE_URL}/fixtures", params={
"apiKey": API_KEY,
"sportId": sport_id,
"from": today,
"to": tomorrow
})
fixtures = response.json()
# Filter to fixtures with odds available
return [f for f in fixtures if f.get("hasOdds")]
fixtures = get_fixtures()
print(f"Found {len(fixtures)} fixtures with odds")
for f in fixtures[:5]:
print(f" {f['participant1Name']} vs {f['participant2Name']} ({f['tournamentName']})")
The /fixtures endpoint returns a flat list of fixtures. We filter on hasOdds so we only scan matches that actually have pricing data from bookmakers.
Step 3: Fetch Odds from All Bookmakers
def get_odds(fixture_id):
"""Get odds from all bookmakers for a fixture."""
response = requests.get(f"{BASE_URL}/odds", params={
"apiKey": API_KEY,
"fixtureId": fixture_id
})
return response.json()
One call, every bookmaker. The response contains a bookmakerOdds dictionary keyed by bookmaker slug (e.g., pinnacle, bet365, singbet). No pagination, no extra calls.
Step 4: Find the Best Price for Each Outcome
def find_best_prices(odds_data, market_id="101"):
"""Find the best price for each outcome across all bookmakers.
Market 101 = Full Time Result (1X2)
Outcomes: 101=Home, 102=Draw, 103=Away
"""
best = {} # {outcome_id: {"price": float, "bookmaker": str}}
for slug, bookie in odds_data.get("bookmakerOdds", {}).items():
market = bookie.get("markets", {}).get(market_id)
if not market:
continue
for outcome_id, outcome in market.get("outcomes", {}).items():
for player_id, player in outcome.get("players", {}).items():
price = player.get("price")
if price and (outcome_id not in best or price > best[outcome_id]["price"]):
best[outcome_id] = {
"price": price,
"bookmaker": slug,
"outcome_id": outcome_id
}
return best
This is the core of the scanner. For each outcome in a market, we loop through every bookmaker and keep track of whoever is offering the highest price. The OddsPapi odds structure is nested: bookmakerOdds → slug → markets → marketId → outcomes → outcomeId → players → 0 → price.
Step 5: Calculate the Arbitrage Percentage
def check_arb(best_prices):
"""Check if an arbitrage opportunity exists.
If the sum of implied probabilities < 1.0, it's an arb.
Profit margin = (1 - sum) * 100
"""
if not best_prices:
return None
total_implied = sum(1 / bp["price"] for bp in best_prices.values())
margin = (1 - total_implied) * 100
return {
"is_arb": total_implied < 1.0,
"margin": margin,
"total_implied": total_implied,
"prices": best_prices
}
The formula is straightforward. Convert each best price to its implied probability (1 / odds), sum them up. If the total is under 1.0 (100%), you have an arb. The margin tells you how much guaranteed profit you can extract.
Step 6: Scan All Fixtures
def scan_for_arbs(sport_id=10, market_id="101"):
"""Scan all fixtures for arbitrage opportunities."""
fixtures = get_fixtures(sport_id)
print(f"Scanning {len(fixtures)} fixtures for arbs...\n")
arbs_found = []
for fixture in fixtures:
fid = fixture["fixtureId"]
name = f"{fixture['participant1Name']} vs {fixture['participant2Name']}"
odds_data = get_odds(fid)
best_prices = find_best_prices(odds_data, market_id)
result = check_arb(best_prices)
if result and result["is_arb"]:
arbs_found.append({"fixture": name, **result})
print(f"ARB FOUND: {name}")
print(f" Margin: {result['margin']:.2f}%")
for oid, bp in result["prices"].items():
print(f" Outcome {oid}: {bp['price']} @ {bp['bookmaker']}")
print()
else:
overround = -result["margin"] if result else 0
bookmakers = len(odds_data.get("bookmakerOdds", {}))
print(f" {name}: {bookmakers} books, {overround:.1f}% overround")
print(f"\n{'='*50}")
print(f"Scanned {len(fixtures)} fixtures")
print(f"Arbs found: {len(arbs_found)}")
return arbs_found
# Run the scanner
arbs = scan_for_arbs()
Run this and watch it scan every fixture. With 350+ bookmakers feeding prices, you will see arbs that scanners limited to 40 soft books will never detect.
Taking It Further: Multi-Market & Real-Time Scanning
The scanner above covers the 1X2 (Full Time Result) market. But arbs hide in other markets too. Here is how to expand it.
Scan Multiple Markets
Loop through several markets to multiply your opportunities. Each market has its own set of outcomes and its own set of bookmaker pricing inefficiencies.
MARKETS = {
"101": "Full Time Result (1X2)",
"1010": "Over/Under 2.5 Goals",
"104": "Both Teams to Score"
}
for market_id, market_name in MARKETS.items():
print(f"\n--- {market_name} ---")
arbs = scan_for_arbs(market_id=market_id)
Over/Under and BTTS markets often have more arbs than 1X2 because they are two-outcome markets — fewer outcomes means the margin between bookmakers needs to be smaller to create an arb, and it happens more often.
Calculate Optimal Stakes
Once you find an arb, you need to know exactly how much to bet on each outcome to guarantee profit.
def calculate_stakes(best_prices, total_stake=100):
"""Calculate how much to bet on each outcome for guaranteed profit."""
total_implied = sum(1 / bp["price"] for bp in best_prices.values())
stakes = {}
for outcome_id, bp in best_prices.items():
implied = 1 / bp["price"]
stake = (implied / total_implied) * total_stake
payout = stake * bp["price"]
stakes[outcome_id] = {
"bookmaker": bp["bookmaker"],
"price": bp["price"],
"stake": round(stake, 2),
"payout": round(payout, 2)
}
profit = (1 / total_implied - 1) * total_stake
return {"stakes": stakes, "profit": round(profit, 2), "roi": round((1/total_implied - 1) * 100, 2)}
The function distributes your total stake proportionally across outcomes so that every outcome pays out the same amount. The difference between total payout and total stake is your guaranteed profit.
Upgrade to WebSocket for Real-Time Detection
Polling the REST API every 30 seconds works for finding arbs, but most of them will have closed by the time you place your bets. The real edge comes from real-time WebSocket streaming. OddsPapi pushes odds changes to your bot the instant they happen, cutting your detection latency from 30 seconds to milliseconds.
WebSocket access requires a Pro tier plan. For the full setup guide, check out our WebSocket Odds API tutorial.
FAQ: Arbitrage Betting Bots
Is arbitrage betting legal?
Yes, arbitrage betting is legal in most jurisdictions. You are simply placing bets at different bookmakers. However, bookmakers do not like arbers and may limit or close your accounts if they detect consistent arb activity. Using multiple accounts and varying your bet sizes can help extend account longevity.
How many bookmakers do I need for arbs?
The more bookmakers your scanner covers, the more arbs you will find. With 40 soft-only books (like The Odds API provides), you will find close to zero actionable arbs. With 350+ books including sharps like Pinnacle and Singbet, you will find several opportunities daily. The sharp-to-soft spread is where the real arbs live.
What sports have the most arbitrage opportunities?
Soccer has the most arbs because it has the widest bookmaker coverage globally. Tennis is also strong because it is a two-outcome market (no draw), which makes arbs more common. Niche markets like esports and regional leagues also produce arbs because fewer bookmakers price them efficiently.
Do I need a paid API plan?
The free tier (250 requests/month) is enough to build and test your scanner. For production scanning across multiple sports and markets, you will want a paid plan for higher rate limits. For real-time WebSocket alerts, you need the Pro tier.
How long do arbitrage opportunities last?
Most arbs last seconds to a few minutes. Sharp bookmakers like Pinnacle move fast, and soft books adjust their lines accordingly. This is why polling-based scanners miss most arbs. Real-time WebSocket detection gives you the speed advantage you need.
Can I use this with Pinnacle?
Yes. OddsPapi aggregates odds from Pinnacle, Singbet, SBOBet, and other sharp bookmakers that most APIs do not carry. You do not need a Pinnacle commercial account or API key — OddsPapi handles the data aggregation for you.
Stop Scraping. Start Scanning.
Every minute you spend scraping individual bookmaker sites is a minute you are not finding arbs. OddsPapi gives you 350+ bookmakers — including sharps like Pinnacle and Singbet — through one API call. Free historical data lets you backtest your strategy before risking real money. And when you are ready for production, WebSocket streaming gives you the sub-second latency that separates profitable arbers from the ones who are always a step behind.