# Sports Betting Odds & Player Props Scraper (`fanndev/espn-betting-odds-scraper`) Actor

Sportsbook lines for NFL, college football, NBA, MLB, NHL, soccer and UFC: spread, moneyline and total per book, how far each line moved since it opened, ~659 player props per game, season futures with implied probability, and the model win projection to compare against the price. No login.

- **URL**: https://apify.com/fanndev/espn-betting-odds-scraper.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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

## Sports Betting Odds & Player Props Scraper

Sportsbook lines for NFL, college football, NBA, WNBA, college basketball, MLB,
NHL, 219 soccer competitions, UFC, golf, tennis and F1 — the point spread, the
moneyline and the total from every book the source lists, **plus the things most
odds scrapers leave on the table**: how far each line has moved since the book
opened it, the full player-prop board (about 659 markets on one NFL game), the
season futures board with implied probability per runner, and the source's own
win projection so you can put the model next to the price.

No login, no API key, no browser. Public endpoints only.

***

### What you get

#### `odds` — the line, per book, with its movement

One row per fixture per sportsbook.

| Field | Example |
| --- | --- |
| `eventName` | `Detroit Lions at Buffalo Bills` |
| `providerName` | `Draft Kings` |
| `details` / `spread` / `overUnder` | `BUF -5.5` / `-5.5` / `54.5` |
| `homeMoneyline` / `awayMoneyline` | `-245` / `+200` |
| `homeImpliedProbability` / `awayImpliedProbability` | `0.7101` / `0.3333` |
| `openSpread` → `currentSpread` (`spreadMovement`) | `+3` → `-5.5` (`-8.5`) |
| `openTotal` → `currentTotal` (`totalMovement`) | `52.5` → `54.5` (`+2.0`) |
| `favoriteFlippedSinceOpen` | `true` |

`spreadMovement`, `totalMovement`, `favoriteFlippedSinceOpen` and the implied
probabilities are **derived here** — upstream publishes the opening and current
numbers and leaves the arithmetic to you. The vig is left in, so the two sides
of a moneyline sum to a little over 1; that excess is the book's margin, and
removing it would be inventing data.

#### `props` — the player board

About **659 prop markets on a single NFL game**: passing yards, receptions,
anytime touchdown scorer, first touchdown scorer, and 70-plus other market
types, each with the line it opened at, the line it sits at now and the
difference.

#### `futures` — season-long markets

MVP, Offensive/Defensive Player of the Year, conference and division winners,
championship. Every runner carries its American price **and** its implied
probability, so `+400` and `-245` sit in the same sortable column.

#### `predictor` — model against book

The source's own win projection and predicted margin per fixture. Put
`homeWinProbability` next to `homeImpliedProbability × 100`, or
`homePredictedPointDiff` next to `spread`, and the disagreements fall out.

#### `events`, `providers`, `reference`

The schedule on its own (no odds, cheap), the sportsbook catalogue for a league
(71 entries for the NFL), and a probe that checks the source is live and
re-tests the assumptions this Actor is built on.

***

### Input

| Field | Default | Notes |
| --- | --- | --- |
| `mode` | `odds` | `odds`, `props`, `futures`, `events`, `predictor`, `providers`, `reference` |
| `league` | `nfl` | A short key, or the source's own `sport/league` form for anything else |
| `dates` | — | `20260920`, `2026`, or a range `20260901-20261001`. Empty = the current week only |
| `dateFrom` / `dateTo` | — | Used when `dates` is empty |
| `eventId` / `eventIds` | — | Scrape specific fixtures instead of a window |
| `season` | current year | `futures` mode |
| `includeUnpricedEvents` | `true` | Keep fixtures no book has priced yet, as `hasOdds: false` |
| `maxItems` | `500` | Record cap |
| `maxEvents` | `60` | Fixture cap — a whole NFL season window is 320 fixtures |
| `maxConcurrency` | `4` | |
| `maxNameLookups` | `400` | Budget for turning ids into names |
| `proxyConfiguration` | off | Not needed for ordinary runs |

League keys: `nfl`, `college-football`, `nba`, `wnba`, `ncaab`, `ncaaw`, `mlb`,
`nhl`, `epl`, `laliga`, `seriea`, `bundesliga`, `ligue1`, `ucl`, `mls`, `ufc`,
`pga`, `atp`, `wta`, `f1` — or anything else as `sport/league`, e.g.
`soccer/ned.1`. There are 17 sports and 219 soccer competitions.

#### Examples

This week's NFL lines with movement:

```json
{ "mode": "odds", "league": "nfl" }
```

Every prop on one game:

```json
{ "mode": "props", "league": "nfl", "eventId": "401872932", "maxItems": 700 }
```

A month of Premier League fixtures:

```json
{ "mode": "events", "league": "epl", "dates": "20260915-20261015" }
```

The NFL futures board:

```json
{ "mode": "futures", "league": "nfl", "season": 2026, "maxNameLookups": 400 }
```

***

### Known limits — read these before you build on it

- **A fixture with no odds is normal.** Books post lines close to start time.
  Fixtures weeks out come back with `hasOdds: false` and a note; that is the
  source answering, not a block. Failures are separate `ERROR` records.
- **Coverage follows the season, not the league.** During testing NFL, college
  football, MLB, the Premier League, MLS and UFC carried lines; NBA, NHL,
  college basketball and tennis did not, because they were out of season.
- **Line movement needs both ends.** `spreadMovement` is null when the book has
  not published an opening number for that market.
- **Names are the expensive part.** Ids come free in the response; names cost one
  request each, cached. Past `maxNameLookups` a record keeps its ids and loses
  only the name — it is never dropped.
- **Prop counts are de-duplicated.** The feed publishes repeated rows (659 rows,
  521 distinct markets on one game). This Actor emits the distinct ones and logs
  how many repeats it dropped.
- **No historical line archive.** The source exposes open, close and current —
  not a tick-by-tick history. The movement-history endpoint returned `count: 0`
  on every fixture sampled. Run this Actor on a schedule if you want a series.
- **`providers` includes a placeholder.** Entry id `0` is literally named
  "Not Available"; it is passed through rather than hidden, as with every other
  upstream field.
- **Join on `providerId`, not `providerName`.** The same book is written
  "Draft Kings" in the odds response and "DraftKings" in the futures response.
  Names are passed through exactly as the source writes them.
- **Book count per fixture varies with kick-off.** A sportsbook's live in-play
  feed appears as a second row near game time and is gone hours later, so one
  row per fixture and two rows per fixture are both normal.

### Technical notes

Python, HTTP only, `curl_cffi` with a TLS ladder that remembers the winning
profile. No WAF answered during testing from either a residential address or
Apify's datacenter range, but the ladder stays because the profile that wins
locally is not always the profile that wins from a datacenter.

`CRAWLING_METHOD.md` has the full probe matrix, the ten gotchas found while
building (including the two hosts that disagree about date syntax and the
internal hostname leaking into pointers), and the candidates probed in the same
pass that were skipped with evidence.

# Actor input Schema

## `mode` (type: `string`):

odds = point spread, moneyline and total per sportsbook for each fixture, plus how far the line has moved since it opened. props = player prop markets (about 659 per NFL game). futures = season markets such as MVP and championship winner, with implied probability. events = the schedule only, no odds. predictor = ESPN's own win projection, to compare against the book. providers = the sportsbook catalogue for a league. reference = check the source is live and report what it returns today.

## `league` (type: `string`):

A short key (nfl, college-football, nba, wnba, ncaab, ncaaw, mlb, nhl, epl, laliga, seriea, bundesliga, ligue1, ucl, mls, ufc, pga, atp, wta, f1) or the source's own form 'sport/league' for anything else, e.g. 'soccer/ned.1'. There are 17 sports and 219 soccer competitions.

## `dates` (type: `string`):

One day (20260920), a whole season (2026) or a range (20260901-20261001). Leave empty and you get the current week only, which is a handful of fixtures.

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

Start of the window, YYYY-MM-DD or YYYYMMDD. Used only when 'Date window' is empty.

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

End of the window, YYYY-MM-DD or YYYYMMDD. Used only when 'Date window' is empty.

## `eventId` (type: `string`):

Scrape one fixture by its ESPN event id instead of a date window.

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

Several fixtures by id. Overrides the date window.

## `season` (type: `integer`):

Which season's futures board to read. Defaults to the current year.

## `includeUnpricedEvents` (type: `boolean`):

Sportsbooks post lines close to start time, so a fixture weeks out returns an empty provider list. On, you get a row with hasOdds=false so you can tell 'not priced yet' from 'not fetched'. Off, those fixtures are skipped.

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

Maximum records per run. An NFL week is about 16 fixtures and 1-2 sportsbooks each in odds mode, but one game alone yields about 659 rows in props mode.

## `maxEvents` (type: `integer`):

How many fixtures the date window may expand to. The source returns pointers, so every fixture costs one extra request - a whole-season window is 320 NFL fixtures and this is what keeps that in hand.

## `maxConcurrency` (type: `integer`):

How many fixtures to work on in parallel.

## `maxNameLookups` (type: `integer`):

The source returns ids and pointers, so athlete and team names cost one extra request each. They are cached (659 NFL props resolve from 47 athletes). Raise this for futures boards, which have far more distinct runners; past the budget records keep their ids and lose only the name.

## `tlsProfile` (type: `string`):

Leave empty. The Actor tries a ladder of browser fingerprints and remembers the one that works, because the winner on a home connection is not always the winner from a datacenter.

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

Optional. The source answered plain requests from both a home connection and a datacenter address during testing, so no proxy is needed for ordinary runs. Turn one on if you are running large windows repeatedly.

## Actor input object example

```json
{
  "mode": "odds",
  "league": "nfl",
  "includeUnpricedEvents": true,
  "maxItems": 500,
  "maxEvents": 60,
  "maxConcurrency": 4,
  "maxNameLookups": 400,
  "tlsProfile": "",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All odds, prop, futures, fixture, predictor, provider, reference and error records produced 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 = {
    "league": "nfl",
    "dates": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/espn-betting-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 = {
    "league": "nfl",
    "dates": "",
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/espn-betting-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 '{
  "league": "nfl",
  "dates": ""
}' |
apify call fanndev/espn-betting-odds-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/espn-betting-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/NzckyoECTGjjSlEn7/builds/O8EAgdnZ4brYAGhcr/openapi.json
