In my previous post, I walked through how I built SharpAPI — a real-time sports betting odds API using SSE streaming. Now let's flip to the other side: using that API to build something useful.
We're going to build a sports betting arbitrage scanner in Python. When two sportsbooks disagree on a game's outcome enough, you can bet both sides and lock in a guaranteed profit. The math is simple. The hard part is getting live odds from multiple books fast enough to catch these windows before they close.
By the end of this post, you'll have a working scanner (~200 lines of Python) that finds arb opportunities and pings you the moment one appears.
What Is Sports Betting Arbitrage?
Arbitrage exploits pricing differences between sportsbooks. Here's the simplest case — a two-way moneyline:
- DraftKings has the Celtics at +150 (decimal 2.50)
- FanDuel has the Heat at -130 (decimal 1.769)
Convert to implied probabilities and sum them:
1/2.50 + 1/1.769 = 0.400 + 0.565 = 0.965
When the sum is less than 1.0, an arbitrage exists. The gap is your guaranteed profit margin — in this case, 3.5% no matter who wins.
You split your bankroll proportionally across both sides, and the math guarantees you come out ahead. No predictions, no gut feelings, just arithmetic.
What You'll Need
- Python 3.9+
- A free SharpAPI API key (no credit card required)
- Basic familiarity with REST APIs and JSON
pip install sharpapi requests
Head to sharpapi.io/dashboard/api-keys and grab your key. The free tier gives you 12 requests per minute and 2 sportsbooks — enough to build and test a working scanner.
export SHARPAPI_KEY="your_api_key_here"
Step 1: Pull Live Odds from Multiple Sportsbooks
The SharpAPI Python SDK normalizes odds from multiple books into a single response. One call, all the data:
import os
from sharpapi import SharpAPI
client = SharpAPI(os.environ["SHARPAPI_KEY"])
odds = client.odds("nba")
for game in odds:
print(f"\n{game['away_team']} @ {game['home_team']}")
for book in game["bookmakers"]:
print(f" {book['name']}:")
for market in book["markets"]:
for outcome in market["outcomes"]:
print(f" {outcome['name']}: {outcome['price']} "
f"(implied: {1/outcome['price']:.1%})")
You'll get back something like:
{
"away_team": "Boston Celtics",
"home_team": "Miami Heat",
"bookmakers": [
{
"name": "DraftKings",
"markets": [{
"key": "h2h",
"outcomes": [
{"name": "Boston Celtics", "price": 1.55},
{"name": "Miami Heat", "price": 2.50}
]
}]
},
{
"name": "FanDuel",
"markets": [{
"key": "h2h",
"outcomes": [
{"name": "Boston Celtics", "price": 1.48},
{"name": "Miami Heat", "price": 2.75}
]
}]
}
]
}
FanDuel has Miami at 2.75 while DraftKings has Boston at 1.55. Let's see if those create an arb.
Step 2: Detect Arbitrage Opportunities
For every game, we find the best price for each outcome across all sportsbooks, then check if the implied probabilities sum to less than 1:
def find_arbs(game: dict) -> list[dict]:
"""Find arbitrage opportunities in a game's odds."""
arbs = []
markets = {}
for book in game["bookmakers"]:
for market in book["markets"]:
key = market["key"]
if key not in markets:
markets[key] = {}
for outcome in market["outcomes"]:
name = outcome["name"]
if name not in markets[key]:
markets[key][name] = []
markets[key][name].append({
"price": outcome["price"],
"book": book["name"],
})
for market_key, outcomes in markets.items():
outcome_names = list(outcomes.keys())
if len(outcome_names) < 2:
continue
best = {}
for name in outcome_names:
best[name] = max(outcomes[name], key=lambda x: x["price"])
implied_sum = sum(
1.0 / best[name]["price"] for name in outcome_names
)
if implied_sum < 1.0:
profit_pct = (1.0 - implied_sum) * 100
arbs.append({
"game": f"{game['away_team']} @ {game['home_team']}",
"market": market_key,
"profit_pct": round(profit_pct, 2),
"legs": [
{
"outcome": name,
"book": best[name]["book"],
"odds": best[name]["price"],
}
for name in outcome_names
],
})
return arbs
Step 3: Calculate Optimal Stake Sizing
When you find an arb, you need to split your bankroll so each leg returns the same total. This guarantees equal profit regardless of outcome:
def calculate_stakes(arb: dict, bankroll: float = 1000.0) -> dict:
"""Calculate optimal stakes for each leg of an arb."""
legs = arb["legs"]
implied_sum = sum(1.0 / leg["odds"] for leg in legs)
stakes = []
for leg in legs:
implied_prob = 1.0 / leg["odds"]
stake = bankroll * (implied_prob / implied_sum)
payout = stake * leg["odds"]
stakes.append({
**leg,
"stake": round(stake, 2),
"payout": round(payout, 2),
})
guaranteed_return = stakes[0]["payout"]
profit = guaranteed_return - bankroll
return {
**arb,
"bankroll": bankroll,
"stakes": stakes,
"guaranteed_return": round(guaranteed_return, 2),
"profit": round(profit, 2),
}
With our earlier example (DraftKings Celtics 1.55, FanDuel Heat 2.75):
DraftKings - Boston Celtics: stake $639.39, payout $991.05
FanDuel - Miami Heat: stake $360.61, payout $991.67
Guaranteed profit: $12.83 on $1,000
That's $12.83 guaranteed, win or lose. The math works every time.
Step 4: Build the Real-Time Scanner Loop
SharpAPI supports SSE streaming for real-time odds updates. This catches arb windows faster than polling because you see price changes the moment they happen. (I wrote about the SSE architecture here.)
Here's a scanner that supports both polling and streaming:
import json
import time
import requests
BASE_URL = "https://api.sharpapi.io/api/v1"
def scan_sport(client, sport: str, bankroll: float) -> int:
"""Scan a single sport for arbs via polling."""
odds = client.odds(sport)
found = 0
for game in odds:
for arb in find_arbs(game):
result = calculate_stakes(arb, bankroll)
print_arb(result)
alert(result)
found += 1
return found
def stream_odds(sport: str, api_key: str):
"""Stream real-time odds updates via SSE."""
url = f"{BASE_URL}/odds/{sport}/stream"
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "text/event-stream",
}
with requests.get(url, headers=headers, stream=True) as resp:
resp.raise_for_status()
buffer = ""
for chunk in resp.iter_content(decode_unicode=True):
buffer += chunk
while "\n\n" in buffer:
event, buffer = buffer.split("\n\n", 1)
data_lines = [
l[6:] for l in event.strip().split("\n")
if l.startswith("data: ")
]
if data_lines:
yield json.loads("".join(data_lines))
def print_arb(result: dict):
"""Pretty-print an arbitrage opportunity."""
print(f"\n{'='*60}")
print(f" ARB FOUND: {result['game']}")
print(f" Market: {result['market']} | "
f"Profit: {result['profit_pct']}% (${result['profit']})")
print(f"{'─'*60}")
for s in result["stakes"]:
print(f" {s['book']:20s} {s['outcome']:25s} "
f"odds {s['odds']:<8} stake ${s['stake']:>8}")
print(f" {'':20s} {'Guaranteed return':25s} "
f"{'':8s} ${result['guaranteed_return']:>8}")
print(f"{'='*60}")
For a simple multi-sport polling loop:
SPORTS = ["nba", "nfl", "mlb", "nhl"]
def run_scanner(client, bankroll=1000.0, interval=30):
scan_count = 0
arb_count = 0
while True:
scan_count += 1
print(f"[Scan #{scan_count}] checking {len(SPORTS)} sports...")
for sport in SPORTS:
try:
arb_count += scan_sport(client, sport, bankroll)
except Exception as e:
print(f" Error scanning {sport}: {e}")
time.sleep(1)
print(f" Total arbs found: {arb_count}")
print(f" Next scan in {interval}s...\n")
time.sleep(interval)
Step 5: Add Alerts (Telegram + Discord)
An arb that closes before you see it is worthless. Let's add real-time alerts:
from datetime import datetime
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")
def alert(result: dict):
"""Send arb alert to all configured channels."""
msg = (
f"Arb: {result['profit_pct']}% on {result['game']}\n"
+ "\n".join(
f" {s['book']} - {s['outcome']} @ {s['odds']} "
f"(${s['stake']})"
for s in result["stakes"]
)
+ f"\nProfit: ${result['profit']} on "
f"${result['bankroll']} bankroll"
)
if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}"
f"/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID,
"text": f"```
{% endraw %}
\n{msg}\n
{% raw %}
```",
"parse_mode": "Markdown"},
timeout=5,
)
if DISCORD_WEBHOOK_URL:
requests.post(
DISCORD_WEBHOOK_URL,
json={"content": f"```
{% endraw %}
\n{msg}\n
{% raw %}
```"},
timeout=5,
)
Setup: create a Telegram bot via @BotFather, get your chat ID from @userinfobot, and set the env vars. For Discord, create a webhook in your channel settings.
The Complete Script
All of the above in one file — save as arb_scanner.py and run:
export SHARPAPI_KEY="your_key"
python arb_scanner.py --sport nba --bankroll 500 --interval 20
Full source: arb_scanner.py gist (~200 lines — everything above combined with argparse CLI)
Setting Realistic Expectations
A few things to know before you run this in anger:
Arbs are rare. With 2 sportsbooks (free tier), expect a handful per day across all sports. With 15+ books (Pro plan), you'll see 5-20x more because more books means more pricing disagreements.
Margins are thin. Most arbs fall in the 0.5-3% range. On $1,000 that's $5-$30 per opportunity. The profit is guaranteed — but it's not a get-rich-quick scheme. Professional arb bettors make money through volume and speed.
Speed is everything. Arb windows can close in seconds as books adjust lines. SSE streaming catches opportunities that polling misses entirely. This is why I built SharpAPI on SSE instead of REST polling — the architecture post explains the tradeoffs.
What's Next: Beyond Basic Arbitrage
Once your scanner works, here are three ways to level up — all supported by the API:
+EV (Positive Expected Value) Detection
Instead of guaranteed profit on a single bet, +EV finds bets where the odds are better than the "true" probability. SharpAPI calculates this using Pinnacle's sharp lines as the reference:
odds = client.odds("nba", include="ev")
for game in odds:
for book in game["bookmakers"]:
for market in book["markets"]:
for outcome in market["outcomes"]:
if outcome.get("edge_pct", 0) > 2.0:
print(f"+EV: {outcome['name']} on {book['name']}")
print(f" Edge: {outcome['edge_pct']}%")
Over hundreds of +EV bets, the math works in your favor — like being the house.
Server-Side Arbitrage Detection
Skip the client-side math entirely and let the API find arbs for you:
odds = client.odds("nba", include="arb")
for game in odds:
if game.get("arb_pct"):
print(f"Arb: {game['arb_pct']}% - "
f"{game['away_team']} @ {game['home_team']}")
This checks all sportsbook combinations server-side, including books outside your current tier.
More Sportsbooks = More Arbs
The free tier includes 2 books. The Hobby plan ($79/mo, 3-day free trial) unlocks 5 books with real-time data. The math is simple: more books, more disagreements, more arbs.
Wrapping Up
In ~200 lines of Python, you now have a scanner that:
- Pulls live odds from multiple sportsbooks via one API call
- Detects arbitrage using implied probability math
- Calculates optimal stakes for guaranteed profit
- Alerts you in real time via Telegram or Discord
The hard part — aggregating odds from dozens of sportsbooks, normalizing formats, keeping data fresh in real time — is handled by the API. You focus on the strategy.
Full docs: docs.sharpapi.io
Free API key: sharpapi.io (no credit card)
Discord: Join the community
This is Part 2 of my "Building with SharpAPI" series. Part 1 covers how I built the API itself.
Top comments (0)