Middle Bets: How to Find Middling Opportunities in Python (Free Odds API)

Middle Bets - OddsPapi API Blog
How To Guides June 10, 2026

Middle bets are the closest thing the betting world has to a free roll: two wagers on the same game where you win both if the result lands in a narrow window, and lose only the vig if it doesn’t. The catch is that finding them means watching the alternate-line ladders on a dozen sportsbooks at once and pouncing when two books disagree about the same total.

This guide shows you how to scan for middles programmatically in Python — pulling the full Over/Under ladder across 350+ bookmakers in a single API call, then flagging every middle window with its break-even hit rate. All code is tested live against the OddsPapi API.

What Is a Middle Bet?

A middle happens when two sportsbooks hang different lines on the same market. You bet the Over at the lower number and the Under at the higher number. If the final result falls strictly between the two lines, both bets cash. If it doesn’t, one leg wins and one loses, so you’re only out the juice.

Classic example from a real MLB total (captured June 4, 2026, Brewers vs Giants):

Leg Bet Book Price
1 Over 11.5 runs Polymarket 1.667
2 Under 12.5 runs Polymarket 2.00

Stake one unit on each leg. If the game lands on exactly 12 runs, the Over 11.5 cashes and the Under 12.5 cashes — you collect on both. Any other total, one bet wins and one loses. That’s the whole idea: you’ve bought the number 12.

Middles vs Arbitrage: Don’t Confuse Them

People lump middling in with arbitrage betting, but the payoff structure is the opposite:

Arbitrage Middle
Outcome if it works Small guaranteed profit, every time Large profit, only when the result lands in the gap
Outcome if it doesn’t N/A — it always works Small loss (the vig)
Risk None (in theory) Low, but real
Edge source Books disagree on price Books disagree on the line

An arb locks in profit regardless of the result. A middle is a low-risk lottery ticket: you risk the juice to win a big payout if the game cooperates. Both rely on the same thing — books disagreeing — so the more bookmakers you watch, the more opportunities surface.

The Old Way vs OddsPapi

The Old Way With OddsPapi
Refresh 8 sportsbook tabs by hand One /odds call returns every book’s full ladder
~40 books on a generic odds API 350+ bookmakers, including sharps and prediction markets
Eyeball alt lines for line disagreement Scan the whole Over/Under ladder in code
Miss windows that open and close in minutes Poll the feed and alert in real time
No way to backtest how often middles hit Free historical odds to study line behavior

Line diversity is the whole game. With only 40 books, most of them copy the same total and middles are vanishingly rare. With 350+ books — sharps like Pinnacle, US books, and prediction markets like Polymarket and Kalshi all pricing the same game — lines disagree far more often, and that disagreement is exactly what a middle scanner eats.

Step 1: Authenticate

OddsPapi uses a query-parameter API key, not a header. Grab a free key and test it:

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"

def get(path, **params):
    params["apiKey"] = API_KEY
    return requests.get(f"{BASE}/{path}", params=params).json()

# Smoke test
print(get("sports")[:1])

Step 2: Find Today’s Baseball Fixtures

Middles love high-scoring sports with deep alt-line ladders — baseball run totals are a perfect hunting ground. Pull the MLB slate (sportId=13):

fixtures = get("fixtures", sportId=13, **{"from": "2026-06-04", "to": "2026-06-06"})
live = [f for f in fixtures if f["hasOdds"]]

for f in live[:5]:
    print(f["fixtureId"], f["participant1Name"], "vs", f["participant2Name"])

Only fixtures with hasOdds: true will return a pricing payload, so filter on it before you spend calls.

Step 3: Pull the Full Over/Under Ladder

The totals market IDs aren’t values you should memorize — the catalog is huge and changes. Build a {marketId: handicap} lookup from /markets, then walk every book’s ladder. In baseball, the market is named "Over Under (incl. extra innings)":

