Different sportsbooks offer different odds on the same player props. A line of -110 at one book might be +100 at another. Finding these discrepancies is called "line shopping" - and it can significantly improve your expected value on prop bets.

This tutorial shows you how to build a Python tool that compares NBA player prop odds across multiple sportsbooks using the BALLDONTLIE API. The same approach works for NFL, NHL, and EPL player props.

If you haven't set up your API key yet, see our Getting Started guide first.

What We're Building

A command-line tool that:

  • Fetches live player prop odds from multiple sportsbooks
  • Groups props by player, prop type, and line value
  • Identifies the best available odds for each side (over/under)
  • Displays results in a formatted comparison table

Prerequisites

  • Python 3.8+
  • A BALLDONTLIE API key (sign up here)
  • Basic familiarity with Python and REST APIs

Understanding Player Props Data

Before we start coding, let's understand the data structure. The player props endpoint returns odds from multiple vendors for each prop:

{
  "id": 1256660847,
  "game_id": 18447232,
  "player_id": 322,
  "vendor": "draftkings",
  "prop_type": "points",
  "line_value": "29.5",
  "market": {
    "type": "over_under",
    "over_odds": -115,
    "under_odds": -105
  },
  "updated_at": "2025-12-24T19:30:15.903Z"
}

Key fields:

  • vendor: The sportsbook (draftkings, fanduel, caesars, betmgm, betrivers, etc.)
  • prop_type: The stat category (points, rebounds, assists, threes, steals, blocks)
  • line_value: The line for the prop (e.g., 29.5 points)
  • market.type: Either "over_under" or "milestone"
  • market.over_odds / under_odds: American odds for each side

Step 1: Project Setup

Create a new directory and install dependencies:

mkdir nba-player-props-comparison
cd nba-player-props-comparison
pip install requests tabulate

Step 2: Fetch Games and Player Props

First, let's write functions to fetch NBA games and their player props:

import requests
from collections import defaultdict

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


def get_headers():
    """Return headers with API key for authentication."""
    return {"Authorization": API_KEY}


def fetch_games(date: str) -> list:
    """Fetch NBA games for a specific date."""
    response = requests.get(
        f"{BASE_URL}/nba/v1/games",
        headers=get_headers(),
        params={"dates[]": date}
    )
    response.raise_for_status()
    return response.json()["data"]


def fetch_player_props(game_id: int, prop_type: str = None) -> list:
    """Fetch player props for a specific game."""
    params = {"game_id": game_id}
    if prop_type:
        params["prop_type"] = prop_type

    response = requests.get(
        f"{BASE_URL}/nba/v2/odds/player_props",
        headers=get_headers(),
        params=params
    )
    response.raise_for_status()
    return response.json()["data"]

The fetch_player_props function returns all props for a game. You can filter by prop_type to focus on specific stats like points, rebounds, or assists.

Step 3: Group Props for Comparison

The API returns one entry per vendor per prop. To compare across sportsbooks, we need to group them:

def group_props_by_player_and_line(props: list) -> dict:
    """
    Group props by player_id, prop_type, and line_value.
    Returns: {(player_id, prop_type, line_value): [prop1, prop2, ...]}
    """
    grouped = defaultdict(list)
    for prop in props:
        # Only include over_under markets for comparison
        if prop["market"]["type"] != "over_under":
            continue
        key = (prop["player_id"], prop["prop_type"], prop["line_value"])
        grouped[key].append(prop)
    return grouped

This creates a dictionary where each key is a unique (player, prop type, line) combination, and the value is a list of all vendor entries for that prop.

Step 4: Find the Best Odds

Now we can compare odds across vendors and find the best available:

def find_best_odds(props: list) -> dict:
    """
    Find the best over and under odds across all vendors for a prop.
    Returns dict with best_over and best_under info.
    """
    best_over = {"odds": float("-inf"), "vendor": None}
    best_under = {"odds": float("-inf"), "vendor": None}

    for prop in props:
        market = prop["market"]
        over_odds = market.get("over_odds")
        under_odds = market.get("under_odds")

        if over_odds is not None and over_odds > best_over["odds"]:
            best_over = {"odds": over_odds, "vendor": prop["vendor"]}

        if under_odds is not None and under_odds > best_under["odds"]:
            best_under = {"odds": under_odds, "vendor": prop["vendor"]}

    return {
        "best_over": best_over if best_over["vendor"] else None,
        "best_under": best_under if best_under["vendor"] else None,
        "all_vendors": [p["vendor"] for p in props]
    }

Higher American odds are always better for the bettor. This function finds which sportsbook offers the best odds for each side of the prop.

Step 5: Display the Comparison

Let's fetch player names and format everything into a readable table:

from tabulate import tabulate


def fetch_players(player_ids: list) -> dict:
    """Fetch player details for a list of player IDs."""
    response = requests.get(
        f"{BASE_URL}/nba/v1/players",
        headers=get_headers(),
        params={"player_ids[]": player_ids}
    )
    response.raise_for_status()
    players = response.json()["data"]
    return {p["id"]: p for p in players}


def format_odds(odds: int) -> str:
    """Format American odds with +/- prefix."""
    if odds >= 0:
        return f"+{odds}"
    return str(odds)


