Expected value is the only number that matters in sports betting. Not your gut, not your win streak, not what the talking heads on ESPN think about tonight’s game. If a bet has positive expected value, you take it. If it doesn’t, you pass. Do that thousands of times, and the math takes care of the rest.
The challenge is finding +EV consistently and fast enough to act before the market corrects. Sharp books like Pinnacle price lines with surgical efficiency — but softer offshore books don’t always keep up. That gap between the sharp market’s true price and an offshore book’s stale line is where your bot lives.
This guide builds a +EV scanner in Python that uses Pinnacle’s no-vig line as the truth benchmark, compares it against offshore sportsbook odds from BetOnline, Bovada, and others, and flags every opportunity where the edge exceeds your threshold.
EV Math for Developers
Expected value is the weighted average of all possible outcomes of a bet. For a simple two-way market:
EV = (prob_win × net_payout) - (prob_lose × stake)
If a bet has positive EV, you profit over sufficient volume. If it’s negative, you lose. The formula is simple — the hard part is estimating prob_win accurately.
Converting Odds to Implied Probability
Every set of odds implies a probability. American odds, decimal odds, fractional — they all encode the same information. For decimal odds (what most APIs return):
implied_prob = 1 / decimal_odds
Decimal odds of 2.10 imply a 47.6% probability. Decimal odds of 1.87 imply 53.5%. These implied probabilities include the book’s vig, so they’ll sum to more than 100% across both sides of a market.
Deriving No-Vig Fair Odds
To get the “true” probability, you strip the vig from a sharp book’s line. Take Pinnacle’s two-way market:
Side A: 1.90 → implied = 52.63%
Side B: 1.98 → implied = 50.51%
Total: 103.14% (the 3.14% is vig)
Normalize to 100%:
Fair prob A = 52.63% / 103.14% = 51.03%
Fair prob B = 50.51% / 103.14% = 48.97%
These are your benchmark “true” probabilities. Any offshore book offering odds that imply a probability lower than 51.03% for Side A is offering you +EV against the sharpest benchmark in the market.
Worked Example
Pinnacle no-vig fair probability for the Lakers: 51.0% (fair decimal odds: 1.961).
BetOnline is offering Lakers at 2.05 (implied probability: 48.8%).
EV per $100 = (0.510 × $105) - (0.490 × $100)
= $53.55 - $49.00
= +$4.55
Edge = (0.510 - 0.488) / 0.488 = +4.5%
The BetOnline line is +EV by 4.5% against the Pinnacle benchmark. Your bot should flag this.
Sharp Benchmarks
Your +EV bot is only as good as its truth source. The “true probability” isn’t something you divine from a model — it’s the no-vig price from the sharpest books in the market.
Pinnacle: The Gold Standard
Pinnacle accepts unlimited sharp action, runs 2-3% vig on major markets, and closes with the most efficient lines in the industry. Their no-vig closing lines are the best publicly available approximation of true probability. For +EV scanning, Pinnacle is your primary benchmark.
Circa Sports
Circa is the sharpest US-based sportsbook. They take six- and seven-figure NFL bets and price lines independently. When Pinnacle and Circa agree on a number, the market consensus is extremely strong. When they diverge, there may be an information edge worth investigating.
Consensus Lines
For higher confidence, average the no-vig lines from multiple sharp books. If Pinnacle, Circa, and Bookmaker all produce similar fair odds, your benchmark is robust. If they disagree, use the sharpest (usually Pinnacle) or take the average weighted by each book’s historical closing line accuracy.
Where Offshore Books Create +EV
Offshore sportsbooks — BetOnline, Bovada, MyBookie, SportsBetting.ag — are softer than Pinnacle for structural reasons:
- Slower adjustments: Their line management systems react minutes behind Pinnacle, not seconds
- Different models: Some offshore books shade lines based on recreational customer exposure rather than sharp market signals
- Limited automation: Line updates may involve manual review, creating lag after sharp moves
- Intentional soft lines: Some books deliberately offer softer numbers on popular events to attract recreational volume
These inefficiencies are systematic and persistent. Your bot exploits them by continuously comparing offshore prices to the sharp benchmark.
Building the +EV Scanner
Here’s the core EVScanner class. It fetches odds from both sharp and soft books via The Odds API, derives no-vig fair probabilities from Pinnacle, and flags +EV opportunities above your configured threshold.
import requests
from dataclasses import dataclass
@dataclass
class EVOpportunity:
event: str
market: str
outcome: str
book: str
book_odds: float
fair_prob: float
book_implied: float
edge: float
kelly_fraction: float
class EVScanner:
SHARP_BOOK = "pinnacle"
def __init__(self, api_key: str, min_edge: float = 0.02, kelly_frac: float = 0.25):
self.api_key = api_key
self.min_edge = min_edge
self.kelly_frac = kelly_frac
def fetch_odds(self, sport: str) -> list[dict]:
resp = requests.get(
f"https://api.the-odds-api.com/v4/sports/{sport}/odds",
params={
"apiKey": self.api_key,
"regions": "us,us2,eu",
"markets": "h2h,spreads,totals",
"oddsFormat": "decimal",
},
)
resp.raise_for_status()
return resp.json()
@staticmethod
def no_vig_probs(odds_a: float, odds_b: float) -> tuple[float, float]:
imp_a, imp_b = 1.0 / odds_a, 1.0 / odds_b
total = imp_a + imp_b
return imp_a / total, imp_b / total
def _kelly(self, edge: float, decimal_odds: float) -> float:
b = decimal_odds - 1
p = (1.0 / decimal_odds) + edge
q = 1.0 - p
k = (b * p - q) / b
return max(k * self.kelly_frac, 0.0)
def scan(self, sport: str) -> list[EVOpportunity]:
events = self.fetch_odds(sport)
opportunities: list[EVOpportunity] = []
for event in events:
sharp_odds = self._extract_book_odds(event, self.SHARP_BOOK)
if not sharp_odds:
continue
fair = self._build_fair_map(sharp_odds)
for bookmaker in event.get("bookmakers", []):
if bookmaker["key"] == self.SHARP_BOOK:
continue
for market in bookmaker.get("markets", []):
for outcome in market.get("outcomes", []):
key = f"{market['key']}:{outcome['name']}"
if key not in fair:
continue
impl = 1.0 / outcome["price"]
edge = fair[key] - impl
if edge >= self.min_edge:
opportunities.append(EVOpportunity(
event=f"{event['home_team']} vs {event['away_team']}",
market=market["key"],
outcome=outcome["name"],
book=bookmaker["key"],
book_odds=outcome["price"],
fair_prob=round(fair[key], 4),
book_implied=round(impl, 4),
edge=round(edge, 4),
kelly_fraction=round(self._kelly(edge, outcome["price"]), 4),
))
return sorted(opportunities, key=lambda o: o.edge, reverse=True)
def _extract_book_odds(self, event: dict, book_key: str) -> dict | None:
for bm in event.get("bookmakers", []):
if bm["key"] == book_key:
return {m["key"]: m["outcomes"] for m in bm.get("markets", [])}
return None
def _build_fair_map(self, sharp_odds: dict) -> dict[str, float]:
fair: dict[str, float] = {}
for market_key, outcomes in sharp_odds.items():
if len(outcomes) == 2:
p_a, p_b = self.no_vig_probs(outcomes[0]["price"], outcomes[1]["price"])
fair[f"{market_key}:{outcomes[0]['name']}"] = p_a
fair[f"{market_key}:{outcomes[1]['name']}"] = p_b
return fair
Usage
scanner = EVScanner(api_key="your-odds-api-key", min_edge=0.02)
for opp in scanner.scan("basketball_nba"):
print(
f"{opp.event} | {opp.outcome} @ {opp.book} | "
f"odds: {opp.book_odds} | edge: {opp.edge:.1%} | "
f"kelly: {opp.kelly_fraction:.1%}"
)
Run this on a cron schedule — every 5 minutes during active markets — and pipe the output into your alerting or execution system.
Filtering and Prioritizing Opportunities
Raw +EV scanning produces noise. Not every opportunity flagged at 2%+ edge is worth acting on. Your bot needs filters.
Edge Thresholds
The 2% default is a starting point. Below 1%, transaction costs (vig at the target book, execution slippage, potential line movement between detection and placement) eat your edge. Above 5%, something is likely wrong — a data feed error, a suspended market, or a line that’s about to snap back. Investigate any opportunity above 5% before betting on it.
In practice, the sweet spot is 2-4% for automated bots operating at volume. If you’re placing fewer than 10 bets per day, you can afford to be pickier and target 3%+.
Market-Specific Considerations
Not all markets are created equal for +EV scanning:
- NFL spreads and totals: Deep liquidity, sharp lines settle fast. Opportunities are smaller but more reliable.
- NBA moneylines: Larger mispricings, especially for afternoon games before the sharp market fully engages.
- Soccer props and minor leagues: Wide vig at offshore books creates persistent +EV windows, but liquidity is thin and limits are lower.
- Live/in-play: Huge edge potential but requires sub-second execution — most bots can’t compete with market makers here.
Focus your scanner on the markets where you can actually execute. A 6% edge on a market with a $50 limit isn’t worth the engineering.
Staleness Filtering
If your Pinnacle benchmark data is 20 minutes old, any “edge” you detect might be an artifact of stale data, not a real mispricing. Timestamp every odds fetch and reject opportunities where the benchmark is older than your configured staleness window — 5 minutes is a reasonable default for pre-game markets.
Volume and Liquidity
Offshore books have betting limits. BetOnline might cap you at $500 on a prop market and $5,000 on NFL sides. Your bot should factor these limits into opportunity ranking. A 3% edge on a $5,000 max bet is worth more than a 5% edge on a $200 max.
From Detection to Execution
Finding +EV is the first half. Converting it into profit is the second.
Manual Execution Workflow
The simplest starting point: your scanner sends you an alert (Telegram, Discord, email) with the event, book, odds, and edge. You place the bet manually. This is slow but lets you validate the scanner’s accuracy before trusting it with automated execution.
Track every alert and every bet you take. After 200+ tracked opportunities, you’ll know whether your scanner’s flagged edges are real.
Kelly Criterion for Stake Sizing
Once you’ve confirmed the scanner finds real edge, size your bets using the Kelly criterion. The kelly_fraction field in each EVOpportunity already computes this — it tells you what percentage of your bankroll to wager based on the edge and odds.
Use fractional Kelly (25-50% of full Kelly) to manage variance. Full Kelly is mathematically optimal but assumes your edge estimate is perfect, which it never is.
Deep dive: Kelly Criterion Betting Bot
CLV Validation
The ultimate test of your +EV scanner: does it beat the closing line? If you consistently bet at odds better than the final market price, your scanner is identifying genuine mispricings. If your CLV is flat or negative despite flagging “edges,” your benchmark is miscalibrated or your execution is too slow.
Deep dive: Closing Line Value API
Bankroll Management
Even with perfect +EV identification, poor bankroll management will sink you:
- Never risk more than 3-5% of bankroll on a single bet, regardless of Kelly output
- Track drawdowns — a 20% drawdown with positive +EV is normal variance; a 40% drawdown means something is wrong
- Keep reserves — don’t deploy 100% of your bankroll across active bets; maintain liquidity for the best opportunities
What’s Next
Build out the rest of the sharp betting intelligence stack:
- Juice Comparison Across Offshore Sportsbooks — Find the lowest-vig lines for every market
- Kelly Criterion Betting Bot — Optimal position sizing for your +EV opportunities
- Closing Line Value API — Measure whether your +EV bets actually beat the market
- Sharp Betting Concepts — The full framework for automated edge detection
- Offshore Sportsbook APIs — Access the soft-line books where +EV opportunities are most frequent