An arbitrage exists when 1/odds_A + 1/odds_B < 1 across two platforms on the same event. The profit margin is 1 minus that sum. Use the dutching formula to size stakes: stake_i = total_stake × (1/d_i) / Σ(1/d_j). The hard part isn’t the math — it’s execution speed, stake limits, and account longevity.

Why This Matters for Agents

Arbitrage detection is the canonical use case for autonomous betting agents. A human can monitor two sportsbooks. An agent can monitor 40 sportsbooks, 3 prediction markets, and 200 props markets simultaneously, computing arb margins in under 100 milliseconds per scan cycle.

This is Layer 3 — Trading. The agent’s arb scanner sits at the core of its trading infrastructure, pulling odds from the Prediction Market API Reference endpoints and sportsbook feeds via The Odds API, identifying mispricing, computing optimal stakes, and executing across platforms before the window closes. The Agent Betting Stack positions arb detection as a Layer 3 function because it requires real-time data ingestion, cross-platform order routing, and sub-second execution — all infrastructure concerns. The intelligence layer (Layer 4) decides whether a detected arb is worth the execution risk; Layer 3 handles the detection and execution mechanics.

The Math

Two-Way Arbitrage Condition

The fundamental arb condition for a two-outcome event (Team A vs. Team B, YES vs. NO):

Arb Condition:  1/d_A + 1/d_B < 1

Where:
  d_A = decimal odds for outcome A on Platform 1
  d_B = decimal odds for outcome B on Platform 2

Each term 1/d_i is the implied probability of outcome i. If the sum of implied probabilities across the best available odds is less than 1.0 (100%), the gap is your guaranteed profit margin.

Arb Margin = 1 - (1/d_A + 1/d_B)

The margin represents your return per dollar of total risk. A 3% arb margin means $3 guaranteed profit per $100 deployed.

Three-Way Arbitrage (1X2 Markets)

Soccer, hockey, and other draw-possible sports create three-way markets: Home Win, Draw, Away Win. The arb condition extends naturally:

Arb Condition:  1/d_home + 1/d_draw + 1/d_away < 1

Where each d comes from whichever platform offers the best odds
for that outcome.

Three-way arbs are more common than two-way arbs because three independent odds across multiple bookmakers produce more combinatorial opportunities for mispricing. With 10 sportsbooks and 3 outcomes, there are 10³ = 1,000 possible combinations to check per match.

Cross-Platform Arbs: Prediction Markets vs. Sportsbooks

The highest-margin arbs often appear between structurally different platforms — a prediction market like Polymarket and a traditional sportsbook. These platforms price the same event using fundamentally different mechanisms:

Prediction Market:  Orderbook-driven, CLOB matching
Sportsbook:         Model-driven, market-maker sets line

Cross-platform arb condition:
  price_YES_polymarket + price_NO_kalshi < $1.00

Or equivalently:
  price_YES_polymarket + (1 - price_YES_sportsbook_implied) < $1.00

For example, if Polymarket prices “Lakers win NBA Finals” YES at $0.42 and a sportsbook implies a NO probability of $0.53 (meaning YES = $0.47), buying YES on Polymarket at $0.42 and betting the NO side on the sportsbook captures the $0.05 spread minus fees.

The Dutching Formula

Once you detect an arb, you need to allocate stakes across outcomes to guarantee equal profit regardless of which outcome wins. This is dutching.

For an n-outcome arb with decimal odds d_1, d_2, …, d_n:

stake_i = total_stake × (1/d_i) / Σ(1/d_j)

Guaranteed profit = total_stake × (1 - Σ(1/d_j)) / Σ(1/d_j)

Each stake is proportional to the inverse of its odds. The intuition: outcomes with lower odds (higher probability) get larger stakes because the payout per dollar is lower.

Dutching Derivation

Let S = total stake, and s_i = individual stake on outcome i. If outcome i wins, the agent receives s_i × d_i. For equal profit across all outcomes, we need:

s_1 × d_1 = s_2 × d_2 = ... = s_n × d_n = P  (equal payout)

