# ESPN Odds & Line Movement Scraper (`incognito_mode/espn-odds-scraper`) Actor

Betting lines for NFL, NBA, MLB, NHL, NCAA, WNBA and soccer, back to 2014 and up to 17 sportsbooks a game. Moneyline, spread and total with opening and closing prices, de-vigged fair odds, the book's hold, line movement, and how every bet settled — computed from the final score.

- **URL**: https://apify.com/incognito\_mode/espn-odds-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 odds rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ESPN Odds & Line Movement Scraper

Betting lines for **NFL, NBA, MLB, NHL, NCAA football and basketball, WNBA and
soccer** — moneyline, spread and total, from every sportsbook ESPN carries,
**back to 2014**.

One row is one book's complete pricing of one game: the opening line and the
closing line, the margin the book charged, the de-vigged fair price, how far
the number moved, and — for finished games — **whether each bet actually won**.

```
gameDate    league  game       book         ML home  spread  open   move  total  ATS
2024-01-15  nba     HOU @ PHI  DraftKings      -355    -8.0  -7.5   -0.5  229.0  home
2024-01-15  nba     HOU @ PHI  MGM             -350    -8.0  -7.0   -1.0  228.5  home
2024-01-15  nba     HOU @ PHI  Caesars (NJ)    -355    -8.0  -7.0   -1.0  228.5  home
```

***

### What this does that the other ESPN scrapers cannot

**Historical odds live on one ESPN host, and it is not the obvious one.** The
scoreboard endpoint every other ESPN Actor is built on returns `odds: []` for
any game that has finished — verified on completed MLB games from 2020, 2024
and 2025 — and so does the game-summary endpoint. Only
`sports.core.api.espn.com` keeps them, and it keeps **every book that ever
priced the game**.

That difference is the product:

| | Scoreboard-based Actors | This Actor |
| --- | --- | --- |
| Today's slate | ✅ | ✅ |
| A game from last season | ❌ empty | ✅ |
| Books per game | 1 | **up to 17** |
| Opening line | ❌ | ✅ (2024 onward) |
| Bet settlement | ❌ | ✅ computed |

**Bet settlement is computed, not copied.** Every ESPN odds item carries
`moneylineWinner` and `spreadWinner`. Across **691 provider rows on 67 finished
games** in three leagues, both were `false` *every single time* — including the
395 where the home team won. They carry no information at all. This Actor
ignores them and settles from the final score instead, with pushes handled
properly and a soccer draw kept distinct from a tie.

**The betting maths is done for you.** Implied probability, the book's
overround, its vig and its hold, de-vigged fair probabilities that sum to
exactly 1, the fair price at zero margin, line movement in points and in
probability, and closing-line value as a percentage.

***

### What one row looks like

```jsonc
{
  "recordId": "401585183-40",
  "gameDate": "2024-01-15",
  "league": "nba",
  "shortName": "HOU @ PHI",
  "home": { "abbreviation": "PHI", "score": 124 },
  "away": { "abbreviation": "HOU", "score": 115 },
  "provider": { "id": "40", "name": "DraftKings", "kind": "sportsbook" },

  "moneyline": {
    "home":  { "open": { "american": -285, "decimal": 1.3509, "impliedProbability": 0.7402 },
               "current": { "american": -355, "decimal": 1.2817 },
               "fairProbability": 0.7468, "fairAmerican": -295,
               "clvPercent": 5.4 },
    "away":  { "current": { "american": 278 }, "fairProbability": 0.2532 },
    "draw":  { "current": { "american": null } },     // soccer only
    "holdPercent": 4.28, "vigPercent": 4.48, "overround": 1.0448
  },

  "spread": { "openLine": -7.5, "currentLine": -8.0, "movementPoints": -0.5 },
  "total":  { "openLine": 228.5, "currentLine": 229.0, "movementPoints": 0.5 },

  "settlement": {
    "isFinal": true, "homeMargin": 9, "combinedScore": 239,
    "moneyline": "home", "spread": "home", "total": "over",
    "homeSpreadProfitUnits": 0.9091, "awaySpreadProfitUnits": -1.0
  },

  "dataQuality": { "snapshots": ["current", "open"], "hasOpeningLine": true }
}
```

