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: