Tutorial · 12 min · endpoints verified 2026-09-11

NFL betting model API tutorial

Build a betting model the way a book-watcher actually works: pull the current consensus line for a game, then pull every snapshot behind it to see whether the number moved toward you or away from you before kickoff. About fifteen minutes.

Why this is hard

The problem with NFL betting data

A single sportsbook’s line tells you what one book thinks. It does not tell you what the market thinks, and it does not tell you whether the line you would get today is better or worse than the line that existed an hour ago. Both of those need aggregation across books and a snapshot history, which is the part most free feeds do not carry.

  • A one-book price is noise. This build aggregates across dozens of sportsbooks per market — the sample game below carries 80 books on the moneyline alone — so the number reflects the market, not one outlier.
  • Line movement is the actual signal. A closing line that moved 1.5 points toward one side between the open and kickoff is informative; the closing number alone is not.
  • Moneyline, spread and totals are usually three different data shapes from three different vendors if you assemble this yourself. Here they are one response, one call.
  • This is priced data. Bookmaker odds sit behind Edge on this API — the endpoints below are the two paid calls this build needs, and there are no free substitutes for them.

Path 1 · recommended

Have your AI agent build it

The player- and match-lookup half of this build is fully agentic; the odds themselves are not — no MCP tool serves bookmaker lines today. Use the server to resolve the game and pull context, then have your agent hit the two odds endpoints directly over HTTP with the same key.

1. Connect the MCP server

Add the server in your MCP client and sign in with your Big Balls account. Your client registers itself and handles the token exchange — there is no key to copy.

MCP client configjson
{
  "mcpServers": {
    "bigballs-sports-data": {
      "url": "https://mcp.bigballsdata.com/mcp"
    }
  }
}

2. Tools your agent gets

  • get_matches

    Live, upcoming or historical matches for a sport or league.

  • get_coverage

    Machine-readable map of what we hold and what we do not.

3. Ask for what you want

Paste this at your agent. It is written to make the model check coverage before it designs anything, which is what stops it inventing a field we do not serve.

Prompttext
Build me a simple NFL closing-line model.

Use the Big Balls Sports Data MCP server to call get_matches for
american_football / nfl and find a game I name, so I have its match id.

The MCP server has no odds tool, so for the odds themselves call the REST
API directly with my key:
1. GET /v1/matches/{id}/odds — the current aggregated moneyline, spread and
   total, with book_count per market.
2. GET /v1/odds/historical?match_id={id} — every snapshot (opening, t24h,
   closing) so I can see whether the number moved and which way.

Compute, per market: how far the closing line moved from the opening line,
and which side of the move I'd be on if I bet the opener versus the close.
Tell me which numbers came from which call.

What the agent cannot reach

Odds require an Edge-plan key — both calls above 403 on Free, Solo and Trio, and the error body names the plan and links straight to billing, so an agent reading the response can surface that rather than silently failing. Nothing else in this build is gated: match lookup and coverage are free.

Path 2 · hand-coded

Build it yourself

Every call below was run against production on 2026-09-11 with an Edge-tier key, against a live Rams–49ers game. The odds calls need Edge; everything else is free.

  1. 01

    Find a game with odds

    Filter matches to live or upcoming and check has_odds before spending an odds call on a game that has none.

    GET /v1/matchesbash
    curl -s "https://api.bigballsdata.com/v1/matches?sport=american_football&league=nfl&status=live" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": [
        {
          "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668",
          "sport": "american_football",
          "league": "NFL",
          "home": { "id": "a6a6cdd2-b5fe-4bb9-bf37-a89daa891e5c", "name": "Los Angeles Rams", "short_name": "LAR" },
          "away": { "id": "3393aa2f-1261-465e-bfe6-a5de7685c0f1", "name": "San Francisco 49ers", "short_name": "SF" },
          "kickoff_utc": "2026-09-11T00:35:00.000Z",
          "status": "live",
          "score": { "home": 7, "away": 10 },
          "has_odds": true
        }
      ]
    }
  2. 02

    Pull the current consensus line

    One call returns moneyline, spread and total, each aggregated across every book currently quoting the game — book_count tells you how many. This game had 80 books on the moneyline and 114 on the total.

    GET /v1/matches/:id/odds (Edge plan)bash
    curl -s "https://api.bigballsdata.com/v1/matches/5b2e8a2c-9c4f-4f0f-b035-fa23b0311668/odds" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": {
        "odds": [
          { "market": "moneyline", "participant": "Los Angeles Rams",  "decimal_odds": 1.17,  "book_count": 80 },
          { "market": "moneyline", "participant": "San Francisco 49ers", "decimal_odds": 1.639, "book_count": 69 },
          { "market": "spread",    "participant": "Los Angeles Rams",  "decimal_odds": 1.019, "book_count": 56 },
          { "market": "total",     "participant": "Over",  "decimal_odds": 1.406, "line": 48.083, "book_count": 114 },
          { "market": "total",     "participant": "Under", "decimal_odds": 1.485, "line": 48.083, "book_count": 93 },
          { "market": "handicap",  "participant": "San Francisco 49ers", "decimal_odds": 1.934, "line": 3.655, "book_count": 29 },
          { "market": "handicap",  "participant": "Los Angeles Rams", "decimal_odds": 1.926, "line": -3.625, "book_count": 24 }
        ]
      },
      "meta": { "source": "stored", "count": 8 }
    }
  3. 03

    Pull every snapshot for line movement

    The historical endpoint returns every snapshot — opening, t24h and closing — newest first, each with its own sportsbook, price and line. This is what a single "current odds" call can never show you: which way the market moved before kickoff.

    GET /v1/odds/historical (Edge plan)bash
    curl -s "https://api.bigballsdata.com/v1/odds/historical?match_id=5b2e8a2c-9c4f-4f0f-b035-fa23b0311668" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": [
        {
          "match_id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668",
          "snapshot_type": "closing",
          "snapshot_at": "2026-09-11T00:30:23.087Z",
          "market": "spread",
          "book": { "id": "oddsapi_betmgm", "name": "BetMGM" },
          "line": { "price": 1.98, "value": -3.5, "participant": "Los Angeles Rams" },
          "source": "aggregator-paid",
          "verification": "unverified",
          "verified_at": null
        }
      ]
    }
  4. 04

    Compute the move

    Group snapshots by market and book, sort by snapshot_at, and diff the opening value against the closing one. A spread that opens at Rams -2.5 and closes at -3.5 moved a full point toward Los Angeles — the kind of signal a single-snapshot feed cannot show.

    Line movement from snapshotspython
    from collections import defaultdict
    
    def movement(snapshots, market):
        by_type = {s["snapshot_type"]: s for s in snapshots if s["market"] == market}
        opening = by_type.get("opening")
        closing = by_type.get("closing")
        if not opening or not closing:
            return None
        return {
            "book": closing["book"]["name"],
            "opening_line": opening["line"]["value"],
            "closing_line": closing["line"]["value"],
            "moved": round((closing["line"]["value"] or 0) - (opening["line"]["value"] or 0), 2),
        }
  5. 05

    Read each field honestly

    verification is not decoration: closing-line rows can be "two_source_agreed", "two_source_diverged", "single_source" or "unverified" from real two-source cross-checking, never inferred from row-absence. A model that treats "unverified" as "wrong" will throw away most of its own history — treat it as "not yet cross-checked", not "bad".

