Skip to main content
How to Build an Arbitrage Scanner in Python Using Odds-API
Back to Blog

How to Build an Arbitrage Scanner in Python Using Odds-API

James Whitfield

James Whitfield

7 min read

Find guaranteed profit opportunities across 250+ bookmakers with a single Python script.

What Is Arbitrage Betting?

Arbitrage (or "arbing") is when you back every possible outcome of a sporting event across different bookmakers, and the combined odds guarantee a profit regardless of the result. It works because bookmakers set their odds independently, and sometimes those odds disagree enough to create a gap you can exploit.

Here's the simplest example. A tennis match has two outcomes:

  • Bookmaker A offers Player 1 at 2.10
  • Bookmaker B offers Player 2 at 2.05

To check if this is an arb, you convert each odd to its implied probability:

1/2.10 + 1/2.05 = 0.4762 + 0.4878 = 0.9640

If the total implied probability is below 1.0, you have an arbitrage opportunity. In this case, 0.964 means a guaranteed ~3.6% profit margin.

What We're Building

By the end of this tutorial, you'll have a Python script that:

  1. Fetches live arbitrage opportunities from the Odds-API
  2. Displays the event, bookmakers, odds, and profit margin
  3. Calculates the exact stake for each leg to guarantee profit
  4. Runs on a loop so you never miss an opportunity

We'll use the official Odds-API Python SDK, which handles all the API communication for you, plus the dedicated Arbitrage Bets endpoint that scans 250+ bookmakers in real time.

Prerequisites

  • Python 3.8+ installed
  • An Odds-API key (grab a free one at odds-api.io, takes 30 seconds)

Install the SDK:

pip install odds-api-io --upgrade

That's it. No complex setup.

Step 1: Get Your API Key

Head to odds-api.io and sign up for a free account. You'll get an API key immediately.

Save it somewhere safe. We'll reference it throughout the tutorial.

Step 2: Connect to the API

The SDK makes connecting dead simple. You can use it as a context manager so the connection is cleaned up automatically:

from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

with OddsAPIClient(api_key=API_KEY) as client:
    sports = client.get_sports()
    print(f"Connected! {len(sports)} sports available.")

Step 3: Check Available Bookmakers

Before scanning for arbs, let's see which bookmakers are available. This helps you filter to bookmakers you actually have accounts with.

from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

with OddsAPIClient(api_key=API_KEY) as client:
    bookmakers = client.get_bookmakers()
    active = [b["name"] for b in bookmakers if b["active"]]

    print(f"Found {len(active)} active bookmakers:")
    for name in sorted(active):
        print(f"  - {name}")

Run this and you'll see a list of 250+ bookmakers. Pick the ones you have accounts with.

Step 4: Scan for Arbitrage Opportunities

This is where it gets good. The SDK's get_arbitrage_bets() method hits the dedicated arbitrage endpoint, which continuously scans all bookmaker combinations and returns only confirmed arb opportunities. No need to manually compare thousands of odds yourself.

from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

# Add the bookmakers you have accounts with
MY_BOOKMAKERS = "Bet365,Unibet,WilliamHill,Betfair Sportsbook,888Sport"

with OddsAPIClient(api_key=API_KEY) as client:
    arbs = client.get_arbitrage_bets(
        bookmakers=MY_BOOKMAKERS,
        limit=50,
        include_event_details=True,
    )

    print(f"Found {len(arbs)} arbitrage opportunities")

The bookmakers parameter is important. The API only returns arbs where every leg uses one of your specified bookmakers. No point finding an arb on a bookmaker you can't bet with.

Step 5: Parse and Display the Results

Each arb opportunity includes everything you need: the event, the market, each leg with its bookmaker and odds, the profit margin, and pre-calculated optimal stakes. Let's display it clearly.