Subject to: s_1 + s_2 + ... + s_n = S          (total stake constraint)

From the equal-payout condition: s_i = P / d_i

Substituting into the constraint: Σ(P / d_i) = S, so P = S / Σ(1/d_i)

Therefore: s_i = S / (d_i × Σ(1/d_j))

Which simplifies to: s_i = S × (1/d_i) / Σ(1/d_j)

The guaranteed profit per outcome is P - S:

Profit = S / Σ(1/d_j) - S = S × (1/Σ(1/d_j) - 1) = S × (1 - Σ(1/d_j)) / Σ(1/d_j)

Why Arbs Exist

Arbs aren’t anomalies — they’re structural consequences of market fragmentation. Four mechanisms create them:

1. Information asymmetry. Sharp books like Pinnacle and Bookmaker move lines within seconds of new information. Soft books like BetOnline and MyBookie adjust minutes or hours later. During that lag, the sharp-side price and the soft-side price diverge enough to create arbs.

2. Model disagreements. A sportsbook’s in-house model prices a prop at -130 while the prediction market’s crowd wisdom prices the equivalent event at 48%. Those two numbers imply different probabilities (56.5% vs. 48%), creating a 4.5% cross-platform arb window.

3. Platform-specific liquidity. Polymarket might have deep liquidity on political markets but thin liquidity on sports. Kalshi might be the reverse. Different liquidity depths produce different spreads and midpoints on the same event.

4. Vig asymmetry. Books with higher vig create wider spreads. A high-vig book offering the underdog at +180 while a low-vig book offers the favorite at -160 can create an arb purely from the vig differential. The AgentBets Vig Index tracks which books are most likely to be on the wrong side of an arb — higher-vig books appear in arbs more frequently.

Worked Examples

Example 1: Two-Way Sportsbook Arb

BetOnline offers Lakers ML (moneyline) at +165. Bookmaker offers Celtics ML at -150. Convert to decimal odds:

Lakers (BetOnline):   +165 → decimal = 1 + 165/100 = 2.65
Celtics (Bookmaker):  -150 → decimal = 1 + 100/150 = 1.667

Sum of implied probabilities:
  1/2.65 + 1/1.667 = 0.3774 + 0.5999 = 0.9773

Arb margin = 1 - 0.9773 = 0.0227 = 2.27%

This is a live arb. With a $1,000 total stake:

Stake on Lakers  = $1,000 × (1/2.65) / 0.9773 = $1,000 × 0.3861 = $386.10
Stake on Celtics = $1,000 × (1/1.667) / 0.9773 = $1,000 × 0.6139 = $613.90

If Lakers win:   $386.10 × 2.65 = $1,023.17  → Profit = $23.17
If Celtics win:  $613.90 × 1.667 = $1,023.17  → Profit = $23.17

Guaranteed $23.17 profit on $1,000, or 2.32% return, regardless of outcome.

Example 2: Cross-Platform Prediction Market Arb

Polymarket “Will Trump win the 2028 Republican nomination?” YES best ask = $0.58. Kalshi same event NO best ask = $0.38.

Total cost of YES + NO = $0.58 + $0.38 = $0.96

If Trump wins:    YES pays $1.00.  Profit = $1.00 - $0.96 = $0.04
If Trump loses:   NO pays $1.00.   Profit = $1.00 - $0.96 = $0.04

Arb margin = 1 - 0.96 = 4.0% before fees

After Polymarket’s ~2% winner fee, the adjusted margin narrows. If Trump wins, you pay 2% on the $0.42 profit from the YES leg: $0.42 × 0.02 = $0.0084. Net profit drops from $0.04 to $0.0316 — still positive, still an arb.

Example 3: Three-Way Soccer Arb

English Premier League: Arsenal vs. Chelsea. Odds scraped from three different books:

Arsenal Win (Bovada):      2.40
Draw (BetOnline):          3.60
Chelsea Win (Bookmaker):   3.25