Full field reference: `.actor/dataset_schema.json` — 196 fields, all documented,
all nullable.

***

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `leagues` | NFL, NBA, MLB, NHL | 16 leagues to choose from. |
| `customLeagues` | — | Any other ESPN competition as `"soccer/ned.1"`. |
| `dateFrom` / `dateTo` | a 5-day window around today | Inclusive. Up to 400 days per run. |
| `eventIds` | — | Specific games, by the id in their espn.com URL. |
| `providers` | every book | Filter by name or id. |
| `includeModelProviders` | `false` | Add ESPN's forecasting partners. |
| `onlyCompleted` | `false` | Keep only games with a settled result. |
| `maxItems` | `200` | **One row is one book per game.** |

`maxItems` is the cost lever and it is easy to underestimate: a 2026 game
produces **1** row and a 2021 game produces **ten**, so a date range costs far
more rows than it has games.

#### Examples

Yesterday's closing lines from every book:

```json
{ "leagues": ["nba"], "dateFrom": "2025-01-15", "dateTo": "2025-01-15" }
```

A season of NFL history to backtest against — every book, settled:

```json
{ "leagues": ["nfl"], "dateFrom": "2023-09-07", "dateTo": "2024-01-08",
  "onlyCompleted": true, "maxItems": 20000 }
```

One specific game:

```json
{ "leagues": ["nba"], "eventIds": ["401585183"] }
```

***

### What ESPN actually has, by era

Measured on the 15 January slate of every NBA season:

| Season | Sportsbooks per game | Opening lines |
| --- | --- | --- |
| 2012 and earlier | **none** | — |
| 2013 | none — forecasting models only | no |
| 2014 – 2023 | 6 – 13 | no |
| **2024** | ~11 | **yes** |
| 2025 | 2 (ESPN BET) | yes |
| 2026 | 1 (DraftKings) | yes |

Two consequences worth planning around:

- **Multi-book comparison is a historical product.** ESPN carried a dozen books
  through 2024 and its own single book afterwards. For line shopping across
  today's market you want a book-by-book scraper; for eleven years of closing
  lines from a dozen books, this is the only place they exist.
- **Opening lines start in 2024.** Earlier seasons carry the closing line only,
  so `movementPoints` and `clvPercent` are null there. `dataQuality.hasOpeningLine`
  says so per row rather than leaving you to infer it.

***

### Known ESPN quirks, handled

| Quirk | What it would do | What this Actor does |
| --- | --- | --- |
| `moneylineWinner` / `spreadWinner` always `false` | A "did it win" column that is wrong half the time | Not republished; settlement computed from the score |
| `close.pointSpread` sometimes holds a **price** (provider 58, some 2023 games) | A closing spread of `-115`; a closing total of `-115` | `close` is never read; `dataQuality.closeDisagreesWithCurrent` flags the games |
| `details` names the **favourite**, not the home team | A line whose sign points at an unknown team | Passed through as a label; `spread.currentLine` is always home-relative |
| Provider id `1001` is `accuscore` in the NBA and **`Bet365`** in the NHL | A forecasting model billed as a sportsbook | Classified by name, with the league carried alongside |
| Soccer has a **draw** | A three-way market de-vigged as two-way reports a negative hold | Three-way markets priced and settled as three outcomes |
| Provider `2000` quotes **decimal** odds and no American | Null prices for a book that published them | Both directions converted |
| A game listed on two calendar dates | Priced — and billed — twice | De-duplicated on the competition id |

***

### Notes

- **No API key, no proxy, no browser.** ESPN's public API has no bot defence:
  42 concurrent requests at concurrency 24 all returned 200 with no throttling.
  A proxy option exists for the case your own network is the problem.
