NFL Key Numbers: What a Half Point Actually Costs (Python)

NFL Key Numbers - OddsPapi API Blog
How To Guides August 5, 2026

Every football bettor has heard that 3 is the most important number in the sport. Almost nobody can tell you what it costs. This post measures it: 82 half-point steps priced by Pinnacle across all 14 NFL Week 1 games, pulled from the live API and de-vigged one rung at a time.

The answer, up front: moving a spread across 3 costs 2.76 times what a normal half point costs. Crossing 7 costs roughly twice. Everything else is noise, and some half points are almost free.

Where the data comes from

Retail books post one spread. Pinnacle posts a ladder. On our Week 1 sample, Pinnacle quoted nine handicap lines on every single game, walking out from the main number in half-point steps, while the seven books quoting the main line offered that line and nothing else.

Line Books quoting it
-3.5 (the main line) 7: Pinnacle, Bet365, Caesars, DraftKings, Circa, HardRock, William Hill
-3 1: Pinnacle
-4 1: Pinnacle
-4.5 through -10.5 1: Pinnacle

That makes Pinnacle’s ladder the only honest source for what a half point is worth. The book prices every rung with its own money at 3% margin, so the gaps between rungs are a market estimate of how often NFL games land on each exact margin.

Step 1: Pull the ladder

NFL spreads use one market ID per line. There is no fixed “spread” ID, so resolve them by name from the market catalog and keep the handicap attached. If you have not set up the basics, our NFL odds API guide covers auth and fixtures.

import time
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"


def api(path, **params):
    for _ in range(4):
        r = requests.get(f"{BASE_URL}/{path}", params={"apiKey": API_KEY, **params})
        body = r.json()
        if r.status_code == 200 and not (isinstance(body, dict) and body.get("error")):
            return body
        time.sleep((body.get("error") or {}).get("retryMs", 2000) / 1000 + 0.3)
    raise RuntimeError(f"{path} kept rate-limiting")


def handicap_ladder(fixture_id, book="pinnacle"):
    """{line: (price_team1, price_team2)} for every spread the book quotes."""
    catalog = api("markets", sportId=14)
    spreads = {m["marketId"]: m["handicap"] for m in catalog
               if m["marketName"] == "Handicap (incl. overtime)"}

    odds = api("odds", fixtureId=fixture_id)
    markets = odds["bookmakerOdds"][book]["markets"]

    ladder = {}
    for market_id, market in markets.items():
        line = spreads.get(int(market_id))
        if line is None:
            continue
        prices = {}
        for outcome_id, outcome in market["outcomes"].items():
            p = outcome["players"].get("0")
            if p and p.get("active") is not False:
                prices[int(outcome_id)] = p["price"]
        base = int(market_id)
        if base in prices and base + 1 in prices:
            ladder[line] = (prices[base], prices[base + 1])
    return dict(sorted(ladder.items()))

Run it on Seahawks at Patriots and you get the whole board:

  -10.5   3.29 / 1.353
     -6   2.24 / 1.694
   -5.5   2.14 / 1.763
     -5   2.10 / 1.800
   -4.5   2.04 / 1.854
     -4   1.961 / 1.917
   -3.5   1.877 / 1.990
     -3   1.740 / 2.170
    3.5   1.294 / 3.680

Look at the bottom four rungs. Each half point costs a little, then the step from -3.5 to -3 costs a lot. Seattle at -4 pays 1.961, at -3.5 pays 1.877 (a 4.3% price drop), and at -3 pays 1.740 (a further 7.3%). The market is telling you that landing on exactly 3 happens far more often than landing on exactly 4.

Step 2: Convert each rung to a fair probability

Raw prices carry the book’s margin, and the margin is not identical on every rung. Strip it out before comparing. Full treatment of the three methods is in our no-vig odds guide; the proportional version is enough here.

def fair_probability(price_a, price_b):
    """Proportional de-vig. Returns the fair probability of side A."""
    overround = 1 / price_a + 1 / price_b
    return (1 / price_a) / overround


ladder = handicap_ladder("id1400003171515752")
fair = {line: fair_probability(a, b) for line, (a, b) in ladder.items()}

Step 3: Measure what each half point buys

Walk adjacent rungs and take the difference in fair probability. Skip any pair that is not exactly half a point apart, because Pinnacle’s ladder has gaps.

import math


def half_point_steps(fair):
    """[(from_line, to_line, probability_gain, margin_captured)]"""
    lines = sorted(fair)
    steps = []
    for low, high in zip(lines, lines[1:]):
        if abs(high - low - 0.5) > 1e-9:
            continue
        margin = math.ceil(abs(high)) if high < 0 else math.ceil(abs(low))
        steps.append((low, high, (fair[high] - fair[low]) * 100, margin))
    return steps


for low, high, gain, margin in half_point_steps(fair):
    print(f"{low:>6} -> {high:<6} +{gain:.2f} pp   (captures margin {margin})")

The margin each step captures is the point of the exercise. Moving from -3.5 to -3 does not make the bet generally easier, it adds exactly one outcome: the game landing on a 3-point margin, which turns from a loss into a push. Moving from -4 to -3.5 adds half the value of a 4-point margin, turning a push into a win. Group the steps by the margin they touch and the NFL’s scoring distribution appears in the prices.

What 14 games say