Sum = 1/2.40 + 1/3.60 + 1/3.25
    = 0.4167 + 0.2778 + 0.3077
    = 1.0022

Sum > 1.0 — no arb. The overround is 0.22%. Now suppose BetOnline moves the draw to 3.80:

Sum = 1/2.40 + 1/3.80 + 1/3.25
    = 0.4167 + 0.2632 + 0.3077
    = 0.9875

Arb margin = 1 - 0.9875 = 1.25%

An arb appears. On $5,000 total stake:

Stake Arsenal = $5,000 × 0.4167 / 0.9875 = $2,109.62
Stake Draw    = $5,000 × 0.2632 / 0.9875 = $1,332.07
Stake Chelsea = $5,000 × 0.3077 / 0.9875 = $1,558.31

Payout any outcome = $5,063.29 → Profit = $63.29 (1.27%)

Implementation

import numpy as np
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class ArbOpportunity:
    """Represents a detected arbitrage opportunity."""
    event: str
    outcomes: list[str]
    odds: list[float]          # decimal odds, one per outcome
    platforms: list[str]       # platform name per outcome
    margin: float              # arb margin (1 - sum of inverse odds)
    stakes: list[float]        # optimal stake per outcome for $1 total
    guaranteed_profit: float   # profit per $1 total stake


@dataclass
class OddsEntry:
    """Odds for one outcome from one platform."""
    platform: str
    outcome: str
    decimal_odds: float
    american_odds: Optional[int] = None


def american_to_decimal(american: int) -> float:
    """Convert American odds to decimal odds.

    Args:
        american: American odds (e.g., -110, +250)

    Returns:
        Decimal odds (e.g., 1.909, 3.50)
    """
    if american > 0:
        return 1 + american / 100
    else:
        return 1 + 100 / abs(american)


def detect_two_way_arb(
    odds_a: float,
    odds_b: float,
    fee_rate: float = 0.0
) -> Optional[dict]:
    """Detect arbitrage between two opposing outcomes.

    Args:
        odds_a: Decimal odds for outcome A (from platform 1)
        odds_b: Decimal odds for outcome B (from platform 2)
        fee_rate: Combined fee rate across platforms (decimal)

    Returns:
        Dict with arb details if arb exists, None otherwise.
    """
    implied_sum = 1 / odds_a + 1 / odds_b
    margin = 1 - implied_sum

    if margin <= fee_rate:
        return None

    net_margin = margin - fee_rate
    total_inverse = implied_sum

    stake_a = (1 / odds_a) / total_inverse
    stake_b = (1 / odds_b) / total_inverse

    return {
        "margin": margin,
        "net_margin": net_margin,
        "implied_sum": implied_sum,
        "stake_a_pct": stake_a,
        "stake_b_pct": stake_b,
        "profit_per_dollar": net_margin / total_inverse,
    }


def detect_n_way_arb(
    odds: list[float],
    fee_rate: float = 0.0
) -> Optional[dict]:
    """Detect arbitrage across n outcomes.

    Args:
        odds: List of best decimal odds, one per outcome.
        fee_rate: Combined fee rate across platforms (decimal).

    Returns:
        Dict with arb details if arb exists, None otherwise.
    """
    inverses = np.array([1 / o for o in odds])
    implied_sum = float(np.sum(inverses))
    margin = 1 - implied_sum

    if margin <= fee_rate:
        return None

    net_margin = margin - fee_rate
    stakes = inverses / implied_sum  # dutching allocation

    return {
        "margin": margin,
        "net_margin": net_margin,
        "implied_sum": implied_sum,
        "stakes": stakes.tolist(),
        "profit_per_dollar": net_margin / implied_sum,
    }


def compute_stakes(
    total_bankroll: float,
    odds: list[float],
) -> list[float]:
    """Compute optimal dutching stakes for a given bankroll.

    Args:
        total_bankroll: Total amount to allocate across all outcomes.
        odds: Decimal odds per outcome.

    Returns:
        List of dollar amounts to stake on each outcome.
    """
    inverses = np.array([1 / o for o in odds])
    implied_sum = float(np.sum(inverses))
    stakes = total_bankroll * inverses / implied_sum
    return stakes.tolist()


