RapidOddsAPIRapidOddsAPI
Home/Blog/Guide

Build a positive EV scanner in Python

Find value bets by de-vigging a sharp book to get a fair price, then scanning every other bookmaker for prices that beat it.

Guide · Updated June 2026

A positive EV bet is one priced higher than its true probability, so it makes money on average over time. The simplest way to find them is to treat a sharp book like Pinnacle as fair value: de-vig its two prices to strip out the margin, which gives the true probability of each side, then scan every other bookmaker for a price that beats that fair price. This guide builds the scanner in Python on top of the RapidOddsAPI REST endpoint.

Every bookmaker price has a margin built in, which is how the book makes money. A positive expected value, or +EV, bet is one where a book's price is higher than the outcome's true chance of happening, usually because that book was slow to move. Bet those consistently and the maths is in your favour. The whole problem is knowing the true probability, and then spotting the books that are off it.

Using a sharp book as fair value

Sharp books such as Pinnacle run thin margins and move fast on professional money, so their prices track true probability closely. We treat Pinnacle as the baseline. There is one step before we can use it: a raw price still has the margin baked in, so we de-vig it first.

Other tools build a fair price by averaging the de-vigged prices of many books into a consensus, which is more robust. Using one sharp book is the simplest version of the same idea, and a good place to start.

De-vigging in one step

Each decimal price implies a probability of 1 / price. In a two way market the two implied probabilities add up to more than 100 percent, and the extra is the margin. To remove it, divide each implied probability by their total. The results sum to 100 percent and are the fair probabilities.

# Pinnacle: New Zealand 4.17, Egypt 1.253 implied_nz = 1 / 4.17 = 0.2398 implied_egy = 1 / 1.253 = 0.7981 total = 1.0379 # 3.8% margin fair_nz = 0.2398 / 1.0379 = 0.2311 # true probability fair_egy = 0.7981 / 1.0379 = 0.7689

So Pinnacle's fair price for New Zealand is 1 / 0.2311 = 4.33. Any book offering more than 4.33 on New Zealand is a positive EV bet.

Step 1: pull the odds

Ask the REST endpoint for the market across the sharp book and the books you want to scan. Here we use the World Cup draw no bet market, a clean two way market, and include Pinnacle as the baseline.

import requests API_KEY = "your_api_key" SPORT = "WORLD_CUP" MARKET = "draw_no_bet" # 2-way market (draw excluded) SHARP = "Pinnacle" # the book we treat as fair value BOOKMAKERS = [ "Pinnacle", "Bet365", "Sportsbet", "TAB", "Pointsbet", "Ladbrokes", "Unibet", "Dabble", "DraftKings", "BetMGM", ] resp = requests.get( f"https://api.rapidoddsapi.com/sports/{SPORT}/markets", params={ "api_key": API_KEY, "market_type": [MARKET], "bookmaker": BOOKMAKERS, }, timeout=30, ) resp.raise_for_status() games = resp.json()["games"]

Step 2: group each game and lay out the prices

Merge the per book entries for each game, then turn each game into a simple {book: {team: price}} lookup so we can read the sharp price and compare the rest. We match games by team name, see the game matching guide for the robust version.

def group_by_matchup(games): merged = {} for g in games: key = (g["game"]["home_team"], g["game"]["away_team"]) if key not in merged: merged[key] = {"game": g["game"], "bookmakers": []} merged[key]["bookmakers"].extend(g["bookmakers"]) return list(merged.values()) def prices_by_book(game): out = {} for book in game["bookmakers"]: for market in book["markets"]: if market["key"] != MARKET: continue for o in market["outcomes"]: out.setdefault(book["name"], {})[o["name"]] = o["price"] return out

Step 3: de-vig the sharp book

Turn the sharp book's two prices into fair probabilities, exactly as in the worked example above.

def fair_probs(sharp_prices): implied = {team: 1 / price for team, price in sharp_prices.items()} total = sum(implied.values()) return {team: p / total for team, p in implied.items()}

Step 4: find prices that beat fair value

For each other book and each team, the edge is how much the book's price beats the fair probability. The expected value per dollar staked is fair_prob × price - 1. If that is positive, the bet is +EV.

edge = (fair_prob * price - 1) * 100 # percent

A book offering 4.45 on a team whose fair probability is 0.2311 gives 0.2311 × 4.45 - 1 = 0.028, a 2.8 percent edge.