def compare_player_props(game_id: int, prop_type: str = "points"):
    """
    Compare player props across vendors for a game.
    Prints a table showing the best odds for each player/line combination.
    """
    print(f"\nFetching {prop_type} props for game {game_id}...")

    # Fetch props
    props = fetch_player_props(game_id, prop_type)
    if not props:
        print("No props found for this game.")
        return

    # Get unique player IDs and fetch player details
    player_ids = list(set(p["player_id"] for p in props))
    players = fetch_players(player_ids)

    # Group props by player and line
    grouped = group_props_by_player_and_line(props)

    # Build comparison data
    rows = []
    for (player_id, ptype, line_value), prop_list in grouped.items():
        player = players.get(player_id, {})
        player_name = f"{player.get('first_name', '?')} {player.get('last_name', '?')}"

        best = find_best_odds(prop_list)
        if not best["best_over"] or not best["best_under"]:
            continue

        rows.append({
            "player": player_name,
            "line": float(line_value),
            "best_over": format_odds(best["best_over"]["odds"]),
            "over_vendor": best["best_over"]["vendor"].capitalize(),
            "best_under": format_odds(best["best_under"]["odds"]),
            "under_vendor": best["best_under"]["vendor"].capitalize(),
            "num_vendors": len(set(best["all_vendors"]))
        })

    # Sort by player name and line
    rows.sort(key=lambda x: (x["player"], x["line"]))

    # Print table
    if rows:
        headers = ["Player", "Line", "Best Over", "Vendor", "Best Under", "Vendor", "# Books"]
        table_data = [
            [r["player"], r["line"], r["best_over"], r["over_vendor"],
             r["best_under"], r["under_vendor"], r["num_vendors"]]
            for r in rows
        ]
        print(f"\n{prop_type.upper()} PROPS - Best Available Odds")
        print("=" * 80)
        print(tabulate(table_data, headers=headers, tablefmt="simple"))
        print(f"\nTotal: {len(rows)} player lines compared across multiple sportsbooks")
    else:
        print("No comparable props found.")

Step 6: Run the Comparison

Here's the main function that ties everything together:

from datetime import date


def main():
    """Main entry point."""
    today = date.today().isoformat()
    print(f"Fetching NBA games for {today}...")

    games = fetch_games(today)
    if not games:
        print("No games scheduled for today.")
        return

    print(f"Found {len(games)} games:\n")
    for i, game in enumerate(games):
        home = game["home_team"]["abbreviation"]
        away = game["visitor_team"]["abbreviation"]
        print(f"  {i + 1}. {away} @ {home} (Game ID: {game['id']})")

    # Use first game for demo
    game_id = games[0]["id"]
    print(f"\nComparing player props for game {game_id}...")

    # Compare different prop types
    for prop_type in ["points", "rebounds", "assists", "threes"]:
        compare_player_props(game_id, prop_type)
        print()


if __name__ == "__main__":
    main()

Example Output:

Fetching NBA games for 2025-12-25...
Found 5 games:

  1. CLE @ NYK (Game ID: 18447232)
  2. SAS @ OKC (Game ID: 18447233)
  3. DAL @ GSW (Game ID: 18447234)
  4. HOU @ LAL (Game ID: 18447235)
  5. MIN @ DEN (Game ID: 18447236)

Comparing player props for game 18447232...

POINTS PROPS - Best Available Odds
================================================================================
Player            Line    Best Over    Vendor       Best Under    Vendor       # Books
----------------  ------  -----------  -----------  ------------  -----------  ---------
Donovan Mitchell  28.5    -105         Fanduel      -105          Draftkings   6
Donovan Mitchell  29.5    +108         Betrivers    -118          Betrivers    5
Evan Mobley       15.5    -127         Betrivers    -105          Betrivers    6
Jalen Brunson    29.5    -113         Betrivers    -118          Betrivers    6
Jalen Brunson    30.5    +107         Betrivers    -143          Betrivers    5

Total: 24 player lines compared across multiple sportsbooks

Available Prop Types

The API supports many prop types for NBA games:

  • Individual stats: points, rebounds, assists, threes, steals, blocks
  • Combo stats: points_assists, points_rebounds, points_rebounds_assists, rebounds_assists
  • Achievements: double_double, triple_double
  • Quarter/time props: points_1q, assists_1q, rebounds_1q, points_first3min, assists_first3min, rebounds_first3min

Available Sportsbooks

The API includes odds from major sportsbooks:

  • DraftKings
  • FanDuel
  • Caesars
  • BetMGM
  • BetRivers
  • Bet365
  • BallyBet
  • BetParx
  • Betway
  • Rebet

Extending This Further

Here are some ideas to enhance this tool:

  • Alert system: Notify when odds differences exceed a threshold (e.g., 15+ cents difference)
  • Web dashboard: Build a React frontend to visualize props in real-time
  • Historical tracking: Store snapshots to analyze line movements over time
  • Multi-game comparison: Compare props across all games on a slate
  • EV calculator: Add expected value calculations based on your own projections

Check out our AI-assisted development guide to quickly generate these extensions using ChatGPT or Claude.

Important Notes

The player props data is live and updated in real-time. The BALLDONTLIE API does not store historical betting data. This means the tool is ideal for:

  • Live line shopping before placing bets
  • Real-time monitoring of odds movements
  • Building alerts for favorable lines

It is not suitable for historical backtesting of betting strategies.

Other Sports

The same player props endpoints are available for:

  • NFL: Passing yards, rushing yards, receiving yards, touchdowns, and more
  • NHL: Goals, assists, points, shots on goal, saves
  • EPL: Goals, assists, shots, tackles, saves

The API also provides game odds, team stats, player stats, standings, and more across NBA, NFL, MLB, NHL, EPL, WNBA, NCAAF, and NCAAB.

Next Steps

Need help? Join our Discord community or browse the full documentation.

Ready to start building? Sign up for a free API key and start comparing player props in minutes.