def scan_sportsbook_arbs(
    events: list[dict],
    min_margin: float = 0.005,
    max_fee: float = 0.0
) -> list[ArbOpportunity]:
    """Scan a list of events for two-way arb opportunities.

    Each event dict has format:
    {
        "name": "Lakers vs Celtics",
        "outcomes": {
            "Lakers": [{"platform": "BetOnline", "odds": 2.65}, ...],
            "Celtics": [{"platform": "Bookmaker", "odds": 1.667}, ...]
        }
    }

    Args:
        events: List of event dicts with odds per outcome per platform.
        min_margin: Minimum arb margin to report (default 0.5%).
        max_fee: Maximum combined fee rate to deduct.

    Returns:
        List of ArbOpportunity objects sorted by margin descending.
    """
    arbs = []

    for event in events:
        outcomes = list(event["outcomes"].keys())
        if len(outcomes) < 2:
            continue

        # For two-way: find best odds for each outcome across platforms
        if len(outcomes) == 2:
            best = {}
            for outcome_name in outcomes:
                platform_odds = event["outcomes"][outcome_name]
                best_entry = max(platform_odds, key=lambda x: x["odds"])
                best[outcome_name] = best_entry

            names = list(best.keys())
            odds_list = [best[n]["odds"] for n in names]
            platforms = [best[n]["platform"] for n in names]

            result = detect_two_way_arb(odds_list[0], odds_list[1], max_fee)
            if result and result["net_margin"] >= min_margin:
                stakes = compute_stakes(1.0, odds_list)
                arbs.append(ArbOpportunity(
                    event=event["name"],
                    outcomes=names,
                    odds=odds_list,
                    platforms=platforms,
                    margin=result["margin"],
                    stakes=stakes,
                    guaranteed_profit=result["profit_per_dollar"],
                ))

        # For three-way (or n-way): find best odds per outcome
        else:
            best = {}
            for outcome_name in outcomes:
                platform_odds = event["outcomes"][outcome_name]
                best_entry = max(platform_odds, key=lambda x: x["odds"])
                best[outcome_name] = best_entry

            names = list(best.keys())
            odds_list = [best[n]["odds"] for n in names]
            platforms = [best[n]["platform"] for n in names]

            result = detect_n_way_arb(odds_list, max_fee)
            if result and result["net_margin"] >= min_margin:
                stakes = compute_stakes(1.0, odds_list)
                arbs.append(ArbOpportunity(
                    event=event["name"],
                    outcomes=names,
                    odds=odds_list,
                    platforms=platforms,
                    margin=result["margin"],
                    stakes=stakes,
                    guaranteed_profit=result["profit_per_dollar"],
                ))

    arbs.sort(key=lambda a: a.margin, reverse=True)
    return arbs


# ── Example: scan with real-format data ──────────────────────────

