If two sportsbooks disagree on the odds for the same event, you can bet both sides and guarantee a profit regardless of the outcome. That’s arbitrage — and building a bot to find and execute these opportunities is one of the most concrete applications of algorithmic betting.

This guide walks through the full pipeline: the underlying math, the four-stage architecture, working Python code for each component, and the operational realities of running an arb bot in production.

Prerequisites: Basic Python, familiarity with REST APIs, understanding of how sportsbook odds work. If you need a refresher on odds formats, see the Odds Converter.


What Is Sports Betting Arbitrage?

Arbitrage (or “arbing”) exploits pricing differences between sportsbooks. When two books price the same event differently enough, you can place offsetting bets that guarantee a profit no matter who wins.

A Simple Example

Consider an NBA game: Lakers vs. Celtics.

OutcomeDraftKingsFanDuel
Lakers win+180 (35.7%)+150 (40.0%)
Celtics win-180 (64.3%)-200 (66.7%)

Neither book offers an arb on its own — their implied probabilities sum to 100%+ (that’s the vig). But if you cherry-pick the best odds per outcome across books:

OutcomeBest OddsBookImplied Prob
Lakers win+180DraftKings35.7%
Celtics win-150BetMGM60.0%

Total implied probability: 35.7% + 60.0% = 95.7%

That’s below 100%. The 4.3% gap is your arbitrage margin. On a $1,000 total stake, that’s approximately $43 in guaranteed profit before fees.

Why Arbs Exist

Sportsbooks set odds independently. They each have different models, different customer bases creating different risk exposure, and different reaction speeds to news. These factors create transient pricing disagreements — usually lasting seconds to minutes.

The window is short because:

  • Sharp bettors exploit the mispricing, causing the book to adjust
  • Odds feeds propagate line changes across the market
  • Automated systems at the books detect and correct outliers

This time pressure is why manual arb betting is impractical at scale, and why bots dominate this space.


The Math: Implied Probability and Overround

Before building anything, you need to be fluent in the core math. Every calculation in an arb bot starts with converting odds to implied probabilities.

Odds Conversion Formulas

def american_to_implied(odds: float) -> float:
    """Convert American odds to implied probability (0-1)."""
    if odds > 0:
        return 100 / (odds + 100)
    else:
        return abs(odds) / (abs(odds) + 100)

def decimal_to_implied(odds: float) -> float:
    """Convert decimal odds to implied probability (0-1)."""
    return 1 / odds

def fractional_to_implied(num: float, den: float) -> float:
    """Convert fractional odds to implied probability (0-1)."""
    return den / (num + den)

For an interactive version, see the Odds Converter tool.

Overround (Vig)

A sportsbook’s overround is the amount by which their implied probabilities exceed 100%. It’s the book’s built-in margin.

def calculate_overround(implied_probs: list[float]) -> float:
    """Calculate overround from a list of implied probabilities.

    Returns the overround as a decimal.
    Example: 0.05 means 105% total implied probability (5% vig).
    """
    return sum(implied_probs) - 1.0
ScenarioTotal ImpliedOverroundMeaning
Single book, fair odds100.0%0%No vig (theoretical)
Single book, typical vig104.5%4.5%Book’s profit margin
Best odds across books98.2%−1.8%Arbitrage opportunity

When the overround is negative (total implied < 100%), an arb exists.

The Arb Percentage Formula

The arb percentage tells you the guaranteed return as a fraction of your total stake:

def arb_percentage(implied_probs: list[float]) -> float:
    """Calculate arb percentage. Positive = arb exists."""
    total = sum(implied_probs)
    return ((1 / total) - 1) * 100

An arb percentage of 2.5% on a $1,000 total stake means $25 guaranteed profit.

Worked Example

Event: NFL game, Rams vs. 49ers (moneyline).

OutcomeBest OddsBookDecimalImplied Prob
Rams+210BetMGM3.1032.26%
49ers-190DraftKings1.52665.52%

Total implied: 32.26% + 65.52% = 97.78%

Arb percentage: (1/0.9778 − 1) × 100 = 2.27%

On a $1,000 stake:

total_stake = 1000
implieds = [0.3226, 0.6552]
total_implied = sum(implieds)  # 0.9778

# Optimal stakes (proportional to implied probability)
stake_rams = total_stake * (implieds[0] / total_implied)   # $330.00
stake_49ers = total_stake * (implieds[1] / total_implied)   # $670.00

# Payouts if each outcome wins
payout_rams = stake_rams * 3.10    # $1,023.00
payout_49ers = stake_49ers * 1.526  # $1,022.42

# Profit (payout - total_stake) is ~$22-23 regardless of outcome

Use the Arbitrage Calculator to run these calculations instantly.


How Arb Bots Work: Architecture Overview

A production arb bot follows a four-stage pipeline:

┌──────────────┐    ┌──────────────────┐    ┌─────────────────┐    ┌──────────────────┐
│  DATA LAYER  │───▶│ COMPARISON ENGINE │───▶│ STAKE CALCULATOR │───▶│ EXECUTION ENGINE │
│              │    │                  │    │                 │    │                  │
│ Multi-book   │    │ Best odds per    │    │ Optimal stake   │    │ Parallel bet     │
│ odds feeds   │    │ outcome, arb     │    │ distribution,   │    │ placement,       │
│              │    │ detection        │    │ fee adjustment  │    │ failure handling  │
└──────────────┘    └──────────────────┘    └─────────────────┘    └──────────────────┘
     10+ books           <1 second              <1 second              <3 seconds

Each stage has distinct responsibilities, and you can develop and test them independently. Let’s build each one.


Stage 1: The Data Layer

The data layer is responsible for collecting real-time odds from multiple sportsbooks and normalizing them into a common format.

Why Scraping Sportsbooks Is a Bad Idea

The obvious approach — scrape odds directly from sportsbook websites — is fragile and often counterproductive:

  • CAPTCHAs and bot detection block automated access
  • DOM changes break scrapers without warning
  • IP bans are common and aggressive
  • Terms of service explicitly prohibit scraping
  • Rate limits restrict how fast you can poll
  • Legal risk varies by jurisdiction

For a prototype, you could scrape one or two sites. For production, you need an odds data provider.

Using an Odds API

The most reliable approach is a dedicated odds API that aggregates data from multiple sportsbooks into a single normalized feed. TheOddsPulse provides real-time odds from 10+ sportsbooks through a single REST endpoint, pre-normalized into a consistent format.

import requests

ODDS_API_BASE = "https://api.theoddspulse.com/v1"
API_KEY = "your-api-key"

def fetch_odds(sport: str, market: str = "h2h") -> list[dict]:
    """Fetch live odds from multiple sportsbooks for a sport.

    Args:
        sport: Sport key (e.g., 'basketball_nba', 'football_nfl')
        market: Market type ('h2h' for moneyline, 'spreads', 'totals')

    Returns:
        List of events with odds from each bookmaker.
    """
    response = requests.get(
        f"{ODDS_API_BASE}/odds",
        params={
            "sport": sport,
            "markets": market,
            "oddsFormat": "decimal",
            "apiKey": API_KEY,
        }
    )
    response.raise_for_status()
    return response.json()

The response gives you odds from every available book for every live event, already normalized to decimal format. No scraping, no parsing, no format conversion.

Polling vs. WebSockets

For arb detection, freshness matters. Stale odds lead to phantom arbs that disappear by the time you try to bet.

ApproachLatencyRate Limit ImpactBest For
Polling (every 5s)0–5s staleHigh — burns API callsGetting started, low-frequency markets
Polling (every 30s)0–30s staleModerateLess competitive markets
WebSocketNear real-timeMinimalProduction, competitive markets

If your odds provider supports WebSocket feeds, use them. Polling at aggressive intervals (sub-5-second) will burn through API quotas fast.


Stage 2: The Comparison Engine

The comparison engine takes normalized odds data and finds the best available odds per outcome across all books, then checks for arbitrage.

from dataclasses import dataclass

@dataclass
class ArbOpportunity:
    event: str
    outcomes: list[dict]  # [{name, book, odds, implied_prob, stake}]
    total_implied: float
    arb_pct: float
    profit: float
    total_stake: float

