# Action Network Scraper - Public Betting Splits Monitor (`neverempty/action-network-betting-splits`) Actor

Action Network public betting splits: tickets % and money % for moneyline, spread and total, one row per game with the consensus line, the opening line and each sportsbook's line. NFL, NCAAF, MLB, NHL, NBA, WNBA, NCAAB. Monitoring returns only games whose splits or lines moved.

- **URL**: https://apify.com/neverempty/action-network-betting-splits.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Developer tools, Automation
- **Stats:** 3 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.65 / 1,000 game returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Action Network Scraper - Public Betting Splits Monitor

Get Action Network's public betting splits - the **share of tickets (bets)** and the **share of money (handle)** on each side of the moneyline, the spread and the total - as one clean row per game, together with the consensus line, the opening line and every sportsbook's line from the same page. Turn on monitoring and schedule it: later runs return **only the games whose percentages, consensus line, consensus odds or status changed**, with the previous values in the same row.

Leagues: `nfl`, `ncaaf`, `mlb`, `nhl`, `nba`, `wnba`, `ncaab` (one Action Network public betting page each).

### What you get for each game

| Column | Meaning |
|---|---|
| `league`, `gameId`, `startTime`, `gameStatus` | Which game, when, and whether it is `scheduled`, `inprogress` or `complete` |
| `awayTeam`, `homeTeam` (+ abbreviations and ids) | The teams as Action Network names them |
| `numBets` | Number of bets Action Network says it tracks for the game |
| `mlAwayTicketsPct`, `mlAwayMoneyPct`, `mlHomeTicketsPct`, `mlHomeMoneyPct` | Moneyline: % of tickets and % of money on each side |
| `spreadAwayTicketsPct` ... `spreadHomeMoneyPct` | The same for the spread (run line / puck line) |
| `overTicketsPct` ... `underMoneyPct` | The same for the total |
| `mlHomeMoneyMinusTickets`, `spreadHomeMoneyMinusTickets`, `overMoneyMinusTickets` | Money % minus tickets % on that side, in points (positive = fewer but bigger bets on it) |
| `consensus...` | Consensus moneyline, spread and total with odds (Action Network's "Consensus" column) |
| `openMlAway`, `openMlHome`, `openSpreadHome`, `openTotal` | The page's "Open" column |
| `books` | One entry per sportsbook on the page (for example DK NJ, FanDuel NJ, BetMGM NJ, bet365 NJ, BetRivers NJ, Caesars NJ, Fanatics NJ, Kalshi): its moneyline, spread and total with odds |
| `splitsSource` | Where the percentages came from (normally `Consensus`) |
| `snapshotAt`, `copyAgeSeconds` | When the copy of the page this row came from was made, and how old it was when read (see "Freshness") |

A percentage is **empty (null), never 0**, when Action Network shows no number for that market. The `value` fields next to the percentages on the page are 0 for every outcome we measured, so they are not returned.

#### Why one row per game and not one row per sportsbook

On the NFL page read on 2026-09-28, the ticket and money percentages were **identical at all 9 sportsbooks for all 96 game sides that had them**. Selling one row per game per sportsbook would sell the same percentages nine times. Here you pay once per game and still get every sportsbook's line inside the row.

### Coverage measured on 2026-09-28 (US residential connection)

| Page | Games | Moneyline % | Spread % | Total % |
|---|---|---|---|---|
| NFL | 16 | 16 | 16 | 16 |
| NCAAF | 59 | 53 | 7 | 56 |
| MLB | 4 | 4 | 0 (run line shows no %) | 4 |
| NHL | 5 | 5 | 5 | 5 |
| WNBA | 2 | 2 | 2 | 2 |
| NBA (pre-season) | 1 | 0 | 0 | 0 |

The pages list the current slate only - today's or this week's games, including games that already ended that week with their final percentages. Coverage changes with the season and the day.

### Monitoring mode

1. Run once with **Monitoring mode** on: every game is new and is returned.
2. Schedule the same input (for example every 15 minutes). Each later run returns a game only when one of its 12 percentages, its consensus spread or total, its consensus odds or its status changed, with `changeKinds` (`splits`, `line`, `odds`, `status`), `biggestSplitMove` and the `previous...` columns filled in.
3. **Minimum percentage move** returns a game only after a ticket or money percentage moved at least that many points since it was last returned. Smaller moves are not remembered, so they add up.

When nothing changed, the run returns one free `no-change` row. Do not put the same input into two schedules that run at the same moment: Apify key-value stores have no atomic update, so two runs finishing together can overwrite each other's memory.

### Freshness: the same page is served from caches of different ages

Action Network serves these pages from a cache. On 2026-09-28 the copies we received were between **5 seconds and 8,240 seconds** old, and reading the same page twice could return an older copy the second time (the number of bets for a college football game went from 1,681 to 1,544 to 35 across reads). This Actor handles that openly:

- Every row carries `snapshotAt` (when that copy was made) and `copyAgeSeconds`.
- If the copy is older than 15 minutes, the Actor asks once more and keeps the fresher copy.
- In monitoring mode, a copy older than the one compared last time is **not** treated as a change: nothing from it is returned or charged (free `older-copy` row), and the memory is left as it was. A game whose number of bets went down since last time is treated the same way.

### Pricing

- **$0.05 per run** that read at least one usable page (charged once per run as `actor-start`, before the first game row).
- **$0.005 per game row** returned (`game-split`, $5 per 1,000).
- Free rows are never charged: a page that could not be read, a challenge, an empty page, an older copy, filters that matched nothing, no change since the last run, and a run that reached a limit.
- The start fee is charged only when a page listed at least one game this Actor can return (a game with percentages, or any game when "Include games without percentages" is on). A run that read no usable page, or only games without percentages, is not charged at all. A run whose own filters match nothing, and a monitoring run in which nothing changed, are charged the start fee: the pages were read and compared.
- A run that has used 600 seconds does not request the remaining leagues (free rows say so), so it is never cut off by the default 900-second timeout after charging.

Example: the NFL page with 16 games costs $0.05 + 16 x $0.005 = $0.13. A monitoring run in which nothing changed costs $0.05.

### Input

| Field | What it does |
|---|---|
| `leagues` | Pages to read: `nfl`, `ncaaf`, `mlb`, `nhl`, `nba`, `wnba`, `ncaab`. Empty with monitoring off = the NFL page (the `league` column says so); monitoring needs at least one league |
| `teams` | Only games with these teams: an exact abbreviation (`GB`, `CHI`) or a word of 4+ letters in the team name (`Packers`) |
| `onlyNotStarted` | Only games whose status is `scheduled` |
| `startsWithinHours` | Only games starting within this many hours (games already started are kept) |
| `includeGamesWithoutSplits` | Also return games that carry lines but no percentages yet (off by default) |
| `minMoneyVsTicketsGap` | Only games where money % and tickets % differ by at least this many points on some side |
| `maxGames` | Maximum game rows per run (default 500) |
| `monitoringMode` | Return only games that changed since the last run with the same input |
| `resetMonitoringState` | Forget what was returned for this input, so every game is new again |
| `minSplitMove` | Monitoring only: return a changed game only after a percentage moved at least this many points |

### Input example

```json
{
    "leagues": ["nfl", "ncaaf"],
    "onlyNotStarted": true,
    "monitoringMode": true,
    "minSplitMove": 3
}
```

### Output example (shortened)

```json
{
    "league": "nfl",
    "gameId": "290887",
    "startTime": "2026-09-29T00:15:00.000Z",
    "gameStatus": "scheduled",
    "awayTeam": "Philadelphia Eagles",
    "homeTeam": "Chicago Bears",
    "numBets": 45584,
    "mlAwayTicketsPct": 93,
    "mlAwayMoneyPct": 93,
    "spreadAwayTicketsPct": 78,
    "spreadAwayMoneyPct": 72,
    "overTicketsPct": 52,
    "overMoneyPct": 47,
    "consensusSpreadHome": 3.5,
    "consensusTotal": 41.5,
    "books": [{ "bookName": "DK NJ", "spreadHome": 3.5, "spreadHomeOdds": -108, "total": 41.5 }],
    "snapshotAt": "2026-09-28T12:16:19.000Z",
    "copyAgeSeconds": 347
}
```

### Limits (read before scheduling)

- **Current slate only.** The Actor reads each league's public betting page without query parameters, as Action Network's robots.txt allows. It does not read past dates and does not use Action Network's API host, whose robots.txt disallows all crawling.
- **US residential connection only.** Action Network answers datacenter addresses with a challenge after a few requests, so this Actor always reads through Apify's US residential proxy and never switches connections after a challenge. If the residential proxy is not available to a run, it stops with a free row.
- **Challenges are not solved.** If Action Network answers with a challenge (for example an AWS WAF page), the run stops asking: that league and the remaining ones come back as free rows. On 2026-09-28 the NCAAB page (out of season) answered one request from a US residential address with a challenge.
- The percentages are Action Network's own figures as published on the page; this Actor does not verify them against the sportsbooks.
- Pro-only data on Action Network (for example sharp report or detailed line history) is not included.

### FAQ

**Is the "money %" the same at every sportsbook?** On the pages we measured, yes - the page repeats the same split under each sportsbook, so it is returned once per game.

**Why are some spread percentages empty?** Action Network shows no percentage for those markets (for example most college football spreads and the MLB run line on 2026-09-28). An empty value means "not shown", not 0%.

**How often should I schedule monitoring?** Action Network refreshes the cached page about every 5 minutes at best, so running more often than every 5 minutes mostly returns `no-change` rows while still costing the start fee.

# Actor input Schema

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

Which Action Network public betting pages to read (one page per league, read in this order). Each page lists that league's current slate - today's or this week's games, as Action Network shows them. If you leave this empty and monitoring is off, the NFL page is read and every row says so in the league column; monitoring needs at least one league.

## `teams` (type: `array`):

Keep a game only if one of its teams matches: an exact abbreviation as Action Network writes it (GB, CHI, NYY), or a word of 4 letters or more inside the full team name (Packers, Yankees). Case-insensitive. Leave empty for every game.

## `onlyNotStarted` (type: `boolean`):

Keep only games whose status on the page is 'scheduled'. The pages also list games in progress and games that ended earlier in the week, with their final percentages.

## `startsWithinHours` (type: `integer`):

Keep only games that start within this many hours from now (games that already started are kept). 0 = no limit.

## `includeGamesWithoutSplits` (type: `boolean`):

Some games carry lines but no betting percentages yet (for example pre-season games or games added shortly before the page was read). Off = those games are not returned or charged; a free row says how many there were. On = they are returned with the percentage columns empty.

## `minMoneyVsTicketsGap` (type: `integer`):

Keep a game only if, for the moneyline, the spread or the total, the share of money differs from the share of tickets by at least this many percentage points on some side (large gaps mean fewer but bigger bets on that side). 0 = no limit.

## `maxGames` (type: `integer`):

How many game rows this run may return. When more games match, a free row says how many were left; in monitoring mode those games are not remembered, so the next run offers them again.

## `monitoringMode` (type: `boolean`):

Off = return every game on the page now. On = remember what was returned for this input and, on later runs, return a game again only when one of its 12 percentages, its consensus spread or total, its consensus odds or its status changed - with the previous values and the size of the biggest percentage move in the same row. The number of bets alone changes on every read and is not treated as a change. A copy of the page older than the one compared last time is not treated as a change either.

## `resetMonitoringState` (type: `boolean`):

Clear the memory for this exact input before the run, so every game counts as new again.

## `minSplitMove` (type: `integer`):

Monitoring mode only. Return a changed game only if one of its ticket or money percentages moved at least this many points since it was last returned (games seen for the first time and games whose consensus spread or total moved are always returned). Smaller moves are not remembered, so they keep adding up until they pass the minimum. 0 = any change counts.

## Actor input object example

```json
{
  "leagues": [
    "nfl"
  ],
  "onlyNotStarted": false,
  "startsWithinHours": 0,
  "includeGamesWithoutSplits": false,
  "minMoneyVsTicketsGap": 0,
  "maxGames": 500,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "minSplitMove": 0
}
```

# Actor output Schema

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

One row per game on Action Network's public betting page: league, start time, status, teams, number of bets tracked, and for moneyline, spread and total the share of tickets and the share of money on each side, the gap between them, the consensus line and odds, the opening line, and each sportsbook's line and odds. Every row says when the copy of the page it came from was made. In monitoring mode each row also carries the previous percentages, the previous consensus line and what changed. Pages that could not be read, challenges, empty pages, older copies, filters that matched nothing, runs with no change and runs that hit a limit come back as their own rows and are not charged.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/action-network-betting-splits").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"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/action-network-betting-splits").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"
  ]
}' |
apify call neverempty/action-network-betting-splits --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/action-network-betting-splits"
        }
    }
}
```

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/akgFMCJyWFsb9RqwG/builds/u0KuOCofUbHkB1b3d/openapi.json