if __name__ == "__main__":
    # Simulated multi-platform odds (format matches The Odds API output)
    events = [
        {
            "name": "Lakers vs Celtics — NBA 2026-03-22",
            "outcomes": {
                "Lakers": [
                    {"platform": "BetOnline", "odds": 2.65},
                    {"platform": "Bovada", "odds": 2.50},
                    {"platform": "Bookmaker", "odds": 2.55},
                ],
                "Celtics": [
                    {"platform": "BetOnline", "odds": 1.55},
                    {"platform": "Bovada", "odds": 1.60},
                    {"platform": "Bookmaker", "odds": 1.667},
                ],
            }
        },
        {
            "name": "Arsenal vs Chelsea — EPL 2026-03-22",
            "outcomes": {
                "Arsenal": [
                    {"platform": "BetOnline", "odds": 2.40},
                    {"platform": "Bovada", "odds": 2.35},
                ],
                "Draw": [
                    {"platform": "BetOnline", "odds": 3.80},
                    {"platform": "Bovada", "odds": 3.50},
                ],
                "Chelsea": [
                    {"platform": "BetOnline", "odds": 3.10},
                    {"platform": "Bookmaker", "odds": 3.25},
                ],
            }
        },
    ]

    arbs = scan_sportsbook_arbs(events, min_margin=0.001)

    if not arbs:
        print("No arbs found.")
    else:
        for arb in arbs:
            print(f"\n{'='*60}")
            print(f"EVENT: {arb.event}")
            print(f"MARGIN: {arb.margin:.2%}")
            print(f"PROFIT per $1,000: ${arb.guaranteed_profit * 1000:.2f}")
            print()
            total_stake = 1000
            stakes = compute_stakes(total_stake, arb.odds)
            for i, outcome in enumerate(arb.outcomes):
                print(f"  {outcome:>12} @ {arb.odds[i]:.3f} on {arb.platforms[i]:<12} "
                      f"→ Stake ${stakes[i]:>8.2f}  "
                      f"→ Payout ${stakes[i] * arb.odds[i]:>8.2f}")

The Odds API Integration

An agent in production pulls live odds from The Odds API, which aggregates lines from 40+ sportsbooks. Here’s the integration pattern:

import requests
import numpy as np
from itertools import product


def fetch_odds_api(
    api_key: str,
    sport: str = "basketball_nba",
    regions: str = "us,us2,eu",
    markets: str = "h2h",
    odds_format: str = "decimal"
) -> list[dict]:
    """Fetch live odds from The Odds API.

    Args:
        api_key: Your Odds API key.
        sport: Sport key (e.g., basketball_nba, soccer_epl).
        regions: Comma-separated regions (us, us2, eu, uk, au).
        markets: Market type (h2h, spreads, totals).
        odds_format: decimal or american.

    Returns:
        List of event dicts from the API.
    """
    url = "https://api.the-odds-api.com/v4/sports/{}/odds/".format(sport)
    resp = requests.get(url, params={
        "apiKey": api_key,
        "regions": regions,
        "markets": markets,
        "oddsFormat": odds_format,
    })
    resp.raise_for_status()
    return resp.json()


def parse_odds_api_to_events(raw_events: list[dict]) -> list[dict]:
    """Convert Odds API response to our scanner format.

    Args:
        raw_events: Raw JSON from The Odds API.

    Returns:
        List of event dicts compatible with scan_sportsbook_arbs().
    """
    parsed = []
    for event in raw_events:
        outcomes_map: dict[str, list[dict]] = {}
        for bookmaker in event.get("bookmakers", []):
            platform = bookmaker["title"]
            for market in bookmaker.get("markets", []):
                if market["key"] != "h2h":
                    continue
                for outcome in market["outcomes"]:
                    name = outcome["name"]
                    odds = outcome["price"]
                    if name not in outcomes_map:
                        outcomes_map[name] = []
                    outcomes_map[name].append({
                        "platform": platform,
                        "odds": odds,
                    })

        if len(outcomes_map) >= 2:
            parsed.append({
                "name": f"{event['home_team']} vs {event['away_team']}",
                "outcomes": outcomes_map,
            })

    return parsed


# ── Full pipeline ────────────────────────────────────────────────

def run_arb_scan(api_key: str, sport: str = "basketball_nba") -> list:
    """End-to-end arb scan: fetch odds → detect arbs → return results.

    Args:
        api_key: The Odds API key.
        sport: Sport key to scan.

    Returns:
        List of ArbOpportunity objects.
    """
    raw = fetch_odds_api(api_key, sport=sport)
    events = parse_odds_api_to_events(raw)
    arbs = scan_sportsbook_arbs(events, min_margin=0.005)
    return arbs

Cross-Platform Scanner: Polymarket + Sportsbooks

import requests