def find_arbs(events: list[dict], min_arb_pct: float = 0.5) -> list[ArbOpportunity]:
    """Scan events for arbitrage opportunities.

    Args:
        events: Normalized event data from the odds API.
        min_arb_pct: Minimum arb percentage to report (filters noise).

    Returns:
        List of ArbOpportunity objects for profitable arbs.
    """
    arbs = []

    for event in events:
        # Find best odds for each outcome across all books
        best_odds = {}  # outcome_name -> {book, odds, implied}

        for bookmaker in event.get("bookmakers", []):
            for market in bookmaker.get("markets", []):
                for outcome in market.get("outcomes", []):
                    name = outcome["name"]
                    odds = outcome["price"]
                    implied = 1 / odds

                    if name not in best_odds or odds > best_odds[name]["odds"]:
                        best_odds[name] = {
                            "name": name,
                            "book": bookmaker["key"],
                            "odds": odds,
                            "implied": implied,
                        }

        if len(best_odds) < 2:
            continue

        # Check for arb
        outcomes = list(best_odds.values())
        total_implied = sum(o["implied"] for o in outcomes)

        if total_implied >= 1.0:
            continue  # No arb

        arb_pct = ((1 / total_implied) - 1) * 100

        if arb_pct < min_arb_pct:
            continue  # Below threshold

        arbs.append(ArbOpportunity(
            event=event.get("home_team", "") + " vs " + event.get("away_team", ""),
            outcomes=outcomes,
            total_implied=total_implied,
            arb_pct=arb_pct,
            profit=0,  # Calculated in stage 3
            total_stake=0,
        ))

    return arbs

Pre-Filtering for Speed

If you’re scanning hundreds of events across 10+ books, the comparison engine needs to be fast. Two optimizations:

  1. Skip stale events — ignore events that started more than N minutes ago
  2. Track previous state — only re-evaluate an event when any book’s odds change (use a hash of the odds vector)

Stage 3: Stake Calculation

Once you’ve found an arb, you need to calculate exactly how much to stake on each outcome. The goal: equal profit regardless of which outcome wins.

Equal-Profit Staking

def calculate_stakes(
    outcomes: list[dict],
    total_stake: float = 1000.0,
    fee_pct: float = 0.0
) -> list[dict]:
    """Calculate optimal stakes for equal profit across all outcomes.

    Args:
        outcomes: List of dicts with 'odds' (decimal) and 'implied' keys.
        total_stake: Total amount to distribute across all bets.
        fee_pct: Platform fee as a percentage (e.g., 2.0 for 2%).

    Returns:
        Updated outcomes list with 'stake', 'payout', and 'profit' keys.
    """
    total_implied = sum(o["implied"] for o in outcomes)
    fee_mult = 1 - (fee_pct / 100)

    for outcome in outcomes:
        # Stake proportional to implied probability
        outcome["stake"] = total_stake * (outcome["implied"] / total_implied)
        # Payout if this outcome wins, after fees
        outcome["payout"] = outcome["stake"] * outcome["odds"] * fee_mult
        # Profit = payout minus total stake (not just this outcome's stake)
        outcome["profit"] = outcome["payout"] - total_stake

    return outcomes

Fee-Adjusted Calculation

Different platforms charge different fees:

PlatformFee StructureEffective Fee
Most sportsbooksVig baked into odds0% additional (already reflected)
Polymarket~2% on net winnings2% on profit portion
Kalshi0% on contract payouts0%
Betfair Exchange2–5% commission on net winningsVaries by market

When arbing across sportsbook-to-sportsbook, the vig is already in the odds you see — so fee_pct should typically be 0. When arbing across sportsbook-to-exchange or sportsbook-to-prediction-market, you may need to factor in the exchange/platform fee.

Bankroll Management

Not every dollar should go into a single arb. A simple approach:

def kelly_stake(arb_pct: float, bankroll: float, fraction: float = 0.25) -> float:
    """Calculate stake using fractional Kelly criterion.

    Args:
        arb_pct: Arb percentage (e.g., 2.5 for 2.5%).
        bankroll: Total available bankroll.
        fraction: Kelly fraction (0.25 = quarter Kelly, conservative).

    Returns:
        Maximum total stake for this arb.
    """
    # For arbs, the edge is known and risk-free, so Kelly = edge / 1
    # But we use fractional Kelly for practical reasons (account limits,
    # execution risk, keeping capital available for multiple arbs)
    kelly_pct = arb_pct / 100
    return bankroll * kelly_pct * fraction

Quarter-Kelly is a common starting point. It keeps you from over-concentrating capital in a single arb, which matters when you need liquidity for the next opportunity.


Stage 4: Execution

Detection without execution is just research. The execution engine places the actual bets.

Speed Is Everything

The window for a typical arb is measured in seconds. Your execution layer needs to:

  1. Place all legs in parallel — don’t wait for one bet to confirm before placing the next
  2. Use pre-authenticated sessions — no login delays at execution time
  3. Minimize network hops — run from a low-latency location (cloud VMs near the books’ servers)
import asyncio
import aiohttp

async def place_bet(session, book_api, bet_details):
    """Place a single bet via a sportsbook's API."""
    try:
        async with session.post(
            book_api["endpoint"],
            json=bet_details,
            headers=book_api["auth_headers"],
            timeout=aiohttp.ClientTimeout(total=5)
        ) as response:
            result = await response.json()
            return {"book": book_api["name"], "success": True, "result": result}
    except Exception as e:
        return {"book": book_api["name"], "success": False, "error": str(e)}

async def execute_arb(arb: ArbOpportunity, book_apis: dict):
    """Execute all legs of an arb simultaneously."""
    async with aiohttp.ClientSession() as session:
        tasks = []
        for outcome in arb.outcomes:
            book = outcome["book"]
            if book not in book_apis:
                continue
            bet = {
                "event_id": arb.event_id,
                "outcome": outcome["name"],
                "stake": round(outcome["stake"], 2),
                "odds": outcome["odds"],
            }
            tasks.append(place_bet(session, book_apis[book], bet))

        results = await asyncio.gather(*tasks)
        return results

Handling Partial Fills

The worst scenario: one leg of your arb executes but the other fails (odds moved, bet rejected, account limited). You’re now exposed to one-sided risk.

Mitigation strategies:

  1. Check odds immediately before execution — abort if the arb has disappeared
  2. Set maximum acceptable odds drift — if odds have moved more than X% since detection, skip
  3. Have a hedging plan — if one leg fails, immediately hedge the executed leg at the best available odds (accepting a reduced profit or small loss)
  4. Track partial execution rate — if a specific book frequently rejects bets, deprioritize it

Why Automation Is Essential

Manual arb betting — scanning odds by eye, calculating stakes on a calculator, placing bets by hand — doesn’t work at scale for three reasons:

  1. Speed: Arbs last 30 seconds to 5 minutes. By the time you spot one, do the math, and navigate two sportsbook apps, the odds have moved.
  2. Scale: Monitoring 10+ books across hundreds of events requires checking thousands of odds combinations. No human can maintain this.
  3. Consistency: A bot runs 24/7, never gets distracted, never fat-fingers a stake, and never second-guesses a profitable opportunity.

The operational edge in arb betting is entirely in the automation. The math is simple — every bettor knows about arbitrage. The winners are the ones who detect and execute faster.


Putting It All Together

Here’s a simplified main loop that ties all four stages together:

import time