catalog = get("markets", sportId=13)
TOTALS = "Over Under (incl. extra innings)"
line_of = {m["marketId"]: m["handicap"]
           for m in catalog if m["marketName"] == TOTALS}

def totals_ladder(fixture_id):
    """Return {book: {line: (over_price, under_price)}} for active totals."""
    data = get("odds", fixtureId=fixture_id)
    ladder = {}
    for book, payload in data.get("bookmakerOdds", {}).items():
        for mid, market in payload.get("markets", {}).items():
            if not str(mid).isdigit():
                continue
            line = line_of.get(int(mid))
            if line is None:
                continue
            outcomes = market["outcomes"]
            over_id, under_id = sorted(outcomes, key=lambda x: int(x))
            over = outcomes[over_id]["players"]["0"]
            under = outcomes[under_id]["players"]["0"]
            if over.get("active") and over.get("price"):
                ladder.setdefault(book, {})[line] = (over["price"], under["price"])
    return ladder

Two defensive rules baked in above, and you should never drop them:

  • Filter on active. Suspended or stale lines come back with active: false and will manufacture fake middles.
  • The /odds endpoint nests players["0"] as a dict (a single current price). The /historical-odds endpoint nests it as a list of snapshots — don’t mix them up.

Step 4: Scan for Middles

For every Over line and every higher Under line, if there’s an integer strictly between them, you have a middle. We keep the best price on each side across all books, then compute the break-even hit rate — how often the game must land in the window for the bet to break even:

def find_middles(ladder, min_price=1.6):
    best_over, best_under = {}, {}
    for book, lines in ladder.items():
        for line, (o, u) in lines.items():
            if o and (line not in best_over or o > best_over[line][0]):
                best_over[line] = (o, book)
            if u and (line not in best_under or u > best_under[line][0]):
                best_under[line] = (u, book)

    middles = []
    for l1, (op, ob) in best_over.items():
        for l2, (up, ub) in best_under.items():
            hits = [t for t in range(0, 40) if l1 < t < l2]
            if l2 > l1 and hits and op >= min_price and up >= min_price:
                # break-even hit rate, equal 1-unit stakes (conservative)
                worst_leg = min(op, up)
                be = (2 - worst_leg) / (op + up - worst_leg)
                middles.append((be, l1, op, ob, l2, up, ub, hits))
    return sorted(middles)

ladder = totals_ladder("id1300010963302275")  # Brewers vs Giants
for be, l1, op, ob, l2, up, ub, hits in find_middles(ladder)[:5]:
    print(f"Over {l1} @ {op} ({ob}) + Under {l2} @ {up} ({ub}) "
          f"-> win both on {hits} | break-even {be:.1%}")

Live output on that fixture:

Over 11.5 @ 1.667 (polymarket) + Under 12.5 @ 2.0 (polymarket) -> win both on [12] | break-even 16.7%
Over 11.5 @ 1.667 (polymarket) + Under 13 @ 1.75 (betparx)    -> win both on [12] | break-even 19.0%

The Math: When Is a Middle Worth It?

With equal one-unit stakes on each leg at decimal prices o (Over) and u (Under), there are three outcomes:

  • Middle hits (total in the gap): both win → profit = (o - 1) + (u - 1) = o + u - 2
  • Misses high (Over cashes, Under loses): profit = o - 2
  • Misses low (Under cashes, Over loses): profit = u - 2

Take the worst miss (w = min(o, u)) and the break-even hit probability is:

p* = (2 - w) / (o + u - w)

For Over 11.5 @ 1.667 + Under 12.5 @ 2.00, that’s (2 - 1.667) / (1.667 + 2.0 - 1.667) = 16.7%. In plain English: the game has to land on exactly 12 runs at least 16.7% of the time for this bet to break even.