Reference

Every endpoint this tutorial uses

All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-11.

MethodPathReturnsWhy you need itPlan
GET/v1/matches?sport=american_football&league=nfl&status=Match id, teams, kickoff time, current score and has_oddsFinds the match id every odds call needs, and has_odds avoids spending a paid call on a game with none.Free
GET/v1/matches/:id/oddsCurrent moneyline, spread and total, aggregated across every quoting bookThe consensus line the model is built on — book_count tells you how thin or deep the market is behind each number.Edge
GET/v1/odds/historicalEvery snapshot (opening, t24h, closing) with per-book price and lineThe only way to see movement. A single "current odds" call is a snapshot; this is the whole series.Edge

Pricing, honestly

Where the free tier stops

This build has no free-tier version worth shipping. Match lookup is free, but both odds calls 403 below Edge — the error body itself is where a free-tier reader learns that, and it names the tier and links straight to billing rather than returning a vague 403.

Free key

jsonjson
{
  "error": {
    "code": "forbidden",
    "message": "Access to bookmaker odds requires the Edge plan or higher."
  },
  "suggested_fix": "Edge is $149/month and unlocks live and stored bookmaker lines across every book — moneyline, spread and totals — plus opening→closing movement and CLV.",
  "docs_url": "https://bigballsdata.com/docs/endpoints#odds"
}

Solo

jsonjson
{
  "data": {
    "odds": [
      { "market": "moneyline", "participant": "Los Angeles Rams", "decimal_odds": 1.17, "book_count": 80 }
    ]
  },
  "meta": { "source": "stored", "count": 8 }
}
  • Free and Solo cannot call either odds endpoint at all — this is a hard gate, not a truncated response.
  • Edge ($149/month, per the API’s own upgrade message) unlocks both the current aggregated line and the full snapshot history behind it.
  • The aggregation itself is the product: 80 books on one moneyline is not something a single-book feed can reconstruct after the fact.

More tutorials

Other build guides

Questions

Can I get NFL odds on the free tier at all?
No. Unlike most of this API, odds is an all-or-nothing gate: /v1/matches/:id/odds and /v1/odds/historical both return a 403 on Free, Solo and Trio, with no reduced-shape response below Edge. There is no free preview to build against.
Whose odds are these — one sportsbook or many?
Many, aggregated. The moneyline on the sample game above had 80 contributing books; the total had 114. book_count on every row tells you how many books fed that specific number, so you can weight thin markets differently from deep ones.
What does "verification" mean on a historical snapshot?
It is an evidence-based field from real two-source cross-checking — two_source_agreed, two_source_diverged (with divergence_categories), single_source, or unverified. A "verified" status is never inferred from a row simply being present; it means two independent sources were actually compared for that snapshot.
Is there an MCP tool for odds?
No. The live MCP server has no odds tool as of this writing — resolve the match through get_matches, then call the two REST odds endpoints directly with the same API key.

Build it

Odds need an Edge key. Everything else in this tutorial — match lookup, coverage — runs on a free one, so you can build the plumbing before you pay for the lines.