The full scanner

Put it together. Pull the odds, group each game, de-vig the sharp book, and list every bet that beats fair value by your minimum edge, best first.

import requests API_KEY = "your_api_key" SPORT = "WORLD_CUP" MARKET = "draw_no_bet" SHARP = "Pinnacle" BOOKMAKERS = [ "Pinnacle", "Bet365", "Sportsbet", "TAB", "Pointsbet", "Ladbrokes", "Unibet", "Dabble", "DraftKings", "BetMGM", ] MIN_EDGE = 1.0 # only show bets with at least this edge, in percent def group_by_matchup(games): merged = {} for g in games: key = (g["game"]["home_team"], g["game"]["away_team"]) if key not in merged: merged[key] = {"game": g["game"], "bookmakers": []} merged[key]["bookmakers"].extend(g["bookmakers"]) return list(merged.values()) def prices_by_book(game): out = {} for book in game["bookmakers"]: for market in book["markets"]: if market["key"] != MARKET: continue for o in market["outcomes"]: out.setdefault(book["name"], {})[o["name"]] = o["price"] return out def fair_probs(sharp_prices): implied = {team: 1 / price for team, price in sharp_prices.items()} total = sum(implied.values()) return {team: p / total for team, p in implied.items()} def main(): resp = requests.get( f"https://api.rapidoddsapi.com/sports/{SPORT}/markets", params={ "api_key": API_KEY, "market_type": [MARKET], "bookmaker": BOOKMAKERS, }, timeout=30, ) resp.raise_for_status() games = group_by_matchup(resp.json()["games"]) print(f"Scanning {len(games)} {SPORT} games against {SHARP}...\n") results = [] for game in games: books = prices_by_book(game) # Need the sharp book, with two prices, to set fair value. if SHARP not in books or len(books[SHARP]) != 2: continue fair = fair_probs(books[SHARP]) # Compare every other book's price to the fair price. for book, prices in books.items(): if book == SHARP: continue for team, price in prices.items(): if team not in fair: continue edge = (fair[team] * price - 1) * 100 if edge >= MIN_EDGE: fair_price = 1 / fair[team] results.append((edge, game["game"], team, price, book, fair_price)) results.sort(key=lambda r: r[0], reverse=True) print(f"{len(results)} positive EV bets (edge >= {MIN_EDGE:.0f}%)\n") for edge, g, team, price, book, fair_price in results: print(f"+{edge:.1f}% {team} {price} @ {book} " f"(fair {fair_price:.2f}) {g['away_team']} at {g['home_team']}") if __name__ == "__main__": main()

What you get back

Each line is a bet priced above fair value: the edge, the price and book to take it at, the fair price for reference, and the game.

Scanning 20 WORLD_CUP games against Pinnacle... 2 positive EV bets (edge >= 1%) +2.8% New Zealand 4.45 @ Sportsbet (fair 4.33) Egypt at New Zealand +1.3% Ivory Coast 4.5 @ Sportsbet (fair 4.44) Ivory Coast at Germany

The top line says Sportsbet has New Zealand at 4.45 while Pinnacle's de-vigged fair price is 4.33. You are getting paid as if New Zealand is less likely than the sharp market thinks, so the bet carries a 2.8 percent edge. Over many bets like it, that edge is your profit.

Things to know

  • The baseline is everything. Your bets are only as good as your fair price. Pinnacle is a solid sharp anchor, but a consensus of many de-vigged books is more robust, and is the natural next step.
  • Edge is per bet, not per result. A +EV bet can still lose. The edge plays out over a large number of bets, so consistency and bankroll matter more than any single result.
  • Small edges are noise. A 1 percent edge can be inside the error of your fair price. Many tools only act above a higher threshold, and weight by how sharp and how recent the baseline price is.
  • Speed matters. A soft book corrects once it notices, so the edge is temporary. Stream prices over the WebSocket feed to catch them as they appear.

Next steps

You have a working positive EV scanner. For the guaranteed profit plays on the same data, see the arbitrage scanner and the middles scanner. For the full range of tools you can build, see what you can build with an odds API, or read the API documentation.

Start building with RapidOddsAPI

Real-time, standardised odds from 100+ bookmakers over REST and WebSocket. Start free with 250 credits, no credit card required.

Get Your Free API KeyRead the Docs