Running that across every Week 1 fixture gives 82 clean half-point steps on Pinnacle’s ladder:

Margin captured Steps measured Mean cost (percentage points of win probability)
3 17 4.24
7 4 3.25
10 2 2.15
6 4 1.83
4 15 1.75
1 7 1.44
2 17 1.37
11 2 1.36
8 4 1.23
5 7 1.14
12 1 1.07
9 2 0.80

Margins 3 and 7 average 4.06 percentage points per half point. Every other margin averages 1.47. That is the 2.76x ratio, measured rather than repeated from a forum post.

The individual steps are consistent enough to trust. Every one of the seventeen margin-3 steps landed between 3.47 and 5.07 percentage points, across nine different games with spreads ranging from pick’em to double digits. Compare that to margin 9, which came in at 0.80, or margin 5 at 1.14. Buying a half point from -5.5 to -5 gets you almost nothing. Buying from -3.5 to -3 gets you four times as much.

One detail worth noticing: both halves of the 3 cost about the same, a little over four points each. Crossing the full number, from -3.5 to -2.5, ran to roughly 8.5 points of win probability in our sample. Any book selling you that full point at a flat rate is selling it cheap.

Step 4: Turn it into a decision rule

Retail books sell points at a fixed price, usually 10 cents per half point with a surcharge on and around 3. The Pinnacle ladder tells you the fair price. Compare the two and you have a rule.

def worth_buying(fair, from_line, to_line, price_offered):
    """Is the book's buy-point price better than the probability you gain?"""
    fair_price = 1 / fair[to_line]
    return {
        "probability_gained": round((fair[to_line] - fair[from_line]) * 100, 2),
        "fair_price_at_new_line": round(fair_price, 3),
        "price_offered": price_offered,
        "worth_it": price_offered > fair_price,
    }


# Your book will move Seattle from -3.5 to -3 if you take 1.80 instead of 1.877.
print(worth_buying(fair, -3.5, -3.0, 1.80))
# {'probability_gained': 4.04, 'fair_price_at_new_line': 1.802,
#  'price_offered': 1.8, 'worth_it': False}

Run that across a slate and the pattern is nearly always the same. Books charge a premium on 3 because they know what it is worth, so buying onto 3 at retail is usually a bad trade, while buying half points in the 5 to 9 range is often priced as if all half points are equal. The mispricing is not on the famous number, it is on the boring ones.

Step 5: Scan the slate for the cheapest points

def cheapest_steps(fixture_ids, limit=5):
    found = []
    for fixture_id in fixture_ids:
        ladder = handicap_ladder(fixture_id)
        fair = {line: fair_probability(a, b) for line, (a, b) in ladder.items()}
        for low, high, gain, margin in half_point_steps(fair):
            found.append((gain, fixture_id, low, high, margin))
        time.sleep(1.0)          # same-endpoint cooldown, do not thread this
    return sorted(found)[:limit]

Sorted ascending you get the half points the market thinks are worthless, which are the ones to take if a book is charging you a flat rate for them. Sorted descending you get the rungs to sell, if your book lets you move a line the other way for a price.

What this does not prove

Fourteen games, one book, one week of one season. The margin-3 result rests on 17 steps and the margin-7 result on only 4, so treat the smaller buckets as directional. Pinnacle also prices its Week 1 ladder six weeks out with limits of around $1,500, which is low by its standards and signals that the book is not fully confident in these numbers yet. Rerun the script in December against a full slate of sixteen games and the sample multiplies quickly.

The method is the durable part. Every price on every rung is free to pull, and /historical-odds gives you the same ladder as it existed at any earlier timestamp, so you can watch the cost of a half point change as money arrives. That is the same data behind middling two lines and it comes on the free tier rather than an enterprise contract.

Frequently Asked Questions

What are key numbers in NFL betting?

The margins NFL games most often land on, driven by scoring in 3s and 7s. Measured against Pinnacle’s alternate spread ladder across 14 Week 1 games, a half point that captures a 3-point margin was worth 4.24 percentage points of win probability, against 1.47 for a half point at any non-key margin.

How much should buying a half point cost?

It depends entirely on which margin you are buying. Crossing 3 was worth about 4.2 percentage points in our sample and crossing 7 about 3.25, while margins 5 and 9 came in at 1.14 and 0.80. A book charging one flat rate for every half point is overcharging on 3 and undercharging on 9.

Which bookmakers publish alternate NFL spreads?

On the Week 1 games we sampled, Pinnacle quoted nine handicap lines per game while the other books posted the main line only. Pinnacle’s ladder is the practical source for half-point pricing on the OddsPapi feed.

What market ID do NFL spreads use?

Each line is its own market ID, so -3.5 and -4 are different markets (14272 and 14270 on our test game). Query /markets?sportId=14, filter on the market name “Handicap (incl. overtime)”, and read the handicap field rather than hardcoding IDs.

Can I reproduce this on past seasons?

Yes. The /historical-odds endpoint returns timestamped snapshots of the same ladder, including the limit at each timestamp, on the free tier. Loop it over past fixtures to build a much larger sample than one week.

Run it yourself

The whole study is four functions and a loop: pull the ladder, de-vig each rung, difference the adjacent ones, group by the margin captured. It runs on 14 fixtures in under a minute.

Get your free API key and price your own half points before Week 1.