- Odds requests take about **1.7 seconds each** regardless of load, so a wide
  date range is bounded by latency. Runs stop cleanly at a 210-second budget
  and report how many games went unpriced.
- Unofficial and not affiliated with ESPN or any sportsbook. Informational use
  only — check the rules that apply where you are before betting.

Built and verified against the live API on **2026-09-08**;
`docs/architecture.md` carries every measurement behind the claims above.

# Changelog

This Actor's version history is a separate document: https://apify.com/incognito\_mode/espn-odds-scraper/changelog.md

# Actor input Schema

## `leagues` (type: `array`):

Which leagues to collect odds for. Leave the default for a run that always finds games: the four major US leagues together cover every day of the year.

## `customLeagues` (type: `array`):

Any ESPN competition not in the list above, written as its sport and slug: "soccer/ned.1", "soccer/por.1", "basketball/nba-development". ESPN carries hundreds of soccer competitions; these are added to whatever is selected above.

## `dateFrom` (type: `string`):

First date to collect, YYYY-MM-DD. Leave both dates empty to scan a five-day window around today, which is what makes a default run always return something.

⚠️ ESPN publishes no odds before 2013 and no sportsbook prices before 2014. Opening lines start in 2024 — earlier seasons carry the closing line only.

## `dateTo` (type: `string`):

Last date to collect, YYYY-MM-DD. Inclusive. Leave empty to use the same day as the from date. At most 400 days in one run.

## `eventIds` (type: `array`):

ESPN event ids — the number in a game's espn.com URL, e.g. 401585183. Needs exactly one league selected, because an event id only means something inside its own league. When set, the dates are ignored.

## `providers` (type: `array`):

Provider names or ids to keep, e.g. "DraftKings" or "40". Leave empty for every book ESPN carried.

⚠️ Which books exist depends on the season, not on this filter: 2014–2024 games carry up to 17 providers, but ESPN collapsed to its own book for 2025 and to DraftKings for 2026. A name that matched last season may match nothing this one.

## `includeModelProviders` (type: `boolean`):

ESPN mixes four forecasting partners — accuscore, teamrankings, numberfire and consensus — in with the sportsbooks. Their numbers are projections, not prices anyone can bet, so they are excluded by default. Turn this on to get them, plus the columns only they carry: the public betting split and projected scores.

## `onlyCompleted` (type: `boolean`):

Keep only games that have finished, which are the ones with a settled result. Useful when building a backtest set.

## `maxItems` (type: `integer`):

Stop after this many odds rows. **One row is one sportsbook's pricing of one game**, so a 2021 NBA game can produce ten rows and a 2026 one just a single row — a date range costs far more rows than it has games. Raise it deliberately.

## `proxyConfiguration` (type: `object`):

ESPN's public API has no bot defence — measured at 24 concurrent requests with no throttling — so a proxy is not needed and is off by default. Set one only if your own network is the problem.

## Actor input object example

```json
{
  "leagues": [
    "nfl",
    "nba",
    "mlb",
    "nhl"
  ],
  "includeModelProviders": false,
  "onlyCompleted": false,
  "maxItems": 200,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Every odds row collected by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "leagues": [
        "nfl",
        "nba",
        "mlb",
        "nhl"
    ],
    "includeModelProviders": false,
    "onlyCompleted": false,
    "maxItems": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/espn-odds-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "leagues": [
        "nfl",
        "nba",
        "mlb",
        "nhl",
    ],
    "includeModelProviders": False,
    "onlyCompleted": False,
    "maxItems": 200,
}

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/espn-odds-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "leagues": [
    "nfl",
    "nba",
    "mlb",
    "nhl"
  ],
  "includeModelProviders": false,
  "onlyCompleted": false,
  "maxItems": 200
}' |
apify call incognito_mode/espn-odds-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/espn-odds-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RTJ1vBhSDGNweWPF9/builds/V9yStuT9sPRYJ1UWd/openapi.json