def display_arb(arb):
    """Print a single arbitrage opportunity in a readable format."""
    event = arb.get("event", {})
    home = event.get("home", "Unknown")
    away = event.get("away", "Unknown")
    sport = event.get("sport", "Unknown")
    league = event.get("league", "Unknown")
    market = arb["market"]["name"]
    hdp = arb["market"].get("hdp")
    profit = arb["profitMargin"]
    total_stake = arb["totalStake"]

    market_display = market
    if hdp is not None and hdp != 0:
        market_display = f"{market} ({hdp:+.1f})"

    print(f"\n{'='*60}")
    print(f"  {sport} | {league}")
    print(f"  {home} vs {away}")
    print(f"  Market: {market_display}")
    print(f"  Profit Margin: {profit:.2f}%")
    print(f"-"*60)

    # Display each leg
    for leg in arb["legs"]:
        print(f"  {leg['side']:20s} @ {float(leg['odds']):6.2f}  [{leg['bookmaker']}]")
        if leg.get('href'):
            print(f"    Link: {leg['href']}")

    # Display optimal stakes
    if arb.get("optimalStakes"):
        print(f"-"*60)
        print(f"  Optimal stakes (based on £{total_stake:.0f} total):")
        for stake_info in arb["optimalStakes"]:
            side = stake_info["side"]
            stake = stake_info["stake"]
            potential = stake_info["potentialReturn"]
            bookmaker = stake_info["bookmaker"]
            print(f"  Bet £{stake:.2f} on {side} at {bookmaker} -> returns £{potential:.2f}")

    print(f"{'='*60}")

Step 6: Put It All Together

Now let's combine everything into a scanner that runs on a loop, checking for new arbs at a regular interval.

import time
from datetime import datetime
from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

# Bookmakers you have accounts with (comma-separated)
MY_BOOKMAKERS = "Bet365,Unibet,WilliamHill,Betfair Sportsbook,888Sport"

# How often to check (in seconds)
SCAN_INTERVAL = 30

# Minimum profit margin to show (percentage)
MIN_PROFIT = 0.5


def display_arb(arb):
    """Print a single arbitrage opportunity."""
    event = arb.get("event", {})
    home = event.get("home", "Unknown")
    away = event.get("away", "Unknown")
    sport = event.get("sport", "Unknown")
    league = event.get("league", "Unknown")
    market = arb["market"]["name"]
    hdp = arb["market"].get("hdp")
    profit = arb["profitMargin"]
    total_stake = arb["totalStake"]

    market_display = market
    if hdp is not None and hdp != 0:
        market_display = f"{market} ({hdp:+.1f})"

    print(f"\n{'='*60}")
    print(f"  {sport} | {league}")
    print(f"  {home} vs {away}")
    print(f"  Market: {market_display}")
    print(f"  Profit Margin: {profit:.2f}%")
    print(f"-"*60)

    for leg in arb["legs"]:
        print(f"  {leg['side']:20s} @ {float(leg['odds']):6.2f}  [{leg['bookmaker']}]")
        if leg.get('href'):
            print(f"    Link: {leg['href']}")

    if arb.get("optimalStakes"):
        print(f"-"*60)
        print(f"  Optimal stakes (based on £{total_stake:.0f} total):")
        for s in arb["optimalStakes"]:
            print(f"  Bet £{s['stake']:.2f} on {s['side']} at {s['bookmaker']} -> returns £{s['potentialReturn']:.2f}")

    print(f"{'='*60}")


def run_scanner():
    """Main loop: scan for arbs, display results, repeat."""
    seen_ids = set()

    print("=" * 60)
    print("  ARBITRAGE SCANNER")
    print(f"  Bookmakers: {MY_BOOKMAKERS}")
    print(f"  Min profit: {MIN_PROFIT}%")
    print(f"  Scan interval: {SCAN_INTERVAL}s")
    print("=" * 60)

    with OddsAPIClient(api_key=API_KEY) as client:
        while True:
            try:
                timestamp = datetime.now().strftime("%H:%M:%S")

                arbs = client.get_arbitrage_bets(
                    bookmakers=MY_BOOKMAKERS,
                    limit=100,
                    include_event_details=True,
                )

                # Filter by minimum profit
                arbs = [a for a in arbs if a["profitMargin"] >= MIN_PROFIT]

                # Only show new ones
                new_arbs = [a for a in arbs if a["id"] not in seen_ids]

                if new_arbs:
                    print(f"\n[{timestamp}] Found {len(new_arbs)} new arb(s)!")
                    for arb in sorted(new_arbs, key=lambda x: x["profitMargin"], reverse=True):
                        display_arb(arb)
                        seen_ids.add(arb["id"])
                else:
                    print(f"[{timestamp}] Scanning... {len(arbs)} active arb(s), no new ones.")

                # Clean up old IDs to prevent memory growing forever
                current_ids = {a["id"] for a in arbs}
                seen_ids = seen_ids & current_ids

            except Exception as e:
                print(f"[{timestamp}] Error: {e}")

            time.sleep(SCAN_INTERVAL)