def fetch_polymarket_price(condition_id: str) -> dict:
    """Fetch YES/NO prices from Polymarket for a condition.

    Args:
        condition_id: The Polymarket condition ID.

    Returns:
        Dict with yes_price, no_price, and spread.
    """
    url = f"https://clob.polymarket.com/midpoint?token_id={condition_id}"
    resp = requests.get(url)
    resp.raise_for_status()
    data = resp.json()
    mid = float(data.get("mid", 0))
    return {
        "yes_price": mid,
        "no_price": 1 - mid,
    }


def detect_cross_platform_arb(
    pm_yes_price: float,
    sportsbook_no_implied: float,
    pm_fee: float = 0.02,
    sb_fee: float = 0.0
) -> Optional[dict]:
    """Detect arb between a prediction market YES and sportsbook NO.

    Args:
        pm_yes_price: Polymarket YES price (e.g., 0.42).
        sportsbook_no_implied: Sportsbook implied NO probability (e.g., 0.53).
        pm_fee: Polymarket fee on winnings (default 2%).
        sb_fee: Sportsbook effective fee rate.

    Returns:
        Dict with arb details or None.
    """
    total_cost = pm_yes_price + sportsbook_no_implied
    raw_margin = 1 - total_cost

    if raw_margin <= 0:
        return None

    # Worst-case fee: PM fee on YES profit if event happens
    pm_profit_if_yes = 1 - pm_yes_price
    pm_fee_cost = pm_fee * pm_profit_if_yes

    # Sportsbook fee on NO profit if event doesn't happen
    sb_profit_if_no = 1 - sportsbook_no_implied
    sb_fee_cost = sb_fee * sb_profit_if_no

    max_fee = max(pm_fee_cost, sb_fee_cost)
    net_margin = raw_margin - max_fee

    if net_margin <= 0:
        return None

    return {
        "raw_margin": raw_margin,
        "net_margin": net_margin,
        "total_cost": total_cost,
        "pm_yes_stake_pct": pm_yes_price / total_cost,
        "sb_no_stake_pct": sportsbook_no_implied / total_cost,
    }


# ── Example: Trump 2028 cross-platform arb ──────────────────────

pm_yes = 0.42   # Polymarket YES price
sb_no = 0.53    # Sportsbook implied NO probability (from +170 underdog line)

result = detect_cross_platform_arb(pm_yes, sb_no)
if result:
    print(f"Cross-platform arb detected!")
    print(f"  Raw margin:   {result['raw_margin']:.2%}")
    print(f"  Net margin:   {result['net_margin']:.2%}")
    print(f"  Total cost:   ${result['total_cost']:.2f} per $1 payout")
    print(f"  PM stake:     {result['pm_yes_stake_pct']:.1%} of total")
    print(f"  SB stake:     {result['sb_no_stake_pct']:.1%} of total")

Limitations and Edge Cases

Execution Risk

The largest killer of arb profitability isn’t bad math — it’s failed execution. An arb that exists at time T may vanish by time T + 200ms. Execution risk has four components:

1. Price staleness. The Odds API caches odds with 30-60 second latency. An arb detected from cached data may already be gone. Direct sportsbook APIs (where available) reduce latency to 1-5 seconds. Polymarket’s WebSocket feed provides sub-second updates.

2. Stake limits. Soft sportsbooks impose low limits on sharp bettors. BetUS might limit your NBA moneyline to $250 — not enough for a profitable arb after accounting for the other leg’s cost. An agent must track per-platform stake limits and only flag arbs where both legs can fill at sufficient size.

3. Account restrictions. Sportsbooks profile winning bettors and restrict accounts. An agent that consistently takes arbs on Bovada will get limited within weeks. This is a structural constraint, not a technical one — the math works, but the business model of soft books doesn’t accommodate sharp action.

4. Settlement mismatch. A Polymarket contract and a sportsbook line might define the same event differently. “Will Biden win the 2028 election?” on Polymarket settles on the electoral college result; a sportsbook prop might settle on the popular vote. Verify settlement rules before treating two positions as hedged.

The Execution Risk Model

Expected arb profit incorporates these risks:

E[profit] = margin × P(fill_both_legs) × (1 - P(price_slip)) - cost_of_capital

