Different sportsbooks offer different odds on the same games. Finding the best line is known as "line shopping" - and it's one of the easiest ways to improve your expected value without any handicapping skill.

This tutorial shows you how to use the BALLDONTLIE API to compare odds across multiple sportsbooks programmatically.

This tutorial assumes you have an API key. If you haven't set that up yet, see our Getting Started guide.

What is Line Shopping?

Sportsbooks set their own lines, and they don't always agree. On a typical NBA game, you might see:

  • FanDuel: Lakers -3.5 (-110)
  • DraftKings: Lakers -3 (-115)
  • Caesars: Lakers -3.5 (-105)

Betting the Lakers -3.5 at Caesars (-105) is better than the same line at FanDuel (-110). Over hundreds of bets, these small differences compound into significant edge.

Fetching Odds from Multiple Books

Let's start by fetching current odds for today's NBA games:

import requests
from datetime import date

API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"

headers = {"Authorization": API_KEY}
today = date.today().isoformat()

# Fetch odds for today's games
response = requests.get(
    f"{BASE_URL}/nba/v2/odds",
    headers=headers,
    params={"dates[]": today}
)

odds_data = response.json()["data"]
print(f"Found {len(odds_data)} odds entries for {today}")

The API returns odds from multiple sportsbooks including:

  • FanDuel
  • DraftKings
  • Caesars
  • BetMGM
  • Bet365
  • BetRivers
  • And more

Each odds entry includes spreads, moneylines, and totals.

Comparing Moneylines

Here's a script that finds the best available moneyline for each side of a game:

import requests
from datetime import date
from collections import defaultdict

API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"

headers = {"Authorization": API_KEY}
today = date.today().isoformat()

# Get games
games_resp = requests.get(
    f"{BASE_URL}/nba/v1/games",
    headers=headers,
    params={"dates[]": today}
)
games = {g["id"]: g for g in games_resp.json()["data"]}

# Get odds
odds_resp = requests.get(
    f"{BASE_URL}/nba/v2/odds",
    headers=headers,
    params={"dates[]": today}
)

# Group odds by game
game_odds = defaultdict(list)
for odd in odds_resp.json()["data"]:
    game_odds[odd["game_id"]].append(odd)

def find_best_moneyline(odds_list, side):
    """Find the best (highest) moneyline odds for a given side."""
    key = "moneyline_home_odds" if side == "home" else "moneyline_away_odds"
    valid_odds = [o for o in odds_list if o[key] is not None]
    if not valid_odds:
        return None, None
    best = max(valid_odds, key=lambda x: x[key])
    return best["vendor"], best[key]

# Display best odds for each game
for game_id, odds_list in game_odds.items():
    if game_id not in games:
        continue

    game = games[game_id]
    home = game["home_team"]["abbreviation"]
    away = game["visitor_team"]["abbreviation"]

    print(f"\n{away} @ {home}")
    print("-" * 40)

    home_vendor, home_odds = find_best_moneyline(odds_list, "home")
    away_vendor, away_odds = find_best_moneyline(odds_list, "away")

    if home_odds:
        print(f"{home}: {home_odds:+d} at {home_vendor.capitalize()}")
    if away_odds:
        print(f"{away}: {away_odds:+d} at {away_vendor.capitalize()}")

Output:

ORL @ NYK
----------------------------------------
NYK: -3335 at Ballybet
ORL: +10000 at Fanduel

BOS @ TOR
----------------------------------------
TOR: +650 at Betparx
BOS: -540 at Draftkings

DEN @ CHA
----------------------------------------
CHA: +390 at Draftkings
DEN: -480 at Betrivers

Converting Odds to Implied Probability

American odds can be converted to implied probability using this formula:

  • Positive odds: probability = 100 / (odds + 100)
  • Negative odds: probability = |odds| / (|odds| + 100)
def american_to_implied_prob(odds):
    """Convert American odds to implied probability."""
    if odds > 0:
        return 100 / (odds + 100)
    else:
        return abs(odds) / (abs(odds) + 100)