Here’s the honest part. The scanner finds candidate middles; it can’t tell you whether they’re profitable. That depends on how often the total actually lands on the middle number(s) — and MLB games land on one specific run total far less than 16.7% of the time, so this particular candidate is likely negative EV. Most middles you find won’t clear their break-even threshold. The ones worth firing are wide windows (multiple integers in the gap) where both legs price close to even money. Use the break-even rate as a filter, and estimate the real landing frequency from a historical distribution — don’t assume the gap is free money.

Same Scanner, Any Line Market

Nothing above is baseball-specific. Swap the sport ID and the market name and the same code finds middles on:

Market Sport ID Middle on…
MLB run line (±1.5) 13 Handicap (incl. extra innings)
NBA spreads & totals 11 Handicap / Over Under
NFL spreads (the classic 3 & 7) 14 Handicap
Soccer Asian Handicaps & goal totals 10 Asian Handicap / Over Under Full Time

Football spreads are the most famous middling market — buying the 3 and the 7 (the two most common NFL margins) is a sharp staple. The market-ID lookup pattern is identical: query /markets?sportId=X, filter by marketName, and feed the resulting {marketId: handicap} map into totals_ladder(). For line shopping the best single price rather than middles, see our line shopping guide.

How Middles Open: Use Free Historical Data

Middle windows are created by line movement — one book moves its total on sharp action while a slower book lags. OddsPapi gives historical odds away on the free tier, so you can replay how a gap opened. The /historical-odds endpoint returns players["0"] as a list of snapshots (max 3 books per call):

hist = get("historical-odds", fixtureId="id1300010963302275",
           bookmakers="pinnacle,fanduel,draftkings")

book = hist["bookmakers"]["pinnacle"]
# Walk markets -> outcomes -> players["0"] (a LIST) for the price history.
# Each snapshot: {"createdAt": ..., "price": ..., "active": ...}

Studying when and how often books diverge is the real edge — and it’s the kind of analysis you’d pay for elsewhere. Pair it with our EV & closing-line-value guide to judge whether a middle leg also carries standalone value.

Reality Check Before You Fire

  • Middles are rare and fleeting. A real, near-even-money middle on a mainline number can vanish in minutes. Poll the feed; don’t refresh by hand.
  • Push risk on integer lines. A middle that hits on exactly one integer (like 12) pushes a leg if you’d used whole-number lines — favor half-point lines on both sides so the result is unambiguous.
  • Limits and voids. Soft books limit or void winners who only ever bet middles. Spread your action.
  • The break-even rate is a filter, not a guarantee. You still need to estimate how often the total lands in the window.

Get Your Free API Key

Stop refreshing eight sportsbook tabs. One OddsPapi call returns the full alt-line ladder across 350+ bookmakers, with free historical odds to study how middles open. Start scanning on the free tier — no credit card, no enterprise sales call. For more baseball-specific parsing, see our MLB Odds API guide.

Frequently Asked Questions

What is a middle bet?

A middle bet is two wagers on the same game at different lines — the Over at a lower number and the Under at a higher number — where both bets win if the final result lands in the gap between them. If it doesn’t, one leg wins and one loses, so you only forfeit the vig.

How is middling different from arbitrage?

Arbitrage locks in a small guaranteed profit no matter the result. Middling risks the vig for a large payout that only lands when the result falls inside the window. Both exploit bookmakers disagreeing, but arbs disagree on price and middles disagree on the line.

Are middle bets profitable?

Only when the game lands in the middle window more often than the break-even hit rate. The scanner finds candidates and computes that threshold; judging real profitability requires estimating the actual landing frequency from historical results. Most candidates you find won’t clear the bar.

Which markets have the most middles?

High-scoring totals with deep alt-line ladders (MLB run totals, NBA points) and key-number spreads (NFL 3 and 7). The more bookmakers you compare, the more line disagreement — and middles — you surface.

How many bookmakers do I need to find middles?

As many as possible. Middles come from line disagreement, so a 40-book feed surfaces very few. OddsPapi aggregates 350+ bookmakers including sharps and prediction markets, which disagree on lines far more often.