Where:
  margin              = 1 - Σ(1/d_i)
  P(fill_both_legs)   = P(fill_A) × P(fill_B)  (assuming independence)
  P(price_slip)       = f(latency, volatility, time_since_odds_update)
  cost_of_capital     = locked_amount × risk_free_rate × settlement_time

An agent should only execute arbs where E[profit] > 0 after this adjustment. In practice, this means ignoring arbs under ~1% margin on sportsbooks (execution risk eats the edge) and under ~3% margin on cross-platform arbs involving prediction markets (settlement time locks capital).

Market-Specific Gotchas

Polymarket: Fees are on net winnings, not on the trade. An arb where you win $0.01 on a $100 position pays almost zero fees. But if one leg wins big, the fee matters. Model both outcomes separately.

Kalshi: Lower liquidity than Polymarket on most markets. Orderbook depth may not support the position size your arb requires. Check depth before sending orders.

Sportsbooks: Line movements after your first leg fills can turn an arb into a loss on the second leg. Execute both legs as close to simultaneously as possible — this is where betting bot infrastructure provides the sub-second execution agents need.

FAQ

How do you detect arbitrage between two sportsbooks?

Convert both sides to implied probabilities and sum them. If 1/odds_A + 1/odds_B < 1 for opposing outcomes across two books, an arb exists. The profit margin equals 1 minus that sum. For example, if BetOnline offers the Lakers at decimal 2.65 and Bookmaker offers the Celtics at 1.667, the sum is 1/2.65 + 1/1.667 = 0.9773 — a 2.27% arb margin before fees.

What is the dutching formula for optimal arbitrage stake sizing?

The dutching formula allocates stakes proportionally to the inverse of each outcome’s decimal odds: stake_i = total_stake × (1/d_i) / Σ(1/d_j). This guarantees equal profit regardless of which outcome wins. For a $1,000 total stake on a two-way arb with odds 2.65 and 1.667, you’d bet $386.10 on the first outcome and $613.90 on the second. Both outcomes pay exactly $1,023.17.

Can agents find arbitrage between prediction markets and sportsbooks?

Yes. Cross-platform arbs arise when a Polymarket YES contract and a sportsbook underdog line on the same event imply probabilities summing to less than 100%. An agent using The Odds API for sportsbook odds and the Polymarket CLOB API for prediction market prices can scan for these discrepancies in real time. The AgentBets Arbitrage Calculator automates this detection.

Why do arbitrage opportunities exist if markets are efficient?

Arbs exist because of structural fragmentation: different platforms use different pricing models, have different liquidity pools, update at different speeds, and serve different customer bases. Offshore sportsbooks adjust lines slowly compared to sharp books. Prediction markets price events via orderbook while sportsbooks use model-driven odds. These structural gaps create persistent arb windows, especially during high-volatility periods like injury news or election night.

What is execution risk in arbitrage betting?

Execution risk is the probability that one leg of an arb fails to fill at the expected price. Causes include price movement during execution, stake limits imposed by sportsbooks, account restrictions on winning bettors, and API latency. Expected arb profit = margin × fill_rate × (1 - slippage_probability). An agent must model execution risk per-platform and only take arbs where expected profit after risk adjustment remains positive.

What’s Next

This guide covers the detection and sizing math. The next step is handling markets with more than two outcomes and understanding the combinatorial explosion:

  • Multi-outcome arb math: Multi-Outcome Markets and Combinatorial Math extends dutching to conditional markets, exotic bets, and correlated legs.
  • Run the numbers yourself: The Arbitrage Calculator lets you input odds from any platforms and instantly computes arb margins, optimal stakes, and profit projections.
  • Foundation math: Prediction Market Math 101 covers the price-to-probability conversion and no-arbitrage condition that underpin everything here.
  • Who’s mispriced? The AgentBets Vig Index ranks sportsbooks by overround — books with the highest vig are the most likely to appear on one side of an arb.
  • Compare odds platforms: The comparison hub shows side-by-side odds across sportsbooks and prediction markets.