# Example
print(f"-150 implies: {american_to_implied_prob(-150):.1%}")  # 60.0%
print(f"+200 implies: {american_to_implied_prob(200):.1%}")   # 33.3%

Building a Line Comparison Tool

Here's a complete script that analyzes all games and highlights the best odds:

import requests
from datetime import date
from collections import defaultdict

API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"

headers = {"Authorization": API_KEY}
today = date.today().isoformat()

# Fetch games and odds
games_resp = requests.get(
    f"{BASE_URL}/nba/v1/games",
    headers=headers,
    params={"dates[]": today}
)
games = {g["id"]: g for g in games_resp.json()["data"]}

odds_resp = requests.get(
    f"{BASE_URL}/nba/v2/odds",
    headers=headers,
    params={"dates[]": today}
)

game_odds = defaultdict(list)
for odd in odds_resp.json()["data"]:
    game_odds[odd["game_id"]].append(odd)

def american_to_implied_prob(odds):
    if odds > 0:
        return 100 / (odds + 100)
    else:
        return abs(odds) / (abs(odds) + 100)

def find_best_odds(odds_list, key):
    valid = [o for o in odds_list if o[key] is not None]
    if not valid:
        return None, None
    best = max(valid, key=lambda x: x[key])
    return best["vendor"], best[key]

print("BEST AVAILABLE MONEYLINE ODDS")
print("=" * 60)

for game_id, odds_list in game_odds.items():
    if game_id not in games:
        continue

    game = games[game_id]
    home = game["home_team"]["abbreviation"]
    away = game["visitor_team"]["abbreviation"]

    home_vendor, home_odds = find_best_odds(odds_list, "moneyline_home_odds")
    away_vendor, away_odds = find_best_odds(odds_list, "moneyline_away_odds")

    if not home_odds or not away_odds:
        continue

    home_prob = american_to_implied_prob(home_odds)
    away_prob = american_to_implied_prob(away_odds)
    combined = home_prob + away_prob

    print(f"\n{away} @ {home}")
    print("-" * 40)
    print(f"{home}: {home_odds:+d} at {home_vendor.capitalize()} ({home_prob:.1%})")
    print(f"{away}: {away_odds:+d} at {away_vendor.capitalize()} ({away_prob:.1%})")
    print(f"Combined implied probability: {combined:.1%}")

    if combined < 1.0:
        edge = (1 - combined) * 100
        print(f">>> Potential value: {edge:.1f}% theoretical edge")

Output:

BEST AVAILABLE MONEYLINE ODDS
============================================================

ORL @ NYK
----------------------------------------
NYK: -3335 at Ballybet (97.1%)
ORL: +10000 at Fanduel (1.0%)
Combined implied probability: 98.1%
>>> Potential value: 1.9% theoretical edge

BOS @ TOR
----------------------------------------
TOR: +650 at Betparx (13.3%)
BOS: -540 at Draftkings (84.4%)
Combined implied probability: 97.7%
>>> Potential value: 2.3% theoretical edge

DEN @ CHA
----------------------------------------
CHA: +390 at Draftkings (20.4%)
DEN: -480 at Betrivers (82.8%)
Combined implied probability: 103.2%

When the combined implied probability is less than 100%, it means the best odds across books are favorable. This doesn't guarantee profit (that would require arbitrage across both sides), but it indicates the market is offering value.

Important Limitations

The BALLDONTLIE API provides current odds only. We do not store historical betting odds or player props.

This means our data is ideal for:

  • Live line shopping: Finding the best current odds before placing a bet
  • Real-time monitoring: Building alerts when lines move
  • Market analysis: Understanding current market pricing

This data is not suitable for:

  • Historical backtesting of betting strategies
  • Analyzing past line movements
  • Training ML models on historical odds

Extending This Further

Want to customize this script? Use our AI-assisted workflow to quickly generate variations. You could:

  • Add spread comparison alongside moneylines
  • Filter for games where the line difference exceeds a threshold
  • Build a Discord bot that alerts you to favorable lines
  • Create a web dashboard showing best odds in real-time

The BALLDONTLIE API gives you the raw data - what you build with it is up to you.