def run_arb_scanner(
    sports: list[str],
    bankroll: float = 10000,
    min_arb_pct: float = 1.0,
    poll_interval: int = 10,
):
    """Main loop: scan for arbs, calculate stakes, log opportunities."""

    print(f"Starting arb scanner | Bankroll: ${bankroll:,.0f} | Min arb: {min_arb_pct}%")

    while True:
        for sport in sports:
            # Stage 1: Fetch odds
            events = fetch_odds(sport)

            # Stage 2: Find arbs
            arbs = find_arbs(events, min_arb_pct=min_arb_pct)

            for arb in arbs:
                # Stage 3: Calculate stakes
                max_stake = kelly_stake(arb.arb_pct, bankroll)
                arb.total_stake = min(max_stake, bankroll * 0.1)  # Cap at 10% of bankroll
                arb.outcomes = calculate_stakes(arb.outcomes, arb.total_stake)
                arb.profit = min(o["profit"] for o in arb.outcomes)

                print(f"\n{'='*60}")
                print(f"ARB FOUND: {arb.event}")
                print(f"Arb: {arb.arb_pct:.2f}% | Stake: ${arb.total_stake:.2f} | Profit: ${arb.profit:.2f}")
                for o in arb.outcomes:
                    print(f"  {o['name']} @ {o['odds']:.3f} on {o['book']} — ${o['stake']:.2f}")

                # Stage 4: Execute (uncomment when ready for live betting)
                # results = asyncio.run(execute_arb(arb, BOOK_APIS))
                # handle_results(results, arb)

        time.sleep(poll_interval)

# Run it
run_arb_scanner(
    sports=["basketball_nba", "football_nfl", "soccer_epl"],
    bankroll=5000,
    min_arb_pct=1.0,
    poll_interval=10,
)

This is a starting point. A production system adds: persistent logging, database for tracking bets and P&L, alerting (Telegram/Discord), account balance tracking per book, and graceful shutdown handling.


From Sports Arb to Cross-Market Arb

Traditional sports arb compares odds across sportsbooks for the same event. But some of the biggest pricing inefficiencies exist between market types — specifically between sportsbooks and prediction markets like Polymarket and Kalshi.

Consider an event like “Will the Eagles win the Super Bowl?” This is priced simultaneously on:

  • Sportsbooks — as futures odds (e.g., +350 on DraftKings)
  • Polymarket — as a binary contract (e.g., $0.22 per share)
  • Kalshi — as a binary contract (e.g., $0.24 per share)

These markets have different participants, different fee structures, and different settlement mechanisms — creating larger and more persistent pricing discrepancies than you’d find between two sportsbooks alone.

Research from IMDEA Networks found that Polymarket traders captured over $40 million in arbitrage profits during the 2024 U.S. election cycle, primarily from cross-market price discrepancies.

The data pipeline for cross-market arb combines:

  • Sportsbook odds via TheOddsPulse — normalized across 10+ books
  • Polymarket prices via the Polymarket API — real-time CLOB data
  • Kalshi prices via the Kalshi API — REST + WebSocket feeds

The Cross-Market Arbitrage Guide covers this in detail, including fee-adjusted calculations, settlement risk, and a complete event-matching pipeline. The Cross-Market Arb Finder tool shows live cross-market pricing in action.


Frequently Asked Questions

What is a sports betting arbitrage bot?

A sports betting arbitrage bot is software that automatically scans odds across multiple sportsbooks, detects when combined implied probabilities total less than 100% (an arbitrage opportunity), calculates optimal stakes for guaranteed profit regardless of outcome, and optionally executes the bets. The bot profits from pricing inefficiencies between books.

How much can you make with an arb betting bot?

Typical arb margins are 1–3% per opportunity. With $1,000 per arb and 5–10 arbs per day, gross profit ranges from $50–300 daily before accounting for fees, account limits, and execution failures. Profits scale with bankroll size and the number of sportsbooks monitored.

Arbitrage betting is legal in most jurisdictions. However, sportsbooks discourage it and may limit or close accounts of suspected arb bettors. Using multiple sportsbooks, varying bet sizes, and mixing arb bets with recreational bets can help extend account longevity.

What programming language is best for an arb bot?

Python is the most common choice due to its ecosystem (requests, websockets, pandas) and rapid development speed. For latency-critical execution, some teams use Go, Rust, or C++ for the execution layer while keeping Python for the scanning and calculation components.

How fast do sports arb opportunities disappear?

Most sports arb opportunities last between 30 seconds and 5 minutes. Sharp lines move within seconds of an odds change at one book. This is why automation is essential — manual arb betting cannot consistently capture opportunities before they close.


See Also


This guide is maintained by AgentBets.ai. Found an error? Let us know on Twitter.

Not financial advice. Built for builders.