if __name__ == "__main__":
    run_scanner()

Step 7: Run It

Save the script as arb_scanner.py, replace YOUR_API_KEY with your actual key, update MY_BOOKMAKERS with your bookmakers, and run:

python arb_scanner.py

You'll see output like this:

============================================================
  ARBITRAGE SCANNER
  Bookmakers: Bet365,Unibet,WilliamHill,Betfair Sportsbook,888Sport
  Min profit: 0.5%
  Scan interval: 30s
============================================================

[14:32:15] Found 2 new arb(s)!

============================================================
  Football | Premier League
  Arsenal vs Chelsea
  Market: Match Winner
  Profit Margin: 1.84%
------------------------------------------------------------
  Arsenal              @   2.30  [Bet365]
    Link: https://www.bet365.com/#/AC/B91/C21112195/...
  Draw                 @   3.60  [Unibet]
    Link: https://www.unibet.com/betting/sports/event/...
  Chelsea              @   4.20  [WilliamHill]
    Link: https://sports.williamhill.com/betting/en-gb/...
------------------------------------------------------------
  Optimal stakes (based on £100 total):
  Bet £43.48 on Arsenal at Bet365 -> returns £100.00
  Bet £27.78 on Draw at Unibet -> returns £100.00
  Bet £23.81 on Chelsea at WilliamHill -> returns £100.00
============================================================

Understanding the Output

Every arb the scanner finds includes:

Profit Margin: The guaranteed profit as a percentage. A 1.84% margin on a £100 total stake means £1.84 guaranteed profit.

Legs: Each outcome you need to bet on, with the bookmaker offering the best odds.

Optimal Stakes: Exactly how much to place on each leg so that every outcome returns the same amount. This is the key to arbing: equal returns, no matter who wins.

Customising Your Scanner

Here are some tweaks you can make:

Change your total stake

The API returns optimalStakes based on a default £1000 total. If you want to calculate stakes for a different bankroll, add this function to your script and call it inside the scanner loop where you process each arb:

def calculate_stakes(arb, total_bankroll):
    """Calculate stakes for a custom bankroll size."""
    stakes = []
    for leg in arb["legs"]:
        odds = float(leg["odds"])
        stake = total_bankroll / odds
        stakes.append({
            "side": leg["side"],
            "bookmaker": leg["bookmaker"],
            "odds": odds,
            "stake": round(stake, 2),
            "returns": round(stake * odds, 2),
        })

    # Normalize so stakes sum to total_bankroll
    total_raw = sum(s["stake"] for s in stakes)
    for s in stakes:
        s["stake"] = round(s["stake"] * total_bankroll / total_raw, 2)
        s["returns"] = round(s["stake"] * s["odds"], 2)

    return stakes

Then use it inside the scanner loop (where arb is available), for example right after display_arb(arb):

# Inside the "for arb in sorted(new_arbs, ...)" loop:
my_stakes = calculate_stakes(arb, 500)
for s in my_stakes:
    print(f"  Bet £{s['stake']} on {s['side']} at {s['bookmaker']} (odds {s['odds']}) -> £{s['returns']}")

Here's the full updated scanner with custom bankroll support built in. Change TOTAL_STAKE to your total stake and drop it in:

import time
from datetime import datetime
from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

# Bookmakers you have accounts with (comma-separated)
MY_BOOKMAKERS = "Bet365,Unibet,WilliamHill,Betfair Sportsbook,888Sport"

# Your total stake per arb (in £)
TOTAL_STAKE = 500

# How often to check (in seconds)
SCAN_INTERVAL = 30

# Minimum profit margin to show (percentage)
MIN_PROFIT = 0.5


def calculate_stakes(arb, total_bankroll):
    """Calculate stakes for a custom bankroll size."""
    stakes = []
    for leg in arb["legs"]:
        odds = float(leg["odds"])
        stake = total_bankroll / odds
        stakes.append({
            "side": leg["side"],
            "bookmaker": leg["bookmaker"],
            "odds": odds,
            "stake": round(stake, 2),
            "returns": round(stake * odds, 2),
        })

    # Normalize so stakes sum to total_bankroll
    total_raw = sum(s["stake"] for s in stakes)
    for s in stakes:
        s["stake"] = round(s["stake"] * total_bankroll / total_raw, 2)
        s["returns"] = round(s["stake"] * s["odds"], 2)

    return stakes


