# PrizePicks Player Props Scraper - Lines & Line Moves (`neverempty/prizepicks-player-props-scraper`) Actor

For DFS and betting models: every PrizePicks player prop as a row - player, team, stat, line, standard/demon/goblin, over-only or both sides, start time. 9,455 lines in 23 leagues in one request (2026-09-28). Monitoring returns only lines that moved, with the earlier value.

- **URL**: https://apify.com/neverempty/prizepicks-player-props-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Developer tools, Automation
- **Stats:** 4 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.07 / 1,000 line 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

## PrizePicks Player Props Scraper - Lines & Line Moves

For DFS and betting models: every PrizePicks player prop as one row - player, team, stat, period, the line itself, whether it is a standard, demon or goblin line, whether it is over-only or offers both sides, and the game start time. On 2026-09-28 a single request returned **9,455 lines across 23 leagues**. Turn monitoring on and later runs return only the lines that have actually changed, with the previous value, the first value ever seen and both differences in the same row - so you are not charged again for a line that has not moved.

Export as JSON, CSV or Excel.

### Why monitoring matters here

Most of PrizePicks' board does not move between runs. Measured by reading the whole board twice, **15 minutes apart**, on 2026-09-28:

| | lines |
|---|---|
| on the board the first time | 9,381 |
| on the board the second time | 9,455 |
| changed (line, odds type, sides, status, flash-sale line or start time) | 35 |
| - of which the line itself moved | 15 |
| - of which the start time moved | 14 |
| newly listed | 105 |
| **charged in monitoring mode** | **140 (1.5%)** |
| lines that had been taken down | 31 |

A line keeps its PrizePicks id when its number moves (for example Rush + Rec Yards 169.5 to 179.5 on the same id), so a moved line comes back as a change with `previousLine` and `lineMove`, not as an unrelated new row.

Both reads are in this repository as `test/fixtures/board-a-summary.json` and `board-b-summary.json` (the id and the watched values of every line), and `npm test` recounts every number above. The 94-line and 97-line samples in `test/fixtures/board-a.json` and `board-b.json` are cut from the same two reads.

How much moves depends on the gap between runs, and one measurement is one measurement. Set the run's maximum total charge if you want a hard ceiling rather than trusting a rate.

### What you get

One row per line. Columns:

- **Identity** - `projectionId` (PrizePicks' id; stable across runs and across line moves - use it to join runs), `groupKey` (shared by a line and its alternates)
- **League** - `league` (PrizePicks' league name, e.g. `NFL`, `NFL1H`, `SOCCER`), `leagueId`, `leagueCode`
- **Player** - `playerId`, `playerName`, `isCombo` (two or more players added up), `position`, `jerseyNumber`, `team`, `teamName`, `teamMarket`
- **Game** - `description` (PrizePicks' own text: usually the opponent, but for season props it is the season, and for esports it can be the opponent plus the map), `eventType`, `gameId`, `gameExternalId`, `gameStartsAt` (UTC), `gameStatus`, `homeTeam`, `awayTeam`
- **The prop** - `stat`, `statDisplayName`, `statTypeId`, `period` (`Full`, `1st Half`, `1Q`, `MAP 3`...), `projectionType`, `line`, `oddsType` (`standard` / `demon` / `goblin`), `allowedWagerTypes`, `overOnly`, `flashSaleLine`, `isPromo`, `isLive`, `lineStatus`, `adjustedOdds`, `trendingCount`, `boardTime`, `lineUpdatedAt`
- **Monitoring only** - `isNewSinceLastRun`, `changeKinds` (`new` / `line` / `odds-type` / `sides` / `status` / `flash-sale` / `start-time`), `previousLine`, `lineMove`, `firstSeenLine`, `lineMoveSinceFirstSeen`, `previousOddsType`, `previousAllowedWagerTypes`, `previousLineStatus`, `previousGameStartsAt`, `firstSeenAt`, `previouslySeenAt`

#### Standard, demon and goblin

PrizePicks lists the same stat at different difficulty levels. On 2026-09-28: 6,525 demon, 1,646 goblin and 1,284 standard lines of 9,455. Filter them with `oddsTypes`.

#### Over-only or both sides

`allowedWagerTypes` is PrizePicks' own field: `over` on 6,756 lines, `under_or_over` on 1,408, and **missing on 1,291**. `overOnly` is `true`, `false`, or empty when PrizePicks does not say - it is never guessed.

### Leagues

Use PrizePicks' own league names: `NFL`, `NBA`, `MLB`, `WNBA`, `NHL`, `SOCCER`, `TENNIS`, `CS2`, `VAL`, `Dota2`, `R6`, `DARTS`, and the period and season boards such as `NFL1H`, `NFL1Q`, `WNBA1Q`, `NHL1P`, `NBASZN`, `NHLSZN`. Each name is checked against PrizePicks' league list (138 leagues, 24 with lines on 2026-09-28) before anything else is asked, because PrizePicks answers an unknown league with an empty list that looks exactly like a league with no lines today. A name PrizePicks does not have comes back as a free row that lists the leagues with lines right now.

Counts on 2026-09-28: SOCCER 3,210 / MLB 1,787 / WNBA 1,651 / NFL 905 / NBASZN 557 / NBA 193, and 17 smaller boards. `NFL` is the full game only; first-half and first-quarter props are the separate `NFL1H` and `NFL1Q` boards. **Leave `leagues` empty and you read every league on the board** - but a run returns at most `maxLines` rows (default 1,000), and the whole board was 9,455 to 9,636 lines on 2026-09-28, so raise `maxLines` (up to 20,000) to receive all of it in one run. PrizePicks decides the order, so the first 1,000 lines are not spread evenly across leagues.

### Input

Everything is optional. With an empty input the whole board comes back, up to `maxLines`.

| field | what it does |
|---|---|
| `leagues` | read only these PrizePicks leagues. Empty = every league |
| `players` | keep a line only if the player's name contains one of these (partial, case-insensitive) |
| `stats` | keep a line only if the stat contains one of these |
| `teams` | keep a line only if the team, team name, market, description or home/away team contains one of these |
| `oddsTypes` | keep only `standard`, `demon` and/or `goblin` lines |
| `onlyBothSides` | keep only lines PrizePicks marks `under_or_over` |
| `excludeLiveLines` | drop lines PrizePicks marks as live |
| `excludeCombos` | drop lines that add up two or more players |
| `startsWithinHours` | only games starting within this many hours. 0 = no limit |
| `keywords` / `keywordMatch` / `excludeKeywords` | keep or drop on any of the text columns; `keywordMatch` is `any` or `all` |
| `maxLines` | how many rows this run may return (default 1,000) |
| `monitoringMode` | return only lines that changed since the last run with this same input |
| `resetMonitoringState` | forget what was already returned, so everything counts as new again |
| `onlyLineMoves` | monitoring only: drop changes where only the odds type, sides, status or start time changed |
| `minLineMove` | monitoring only: the line must have moved at least this far |

Filtering happens after the board is read, so it never costs an extra request. Filters keep rows whose value is unknown rather than dropping them, because unknown is not the same as small - with two exceptions: `teams` drops rows with no team information at all, and `onlyBothSides` drops rows where PrizePicks does not say, because naming a team or asking for both sides means you do not want guesses.

### Honest limits

- **US residential addresses only.** PrizePicks answers datacentre addresses with HTTP 403 ("PrizePicks is currently unavailable in your location"), so every request goes through Apify's US residential proxy. PrizePicks also decides by state, so a refused request is asked again through another US residential address, up to four times. If PrizePicks answers with a browser check page instead of data, the run stops at once and does not try to get around it.
- **Pre-game board as PrizePicks publishes it.** `lineStatus` was `pre_game` on all 9,455 lines; 18 of them were also marked `isLive`. Results and scores are not included.
- **`description` is not always the opponent.** It is PrizePicks' own text: `CHI` for an NFL player, the other player for tennis, `2026-2027 Season` for season props, `Nemiga MAP 3` for esports. It is returned as written and never relabelled as an opponent. `homeTeam` and `awayTeam` are filled in only when PrizePicks attaches them to the game.
- **Some values jump around by themselves.** `trendingCount`, the board position and `lineUpdatedAt` change within seconds without the line changing, so they are returned but never count as a change. Two reads of the whole board made at the same moment returned the same watched values on all 9,455 lines, while those jumping values changed on hundreds of lines within the same minute.
- **Paging.** PrizePicks returns a whole league in one answer and ignores page settings (asking for 50 per page still returned all 193 NBA lines). If it ever answers with more than one page, the run follows the pages and stops as soon as a page brings no new line, so the same line is never charged twice.
- **Monitoring memory is per input.** Two schedules with different filters keep separate memories. If two runs with the *same* input finish at the same instant, one can overwrite the other's memory - Apify key-value stores have no atomic update, so this cannot be fully prevented. The run says so in its log when it happens. Do not put the same input in two schedules that overlap.
- **A changed line that was not returned is not remembered.** If a run hits "Maximum lines to return" or the run's maximum total charge, or if a line changed but has not yet moved far enough for `minLineMove` / `onlyLineMoves`, its earlier values are deliberately kept. So a line that moves 0.5 twice does reach a `minLineMove` of 1 - the movement adds up instead of being reset each run.
- **A changed line your other filters exclude *is* remembered, at its new value.** Leagues, players, stats, teams and odds types are about which lines you want at all, so widening one of them later does not dump the whole board on you as new.
- **`startsWithinHours` is different: lines outside the window are not remembered.** The window moves by itself as time passes, so a line whose game is still too far away is offered as new on the first run in which its game enters the window, instead of being hidden until its number happens to move.
- This is not advice about betting or investing. It returns what PrizePicks publishes, nothing more.

### Rows that are never charged

Every run that returns nothing, or not everything, says why, in a free row:

| `status` | meaning |
|---|---|
| `no-results` | PrizePicks answered correctly and listed no lines |
| `no-filter-match` | lines were read, the filters removed all of them |
| `no-line-move` | monitoring mode: nothing you asked for has changed |
| `limit-reached` | more lines matched than "Maximum lines to return" |
| `charge-limit-reached` | the run's maximum total charge was reached before all matching lines were returned |
| `partial-board` | some of the leagues you asked for could not be read; the others were returned |
| `blocked` | PrizePicks refused the request (403 / 429 / 503) every time it was asked |
| `challenge-page` | PrizePicks answered with a browser check page; the run stopped without getting around it |
| `unreadable` | the answer could not be read, or its shape changed |
| `unsupported-input` | the input asks for something that does not exist (a league PrizePicks does not have, an unknown odds type) |
| `monitoring-memory-unavailable` | monitoring mode could not read its memory for this input, so the run stopped before asking PrizePicks instead of charging every line again as new |

`no-results`, `no-line-move` and `blocked` are deliberately different things. A refused request is never reported as "there are no lines".

### Pricing

Two charges, and nothing else:

| event | price |
|---|---|
| `actor-start` - once per run in which PrizePicks answered with at least one line | $0.04 (less on paid Apify plans) |
| `prop-scraped` - per line returned | $0.10 per 1,000 (less on paid Apify plans) |

Rows that explain why nothing came back are always free. A run is charged **nothing at all, not even the start**, when PrizePicks refused it, showed a browser check page, listed no lines, or when it was stopped before reading any line (a league PrizePicks does not have, an unknown odds type, monitoring memory that could not be read). A run whose maximum total charge is below the start price returns no line and is not charged; the same applies when the maximum covers the start but not one line on top of it. Filters that simply match nothing do not make a run free - the board was read, so the $0.04 start applies.

The whole board (9,455 lines) costs about $0.99 per run; one league such as NBA (193 lines) about $0.06. In monitoring mode only the lines that changed are charged; there is no separate per-check fee. A run where nothing moved therefore costs $0.04 and returns one free row saying so. Every run is a start, so a schedule every 15 minutes is about 2,880 runs a month - decide the schedule with that in mind, and set the run's maximum total charge if you want a hard ceiling.

### How it reads PrizePicks

`https://partner-api.prizepicks.com/projections` (one request per league, or one for the whole board), plus `https://partner-api.prizepicks.com/leagues` when you name leagues. No login. `https://partner-api.prizepicks.com/robots.txt` answered 404 (no rules) and `https://www.prizepicks.com/robots.txt` is `User-Agent: *` with an empty `Disallow:` when checked on 2026-09-28. The whole board is about 11.5 MB of JSON (about 514 KB over the wire with gzip).

### Unofficial. Public data only.

Not affiliated with, endorsed by or connected to PrizePicks or any sportsbook. It reads only what PrizePicks publishes without a login, and returns no personal data: the people in the output are professional athletes, and the columns are their public lines.

# Actor input Schema

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

PrizePicks league names, as PrizePicks writes them - for example NFL, NBA, MLB, WNBA, NHL, SOCCER, TENNIS, CS2, VAL, NFL1H, NFL1Q, NBASZN. Case does not matter. Leave empty for every league on the board. Each name is checked against PrizePicks' own league list before anything is asked; a name PrizePicks does not have comes back as a free row that lists the leagues with lines right now. On 2026-09-28, 24 of PrizePicks' 138 leagues carried lines.

## `players` (type: `array`):

Keep a line only if the player's name contains one of these (case-insensitive, partial). Example: Saquon Barkley, Hurts.

## `stats` (type: `array`):

Keep a line only if the stat contains one of these (case-insensitive, partial). Examples: Pass Yards, Anytime TDs, Shots On Target, Total Bases, Pts+Rebs+Asts.

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

Keep a line only if the player's team, team name or market, PrizePicks' description (usually the opponent) or the game's home or away team contains one of these (case-insensitive, partial). Examples: PHI, Eagles, Dallas. Lines with no team information at all are dropped when this is set.

## `oddsTypes` (type: `array`):

standard, demon (harder to hit, higher payout) or goblin (easier to hit, lower payout). Leave empty for all three. On 2026-09-28: 6,525 demon, 1,646 goblin and 1,284 standard of 9,455 lines.

## `onlyBothSides` (type: `boolean`):

Keep only lines PrizePicks marks as under_or_over. Lines marked over-only are dropped, and so are lines where PrizePicks does not say (1,291 of 9,455 on 2026-09-28), because unknown is not the same as both sides.

## `excludeLiveLines` (type: `boolean`):

Drop lines PrizePicks marks as live (18 of 9,455 on 2026-09-28).

## `excludeCombos` (type: `boolean`):

Drop lines that add up two or more players (for example 'Jalen Hurts + Case Keenum'; 75 of 9,455 on 2026-09-28).

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

Keep a line only if its game starts within this many hours from now. 0 = no limit. In monitoring mode, lines outside the window are not remembered, so a line is offered as new on the first run in which its game enters the window. Lines with no start time are kept.

## `maxLines` (type: `integer`):

How many rows this run may return and charge for. The whole board was 9,455 to 9,636 lines on 2026-09-28, so raise this to receive all of it. When more lines match, a free row says how many were left; in monitoring mode the lines that were not returned are not remembered, so they are offered again next time.

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

Off = return the board as it stands now. On = remember the line, odds type, sides, status, flash-sale line and start time already returned for this input and, on later runs, return a line again only when one of them has actually changed - with the previous value, the first value ever seen and the difference in the same row. Measured on 2026-09-28: 15 minutes apart, 140 of 9,455 lines (1.5%) were new or had changed.

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

Clear the remembered values for this exact input before the run, so every line counts as new again.

## `onlyLineMoves` (type: `boolean`):

Monitoring mode only. Drop changes where only the odds type, sides, status or start time changed, and keep the ones where the line itself moved (for example 169.5 to 179.5). Lines seen for the first time are kept. A change held back this way is not recorded as delivered, so the line comes back when its number does move.

## `minLineMove` (type: `number`):

Monitoring mode only. Keep a line only if it has moved at least this far from the value returned last time, in either direction. 0 = no limit. Lines with no previous value are kept. A line held back because it has not moved far enough is not recorded as delivered, so its movement keeps adding up until it passes this number.

## `keywords` (type: `array`):

Keep a line only if one (Any) or all (All) of these appear in the player, team, description, stat, league, position, period or home/away team.

## `keywordMatch` (type: `string`):

Any = at least one keyword. All = every keyword.

## `excludeKeywords` (type: `array`):

Drop a line if any of these appear in the same fields.

## Actor input object example

```json
{
  "leagues": [
    "NFL"
  ],
  "onlyBothSides": false,
  "excludeLiveLines": false,
  "excludeCombos": false,
  "startsWithinHours": 0,
  "maxLines": 1000,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "onlyLineMoves": false,
  "minLineMove": 0,
  "keywordMatch": "any"
}
```

# Actor output Schema

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

One row per PrizePicks player prop line: the league, the player with PrizePicks' id, position, team and market, PrizePicks' description (usually the opponent), the game with its start time and, when PrizePicks gives them, the home and away teams, the stat and period, the line itself, the odds type (standard, demon or goblin), whether the line is over-only or offers both sides, flash-sale and live flags and the trending count. In monitoring mode each row also carries the value returned last time, the first value ever seen for that line, both differences, and which of line, odds type, sides, status or start time changed. An empty league, a refused request, a browser check page, a league that does not exist, filters that matched nothing, a run with no change, a partly read board and a run 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/prizepicks-player-props-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"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/prizepicks-player-props-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"
  ]
}' |
apify call neverempty/prizepicks-player-props-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/prizepicks-player-props-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/cMDTVV8HCkMSKZ0ia/builds/WBI4r0N6AJqXYx5L5/openapi.json
