Guide / Python 3 / requests

Build an odds scanner in Python (about 50 lines)

Build a betting odds scanner that reads NFL prices, finds the best price for each side at each line, and prints a table. One request includes the requested markets across books.

What you will build

This is the script's output from a saved sample response. Moneyline prices use the Eagles and Bears example on our homepage. Spread and total prices are test data chosen to exercise the arbitrage check.

Fixture                                Market         Side / line                     Price  Book
Philadelphia Eagles @ Chicago Bears    h2h            Philadelphia Eagles              -182  Novig
Philadelphia Eagles @ Chicago Bears    h2h            Chicago Bears                    +176  Novig
Philadelphia Eagles @ Chicago Bears    spreads        Philadelphia Eagles -3.5         +110  FanDuel
Philadelphia Eagles @ Chicago Bears    spreads        Chicago Bears +3.5               +110  DraftKings
Philadelphia Eagles @ Chicago Bears    spreads        Philadelphia Eagles -4.5         +150  FanDuel
Philadelphia Eagles @ Chicago Bears    spreads        Chicago Bears +4.5               -170  FanDuel
Philadelphia Eagles @ Chicago Bears    totals         Over +44.5                       +105  FanDuel
Philadelphia Eagles @ Chicago Bears    totals         Under +44.5                      +105  DraftKings
Philadelphia Eagles @ Chicago Bears    totals         Over +45.5                       +140  FanDuel
Philadelphia Eagles @ Chicago Bears    totals         Under +45.5                      -160  FanDuel
Philadelphia Eagles @ Chicago Bears    totals/1h      Over +21.5                       -110  FanDuel
Philadelphia Eagles @ Chicago Bears    totals/1h      Under +21.5                      -110  FanDuel
ARB flag: Philadelphia Eagles @ Chicago Bears | spreads (line -3.5) | 95.24% combined
ARB flag: Philadelphia Eagles @ Chicago Bears | totals (line +44.5) | 97.56% combined

The table keeps alternate lines and periods separate. ARB flags are arithmetic checks on the sample prices.

Get a free key

Create an account and get an API key. The Free plan includes 1,000 requests a month and all 46 books. One run of this script uses one request.

Save the script below as odds_scanner.py. Install requests, set your API key in the environment, then run it.

python3 -m pip install requests
export ODDSLINK_API_KEY="your-api-key"
python3 odds_scanner.py

On PowerShell, set the key with $env:ODDSLINK_API_KEY="your-api-key" and use python odds_scanner.py.

The full Python odds scanner

The documented endpoint is https://oddslink.app/v1/sports/nfl/odds. Authentication uses the X-API-Key header. The request selects h2h,spreads,totals and oddsFormat=american.

import os
import requests


def implied(price):
    return 100 / (price + 100) if price > 0 else -price / (-price + 100)


def american(probability):
    return round(100 / probability - 100 if probability <= 0.5
                 else -100 * probability / (1 - probability))


def scan(fixtures):
    print(f"{'Fixture':<38} {'Market':<14} {'Side / line':<30} {'Price':>6}  Book")
    for fixture in fixtures:
        label = f"{fixture['away_team']} @ {fixture['home_team']}"
        best, groups, three_way = {}, {}, set()
        for book in fixture.get('bookmakers', []):
            for market in book.get('markets', []):
                kind, period = market['key'], market.get('period', '')
                if kind == 'h2h' and any(s['name'] == 'Draw' for s in market['outcomes']):
                    three_way.add(period)
                if kind not in ('h2h', 'spreads', 'totals'):
                    continue
                for side in market['outcomes']:
                    price, point = side.get('price'), side.get('point')
                    if price is None or abs(price) < 100:
                        continue
                    if kind != 'h2h' and point is None:
                        continue
                    key = (kind, period, side['name'], point)
                    if key not in best or implied(price) < implied(best[key][0]):
                        best[key] = (price, book['title'])
        for (kind, period, name, point), (price, book) in best.items():
            side = name if point is None else f"{name} {point:+g}"
            market = kind + (f'/{period}' if period else '')
            print(f"{label:<38} {market:<14} {side:<30} {price:+6d}  {book}")
            line = -point if kind == 'spreads' and name == fixture['home_team'] else point
            groups.setdefault((kind, period, line), {})[name] = implied(price)
        for (kind, period, line), sides in groups.items():
            expected = {'Over', 'Under'} if kind == 'totals' else {
                fixture['away_team'], fixture['home_team']}
            if set(sides) != expected or (kind == 'h2h' and period in three_way):
                continue
            combined = sum(sides.values())
            if combined < 1:
                market = kind + (f'/{period}' if period else '')
                suffix = '' if line is None else f' (line {line:+g})'
                print(f"ARB flag: {label} | {market}{suffix} | {combined:.2%} combined")


def main():
    base = os.getenv('ODDSLINK_BASE_URL', 'https://oddslink.app').rstrip('/')
    response = requests.get(
        f'{base}/v1/sports/nfl/odds',
        params={'markets': 'h2h,spreads,totals', 'oddsFormat': 'american'},
        headers={'X-API-Key': os.environ['ODDSLINK_API_KEY']}, timeout=30,
    )
    response.raise_for_status()
    scan(response.json())


if __name__ == '__main__':
    main()

ODDSLINK_BASE_URL is an override for local mock testing. Leave it unset when using the API.

How each part works

1. Fetch fixtures and prices

main() reads ODDSLINK_API_KEY and calls the NFL endpoint. A timeout stops the request from waiting indefinitely. raise_for_status() reports an HTTP error before the script tries to read prices.

The JSON response is an array of fixtures. Each fixture contains bookmakers[], then markets[], then outcomes[]. On moneylines and spreads, including periods and alternate lines, a team outcome's name equals the fixture's home_team or away_team. Totals use Over or Under.

2. Find the best price per side and line

best belongs to one fixture. Its key is (market, period, side, point). Eagles -3.5 stays separate from Eagles -4.5. A first-half total stays separate from a full-fixture total.

side.get('price') returns None for an absent price, just as a JSON null does. The script skips both and skips invalid American prices, including zero. It does not fill a missing price with zero.

For positive American odds A, implied probability is 100 / (A + 100). For negative odds it is -A / (-A + 100). A lower implied probability means a higher payout for the same side and line.

The reverse conversion is 100 / p - 100 when p <= 0.5, and -100 * p / (1 - p) otherwise, for 0 < p < 1. The american() helper rounds to integer odds. For example, +150 is 40% and -200 is about 66.67%.

3. Print a table

Each row prints the fixture, market, side with its line, price and book. Positive prices retain their plus sign. Equal prices keep the first book encountered.

Extend it: flag arbitrage

The script already includes a two-sided arbitrage check. Add the best implied probabilities for both sides. When the combined value is below 100%, print an ARB flag.

In the sample, +110 on Eagles -3.5 and +110 on Bears +3.5 imply 100 / 210 + 100 / 210 = 95.24%. The +105 prices for Over and Under 44.5 imply 97.56%.

For spreads, the grouping uses the away team's line: home +3.5 pairs with away -3.5. Totals require the same point. The check also requires exactly the expected sides, so a moneyline containing a Draw is excluded.

A flag identifies a price combination to inspect. Account for fees, available size and settlement rules in your scanner. Whole-number lines can push. This calculation does not model those details.

For +EV screens, compare the book's implied probability with your own fair probability. For middles scanners, keep different lines and identify their overlapping outcomes. OddsLink supplies the data; it does not place bets or give picks.

Run it on a schedule

Use cron on Linux or macOS, or Task Scheduler on Windows. This cron example runs once a day at 09:00 in the scheduler's timezone and appends output to a file.

0 9 * * * cd /path/to/scanner && /usr/bin/python3 odds_scanner.py >> scanner.log 2>&1

Set ODDSLINK_API_KEY in the scheduler's environment. A scheduled job does not inherit the export from your interactive terminal. Use the Python path where you installed requests.

Choose a schedule within your request allowance. Each run costs one request, even when the response contains multiple fixtures and markets. See plan allowances.

Questions

Can I build a free odds scanner?

Yes. The permanent Free plan includes 1,000 requests a month and all 46 books. This script makes one price request per run. It uses Python 3 and the requests package.

What does this sports betting API Python example return?

The NFL endpoint returns fixtures with bookmakers, markets and outcomes. Each outcome names a side and its price. Spreads and totals also include a point, which identifies the line.

Why compare only the same line?

Eagles -3.5 and Eagles -4.5 settle differently. Keep each side and point separate when selecting a price. For an arbitrage check, pair opposite spreads or Over and Under at the same total.

Can I use it for arbitrage, +EV or middles?

Yes. The script flags two-sided markets whose combined implied probability is below 100%. Add your own fair probabilities for +EV screens, or compare different lines for middles. OddsLink supplies data and does not place bets or give picks.

Does the Free plan include in-play odds?

The endpoint in this guide returns fixtures that have not started. In-play odds and props start on Growth. See pricing for plan features and request allowances.

Build your scanner with one response shape.

Use the odds scanner API for price comparison, arbitrage, +EV and middles tools. Start with the Free plan.