def display_arb(arb):
    """Print a single arbitrage opportunity with custom bankroll stakes."""
    event = arb.get("event", {})
    home = event.get("home", "Unknown")
    away = event.get("away", "Unknown")
    sport = event.get("sport", "Unknown")
    league = event.get("league", "Unknown")
    market = arb["market"]["name"]
    hdp = arb["market"].get("hdp")
    profit = arb["profitMargin"]

    market_display = market
    if hdp is not None and hdp != 0:
        market_display = f"{market} ({hdp:+.1f})"

    print(f"\n{'='*60}")
    print(f"  {sport} | {league}")
    print(f"  {home} vs {away}")
    print(f"  Market: {market_display}")
    print(f"  Profit Margin: {profit:.2f}%")
    print(f"-"*60)

    for leg in arb["legs"]:
        print(f"  {leg['side']:20s} @ {float(leg['odds']):6.2f}  [{leg['bookmaker']}]")
        if leg.get('href'):
            print(f"    Link: {leg['href']}")

    # Calculate stakes for your bankroll
    stakes = calculate_stakes(arb, TOTAL_STAKE)
    print(f"-"*60)
    print(f"  Your stakes (£{TOTAL_STAKE} bankroll):")
    for s in stakes:
        print(f"  Bet £{s['stake']:.2f} on {s['side']} at {s['bookmaker']} -> returns £{s['returns']:.2f}")

    expected_profit = stakes[0]["returns"] - TOTAL_STAKE
    print(f"  Guaranteed profit: £{expected_profit:.2f}")
    print(f"{'='*60}")


def run_scanner():
    """Main loop: scan for arbs, display results, repeat."""
    seen_ids = set()

    print("=" * 60)
    print("  ARBITRAGE SCANNER")
    print(f"  Bookmakers: {MY_BOOKMAKERS}")
    print(f"  Bankroll: £{TOTAL_STAKE}")
    print(f"  Min profit: {MIN_PROFIT}%")
    print(f"  Scan interval: {SCAN_INTERVAL}s")
    print("=" * 60)

    with OddsAPIClient(api_key=API_KEY) as client:
        while True:
            try:
                timestamp = datetime.now().strftime("%H:%M:%S")

                arbs = client.get_arbitrage_bets(
                    bookmakers=MY_BOOKMAKERS,
                    limit=100,
                    include_event_details=True,
                )

                # Filter by minimum profit
                arbs = [a for a in arbs if a["profitMargin"] >= MIN_PROFIT]

                # Only show new ones
                new_arbs = [a for a in arbs if a["id"] not in seen_ids]

                if new_arbs:
                    print(f"\n[{timestamp}] Found {len(new_arbs)} new arb(s)!")
                    for arb in sorted(new_arbs, key=lambda x: x["profitMargin"], reverse=True):
                        display_arb(arb)
                        seen_ids.add(arb["id"])
                else:
                    print(f"[{timestamp}] Scanning... {len(arbs)} active arb(s), no new ones.")

                # Clean up old IDs to prevent memory growing forever
                current_ids = {a["id"] for a in arbs}
                seen_ids = seen_ids & current_ids

            except Exception as e:
                print(f"[{timestamp}] Error: {e}")

            time.sleep(SCAN_INTERVAL)


if __name__ == "__main__":
    run_scanner()

Add Telegram notifications

Want to get pinged on your phone when an arb appears? First, you'll need two things:

  1. A bot token: Message @BotFather on Telegram, send /newbot, follow the prompts, and you'll get a token like 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11.
  2. Your chat ID: Open @myidbot on Telegram and send /getid. It'll reply with your numeric chat ID.

Once you have both, add these two functions to your script:

import requests

TELEGRAM_BOT_TOKEN = "YOUR_BOT_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID"

def send_telegram_alert(message):
    """Send a Telegram message when a new arb is found."""
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    payload = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message,
        "parse_mode": "HTML",
    }
    requests.post(url, json=payload)


def format_arb_alert(arb):
    """Format an arb as a Telegram message."""
    event = arb.get("event", {})
    legs_parts = []
    for leg in arb["legs"]:
        line = f"  {leg['side']} @ {leg['odds']} [{leg['bookmaker']}]"
        if leg.get("href"):
            line += f"\n  <a href=\"{leg['href']}\">Place bet</a>"
        legs_parts.append(line)
    legs_text = "\n".join(legs_parts)

    return (
        f"<b>New Arb Found!</b>\n"
        f"{event.get('home', '?')} vs {event.get('away', '?')}\n"
        f"Market: {arb['market']['name']}\n"
        f"Profit: {arb['profitMargin']:.2f}%\n\n"
        f"{legs_text}"
    )

