A middle bet happens when two sportsbooks disagree on a spread or total by enough that a gap opens between them — a gap where both sides of your bet win. Book A has Patriots -3, Book B has Jets +4.5. If New England wins by exactly 4, you cash both tickets. If the score lands anywhere else, you lose the vig on one side and win the other, netting a small loss. That asymmetry — small loss most of the time, double win occasionally — is what makes middles positive EV.
Unlike arbitrage, middles don’t guarantee profit on every outcome. You’re paying a known cost (vig on both bets) for a probability-weighted shot at a much larger payout. The edge comes from the math: when the expected value of the double-win scenario exceeds the expected vig drag, you have a bet worth making. Automating the detection of these opportunities is straightforward once you understand the underlying math.
Middling Math
The core question is simple: does the probability of the final score landing in the middle zone, multiplied by the double-win payout, exceed the cost of the combined vig?
A Concrete Example
Book A offers Patriots -3 (-110). Book B offers Jets +4.5 (-110). You bet $110 to win $100 on each side.
Three outcome scenarios:
| Scenario | Pats Win By | Book A Bet | Book B Bet | Net |
|---|---|---|---|---|
| Middle hits | Exactly 4 | Win +$100 | Win +$100 | +$200 |
| Pats cover both | 5 or more | Win +$100 | Lose -$110 | -$10 |
| Jets cover both | Pats win by 0-2, or lose | Lose -$110 | Win +$100 | -$10 |
If the middle doesn’t hit, you lose $10 (the vig differential). If it hits, you win $200. The middle zone here is a single number — Patriots win by exactly 4. In NFL games, the probability of any specific margin occurring is roughly 3-6% depending on the number, with key numbers like 3 and 7 hitting at the high end of that range.
Expected Value Calculation
middle_probability = 0.05 # 5% chance the margin lands on exactly 4
win_both = 200 # net profit when middle hits
lose_vig = -10 # net loss when middle misses
EV = (middle_probability * win_both) + ((1 - middle_probability) * lose_vig)
EV = (0.05 * 200) + (0.95 * -10)
EV = 10.00 - 9.50
EV = +$0.50 per $220 risked
That’s a thin edge — about 0.23% ROI per middle — but it’s positive. And wider gaps push EV significantly higher. If Book A had Patriots -2.5 and Book B had Jets +4.5, the middle zone expands to margins of 3 and 4, roughly doubling the hit probability and the EV.
Why Wider Gaps Win
The relationship between gap width and EV is nonlinear. Every additional point in the gap captures another slice of the margin distribution, but NFL margins cluster around key numbers. A gap that spans 3 and 7 is worth substantially more than a gap that spans 5 and 6, because 3 and 7 occur far more frequently as final margins. Your bot should weight middle opportunities not just by gap width but by which numbers fall inside the gap.
Push Scenarios
When one side pushes (the final margin lands exactly on one of your numbers), you get a refund on that bet and win the other. Pushes add positive EV to the middle because they convert a potential loss into a half-win. If you bet Patriots -3 and Jets +4, and the Patriots win by exactly 3, you push at Book A and win at Book B — a $100 profit instead of a $10 loss. Your EV model should account for pushes on both endpoints.
Detection Algorithm
Middle detection boils down to pairwise comparison of lines across books for the same event and market. For every game, compare every book’s spread (or total) against every other book’s spread. Flag any pair where a gap exists.
Spread Middles
For point spreads, a middle exists when the favorite’s number at Book A is smaller than the underdog’s number at Book B. Formally:
Book A: Team X -3.0
Book B: Team X -4.5 (equivalently, Team Y +4.5)
Gap = abs(-3.0) - abs(-4.5) ... wait, think in terms of the underdog.
Book A: Team Y +3.0
Book B: Team Y +4.5
Middle exists because 3.0 ≠ 4.5 — there's a zone between 3 and 4.5
where betting Team X -3.0 at Book A AND Team Y +4.5 at Book B both win.
The general rule: a middle exists when the spread on the same side differs by more than 0 across two books. But you want to filter for gaps of at least 1 point to make the EV meaningful after vig.
Totals Middles
Same logic, applied to over/under lines. If Book A has Over 47.5 and Book B has Under 49.5, the middle zone is 48 and 49. Any final total landing in that range wins both bets.
Accounting for Odds
Two books might show the same spread number but different juice. Patriots -3 (-110) at Book A and Patriots -3 (-120) at Book B are the same line with different vig. That’s not a middle — but Patriots -3 (-110) at Book A and Jets +3.5 (+100) at Book B is a 0.5-point middle with favorable juice on the underdog side. Your scanner needs to compare the point number, not the odds price, to detect the gap — then use the odds to calculate EV.
Parameters
| Parameter | Description | Default |
|---|---|---|
min_gap | Minimum point gap to flag as a middle | 1.0 |
max_vig | Maximum combined vig (in implied probability) to consider | 0.10 |
sports | Which sports to scan | NFL, NBA |
min_ev | Minimum expected value to surface an opportunity | $0.50 per $220 |
Building the Middle Scanner
Here’s a MiddleScanner class that fetches odds from The Odds API, compares lines across books per event, detects middle opportunities, and ranks them by estimated EV.
from dataclasses import dataclass
from itertools import combinations
import requests
MARGIN_PROB = {
"americanfootball_nfl": {
1: 0.042, 2: 0.038, 3: 0.095, 4: 0.048, 5: 0.035,
6: 0.055, 7: 0.075, 8: 0.030, 9: 0.020, 10: 0.045,
},
"basketball_nba": {
i: 0.02 for i in range(1, 16)
},
}
@dataclass
class MiddleOpportunity:
event: str
market: str
book_a: str
book_b: str
side_a: str
side_b: str
line_a: float
line_b: float
odds_a: float
odds_b: float
gap: float
estimated_ev: float
class MiddleScanner:
def __init__(
self,
api_key: str,
sport: str = "americanfootball_nfl",
min_gap: float = 1.0,
max_vig: float = 0.10,
):
self.api_key = api_key
self.sport = sport
self.min_gap = min_gap
self.max_vig = max_vig
def fetch_odds(self, market: str = "spreads") -> list[dict]:
resp = requests.get(
f"https://api.the-odds-api.com/v4/sports/{self.sport}/odds",
params={
"apiKey": self.api_key,
"regions": "us,us2",
"markets": market,
"oddsFormat": "decimal",
},
)
resp.raise_for_status()
return resp.json()
def _extract_lines(self, event: dict, market: str) -> dict[str, list[dict]]:
"""Extract {book: [{side, line, odds}]} for an event."""
books = {}
for bm in event.get("bookmakers", []):
for mkt in bm.get("markets", []):
if mkt["key"] != market:
continue
entries = []
for outcome in mkt.get("outcomes", []):
entries.append({
"side": outcome["name"],
"line": outcome.get("point", 0),
"odds": outcome["price"],
})
if entries:
books[bm["key"]] = entries
return books
def _middle_probability(self, gap: float) -> float:
"""Estimate probability of the score landing in the middle zone."""
probs = MARGIN_PROB.get(self.sport, {})
total = 0.0
for margin in range(1, int(gap) + 1):
total += probs.get(margin, 0.02)
if gap % 1 != 0:
total += probs.get(int(gap) + 1, 0.02) * (gap % 1)
return total
def _calculate_ev(
self, gap: float, odds_a: float, odds_b: float, stake: float = 110.0
) -> float:
prob = self._middle_probability(gap)
win_a = stake * (odds_a - 1)
win_b = stake * (odds_b - 1)
win_both = win_a + win_b
lose_vig = abs(win_a - stake) if odds_a < 2.0 else abs(win_b - stake)
lose_vig = stake - min(win_a, win_b)
ev = (prob * win_both) + ((1 - prob) * -lose_vig)
return round(ev, 2)
def scan(self, market: str = "spreads") -> list[MiddleOpportunity]:
events = self.fetch_odds(market)
opportunities = []
for event in events:
title = f"{event.get('away_team')} @ {event.get('home_team')}"
books = self._extract_lines(event, market)
book_names = list(books.keys())
for b1, b2 in combinations(book_names, 2):
for side_a in books[b1]:
for side_b in books[b2]:
if side_a["side"] == side_b["side"]:
continue
gap = abs(side_a["line"]) - abs(side_b["line"])
if gap < 0:
gap = abs(side_b["line"]) - abs(side_a["line"])
effective_gap = abs(abs(side_a["line"]) - abs(side_b["line"]))
if effective_gap < self.min_gap:
continue
imp_a = 1 / side_a["odds"]
imp_b = 1 / side_b["odds"]
combined_vig = (imp_a + imp_b) - 1
if combined_vig > self.max_vig:
continue
ev = self._calculate_ev(
effective_gap, side_a["odds"], side_b["odds"]
)
opportunities.append(MiddleOpportunity(
event=title,
market=market,
book_a=b1,
book_b=b2,
side_a=side_a["side"],
side_b=side_b["side"],
line_a=side_a["line"],
line_b=side_b["line"],
odds_a=side_a["odds"],
odds_b=side_b["odds"],
gap=effective_gap,
estimated_ev=ev,
))
opportunities.sort(key=lambda o: o.estimated_ev, reverse=True)
return opportunities
Usage
scanner = MiddleScanner(api_key="your-odds-api-key", sport="americanfootball_nfl")
middles = scanner.scan("spreads")
for m in middles[:10]:
print(
f"{m.event}: {m.side_a} {m.line_a} ({m.book_a}) vs "
f"{m.side_b} {m.line_b} ({m.book_b}) | "
f"Gap: {m.gap} | EV: ${m.estimated_ev}"
)
Run this on a loop — every 60 seconds is sufficient for middles since they persist longer than arb opportunities. Lines need to fully converge before a middle disappears, which typically takes 10-30 minutes.
Which Books Create the Most Middles?
Not all sportsbooks use the same odds model. Books that set their own lines based on internal models, rather than copying Pinnacle or a consensus feed, create the most cross-book discrepancies — and therefore the most middles.
BetOnline vs. Bovada
These two are the most common middle sources among offshore books. BetOnline uses a proprietary odds model that frequently diverges from the sharp consensus by 0.5-1.5 points on NFL spreads. Bovada’s line management is slower to react to market movement, which means they hold stale numbers longer. The combination of BetOnline’s unique lines and Bovada’s slow adjustments produces a steady stream of middle windows.
Books with Unique Odds Models
MyBookie, BetUS, and Heritage all post lines that occasionally diverge from the pack. The divergence is usually small — 0.5 to 1 point — but that’s all you need for a middling opportunity. Monitor at least 4-6 books to maximize your hit rate. Each additional book you add increases the number of pairwise comparisons and, with it, the number of potential middles.
Sport-Specific Patterns
NFL is the richest environment for middles. Key number clustering (3, 7, 6, 10) means that even small line discrepancies can create high-EV middles because those specific margins occur frequently. A 1-point middle that includes 3 or 7 is worth substantially more than the same gap at 5 or 8.
NBA produces more middles by volume (more games, more totals movement) but each individual middle has lower EV because NBA margins are more uniformly distributed. The middle probability per gap point is lower than NFL.
Live betting generates the highest frequency of middles because books adjust at different speeds in real time. A fast book might move a live spread by 2 points while a slower book is still showing the old number. The window is tight — often under 60 seconds — but if your bot can detect and execute in that window, live middles offer the best EV-per-hour of any middling strategy.
From Detection to Execution
Finding middles is the easy part. Converting detection into profit requires operational discipline.
Speed Requirements
Middles last longer than arbs. An arb disappears the instant one book adjusts its odds. A middle requires both books to converge on the same number before it closes, which typically takes 10-30 minutes for pre-game markets. You have more time to act — but don’t confuse “more time” with “unlimited time.” The best middles (widest gaps, highest EV) get snapped up first as other sharp bettors and bots detect them.
For pre-game markets, a 60-second scan interval with 30-second execution is more than fast enough. For live markets, you need sub-10-second detection and near-instant execution.
Stake Sizing
Middling stake sizing is different from arb staking. With arbs, you size each leg to guarantee equal profit on every outcome. With middles, you want equal risk — bet the same amount on each side so your vig loss is symmetric. The standard approach is flat-staking both sides at the same dollar amount.
For a more sophisticated approach, weight your stake by the EV of the middle. Higher-EV middles (wider gaps, key numbers in the zone) get larger stakes. Lower-EV middles get minimum stakes. A Kelly-adjacent sizing model works well here, but most middlers stick with flat sizing for simplicity.
Tracking Your Middle Hit Rate
Log every middle you bet, the gap width, the sport, and whether the middle hit. Over 500+ middles, your actual hit rate should converge toward the theoretical probability. If it doesn’t, your probability model is miscalibrated — either your margin distributions are off, or you’re including middles with inflated gaps (line errors, stale data).
Track your P&L separately for middles versus your other betting strategies. Middling should show a characteristic pattern: long stretches of small losses (the vig drag) punctuated by occasional large wins (the double-cash). If you’re seeing large losses, you’re overpaying vig or betting middles with gaps too narrow to be +EV.
Bankroll Implications
Middling is a high-volume, low-edge strategy. You need a bankroll large enough to absorb the vig drag during dry spells between middle hits. A rough guideline: your middle betting bankroll should be at least 50x your average per-middle stake. This gives you enough runway to survive the variance and let the positive EV compound.
What’s Next
Middling is one tool in the cross-book exploitation toolkit. Combine it with these related strategies for a broader edge:
- +EV Betting Bots — Use no-vig benchmarks to find positive expected value beyond middles
- Reverse Line Movement — Detect when line movement contradicts public betting percentages
- Juice Comparison — Find the lowest-vig books for each leg of your middle
- Sharp Betting Concepts — The full framework for automated edge detection across offshore sportsbooks
- Offshore Sportsbook APIs — Access the multi-book data that feeds your middle scanner