Then call them inside the scanner loop, right after display_arb(arb):

send_telegram_alert(format_arb_alert(arb))

Here's the full scanner with Telegram alerts and custom bankroll built in:

import time
import requests
from datetime import datetime
from odds_api import OddsAPIClient

API_KEY = "YOUR_API_KEY"

# Bookmakers you have accounts with (comma-separated)
MY_BOOKMAKERS = "Bet365,Unibet,WilliamHill,Betfair Sportsbook,888Sport"

# Your total stake per arb (in £)
TOTAL_STAKE = 500

# How often to check (in seconds)
SCAN_INTERVAL = 30

# Minimum profit margin to show (percentage)
MIN_PROFIT = 0.5

# Telegram notifications (optional, leave blank to disable)
TELEGRAM_BOT_TOKEN = "YOUR_BOT_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_CHAT_ID"


def send_telegram_alert(message):
    """Send a Telegram message when a new arb is found."""
    if not TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKEN == "YOUR_BOT_TOKEN":
        return
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    payload = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message,
        "parse_mode": "HTML",
    }
    try:
        requests.post(url, json=payload, timeout=5)
    except (requests.RequestException, ValueError):
        pass


def format_arb_alert(arb, bankroll):
    """Format an arb as a Telegram message."""
    event = arb.get("event", {})
    home = event.get("home", "?")
    away = event.get("away", "?")
    sport = event.get("sport", "?")
    league = event.get("league", "?")
    market = arb["market"]["name"]
    profit = arb["profitMargin"]

    legs_parts = []
    for leg in arb["legs"]:
        line = f"  {leg['side']} @ {leg['odds']} [{leg['bookmaker']}]"
        if leg.get("href"):
            line += f"\n  <a href=\"{leg['href']}\">Place bet</a>"
        legs_parts.append(line)
    legs_text = "\n".join(legs_parts)

    stakes = calculate_stakes(arb, bankroll)
    stakes_text = "\n".join(
        f"  £{s['stake']:.2f} on {s['side']} at {s['bookmaker']}"
        for s in stakes
    )
    guaranteed = stakes[0]["returns"] - bankroll

    return (
        f"<b>New Arb Found!</b>\n"
        f"{sport} | {league}\n"
        f"{home} vs {away}\n"
        f"Market: {market}\n"
        f"Profit: {profit:.2f}%\n\n"
        f"{legs_text}\n\n"
        f"<b>Stakes (£{bankroll} bankroll):</b>\n"
        f"{stakes_text}\n"
        f"<b>Guaranteed profit: £{guaranteed:.2f}</b>"
    )


def calculate_stakes(arb, total_bankroll):
    """Calculate stakes for a custom bankroll size."""
    stakes = []
    for leg in arb["legs"]:
        odds = float(leg["odds"])
        stake = total_bankroll / odds
        stakes.append({
            "side": leg["side"],
            "bookmaker": leg["bookmaker"],
            "odds": odds,
            "stake": round(stake, 2),
            "returns": round(stake * odds, 2),
        })

    total_raw = sum(s["stake"] for s in stakes)
    for s in stakes:
        s["stake"] = round(s["stake"] * total_bankroll / total_raw, 2)
        s["returns"] = round(s["stake"] * s["odds"], 2)

    return stakes


def display_arb(arb):
    """Print a single arbitrage opportunity with custom bankroll stakes."""
    event = arb.get("event", {})
    home = event.get("home", "Unknown")
    away = event.get("away", "Unknown")
    sport = event.get("sport", "Unknown")
    league = event.get("league", "Unknown")
    market = arb["market"]["name"]
    hdp = arb["market"].get("hdp")
    profit = arb["profitMargin"]

    market_display = market
    if hdp is not None and hdp != 0:
        market_display = f"{market} ({hdp:+.1f})"

    print(f"\n{'='*60}")
    print(f"  {sport} | {league}")
    print(f"  {home} vs {away}")
    print(f"  Market: {market_display}")
    print(f"  Profit Margin: {profit:.2f}%")
    print(f"-"*60)

    for leg in arb["legs"]:
        print(f"  {leg['side']:20s} @ {float(leg['odds']):6.2f}  [{leg['bookmaker']}]")
        if leg.get('href'):
            print(f"    Link: {leg['href']}")

    stakes = calculate_stakes(arb, TOTAL_STAKE)
    print(f"-"*60)
    print(f"  Your stakes (£{TOTAL_STAKE} bankroll):")
    for s in stakes:
        print(f"  Bet £{s['stake']:.2f} on {s['side']} at {s['bookmaker']} -> returns £{s['returns']:.2f}")

    expected_profit = stakes[0]["returns"] - TOTAL_STAKE
    print(f"  Guaranteed profit: £{expected_profit:.2f}")
    print(f"{'='*60}")


def run_scanner():
    """Main loop: scan for arbs, display results, repeat."""
    seen_ids = set()

    print("=" * 60)
    print("  ARBITRAGE SCANNER")
    print(f"  Bookmakers: {MY_BOOKMAKERS}")
    print(f"  Bankroll: £{TOTAL_STAKE}")
    print(f"  Min profit: {MIN_PROFIT}%")
    print(f"  Scan interval: {SCAN_INTERVAL}s")
    print("=" * 60)

    with OddsAPIClient(api_key=API_KEY) as client:
        while True:
            try:
                timestamp = datetime.now().strftime("%H:%M:%S")

                arbs = client.get_arbitrage_bets(
                    bookmakers=MY_BOOKMAKERS,
                    limit=100,
                    include_event_details=True,
                )

                # Filter by minimum profit
                arbs = [a for a in arbs if a["profitMargin"] >= MIN_PROFIT]

                # Only show new ones
                new_arbs = [a for a in arbs if a["id"] not in seen_ids]

                if new_arbs:
                    print(f"\n[{timestamp}] Found {len(new_arbs)} new arb(s)!")
                    for arb in sorted(new_arbs, key=lambda x: x["profitMargin"], reverse=True):
                        display_arb(arb)
                        send_telegram_alert(format_arb_alert(arb, TOTAL_STAKE))
                        seen_ids.add(arb["id"])
                else:
                    print(f"[{timestamp}] Scanning... {len(arbs)} active arb(s), no new ones.")

                # Clean up old IDs to prevent memory growing forever
                current_ids = {a["id"] for a in arbs}
                seen_ids = seen_ids & current_ids

            except Exception as e:
                print(f"[{timestamp}] Error: {e}")

            time.sleep(SCAN_INTERVAL)


if __name__ == "__main__":
    run_scanner()

How Arbitrage Actually Works (The Maths)

If you want to understand the formula behind the scenes:

For a two-outcome market (e.g. tennis), an arb exists when:

(1 / odds_A) + (1 / odds_B) < 1.0

For a three-outcome market (e.g. football with the draw):

(1 / odds_home) + (1 / odds_draw) + (1 / odds_away) < 1.0

The profit margin is:

profit_margin = (1 - sum_of_implied_probabilities) × 100

And the optimal stake for each leg:

stake_for_leg = total_bankroll × (1 / odds_for_leg) / sum_of_implied_probabilities

This ensures every outcome returns the same amount, locking in your profit.

Tips for Real-World Arbing

  1. Speed matters. Odds change fast. The sooner you spot and place an arb, the better. Consider reducing SCAN_INTERVAL to 10-15 seconds if your plan allows it.
  2. Verify odds before placing. Always confirm the odds are still live on the bookmaker's site before staking. The API provides directLink URLs in each leg that take you straight to the bet slip.
  3. Start small. Test with small stakes until you're confident the process works. Build up gradually.
  4. Use multiple bookmakers. The more bookmaker accounts you have, the more arb opportunities you'll find. Add all your bookmakers to MY_BOOKMAKERS.
  5. Watch for limits. Some bookmakers limit or restrict accounts that consistently win. Spread your activity across many bookmakers and avoid arbing the same ones repeatedly.
  6. Account for fees. Some bookmakers charge withdrawal fees or have currency conversion costs. Factor these in when assessing whether a small-margin arb is worth it.

Full Source Code

The complete scanner is in Step 6 above. For more examples, check out the official SDK examples, including a ready-made arbitrage finder.

Want to take it further? The SDK also supports async with AsyncOddsAPIClient for real-time streaming, or try client.get_value_bets() to find +EV bets too.

Get your free API key at odds-api.io and start scanning in under 5 minutes.