# Underdog Fantasy Scraper — Player Props, Lines & Multipliers (`logiover/underdog-fantasy-scraper`) Actor

Underdog Fantasy scraper: export the live Pick'em board — every player prop, one row per line with Higher/Lower payout multipliers, American and decimal prices, implied probability, player, team, matchup and start time; alternate ladders optional. NFL, CFB, NBA, NHL, soccer, tennis, esports.

- **URL**: https://apify.com/logiover/underdog-fantasy-scraper.md
- **Developed by:** [Logiover](https://apify.com/logiover) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.15 / 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.
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

## Underdog Fantasy Scraper — Player Props, Lines, Alternate Lines & Payout Multipliers

**Export the live Underdog Fantasy Pick'em board — every player prop with the payout multipliers, American and decimal prices and implied probabilities of its Higher and Lower side, player, team, matchup, start time and league, and on request the full ladder of alternate lines — as clean JSON, CSV or Excel. One row per line, both sides in the row. No login, no API key, no browser.**

> Independent tool. Not affiliated with, endorsed by, or sponsored by Underdog, Underdog Sports or any league, team or player named in the data. "Underdog" and "Underdog Fantasy" are trademarks of their owners and are used here only to describe the public website this Actor reads. This Actor only reads the lines Underdog serves to every anonymous visitor of its web app: it does not log in, does not use an account, does not place or automate entries, and does not offer betting advice.

**Underdog Fantasy Scraper** is an Underdog Fantasy API alternative and Underdog Pick'em scraper that turns the public board behind underdogfantasy.com and app.underdogsports.com into a flat table you can sort, filter, join and chart. Every row is one **line** — "Points 24.5" — with the **payout multipliers**, **American, decimal and fractional odds** and **implied probabilities** of both its **Higher (over)** and **Lower (under)** side, the **player** (name, team, position, jersey number, country, headshot), the **matchup** (home and away team, venue, season type, broadcast), the **start time in ISO 8601 UTC**, the **league** (NFL, college football, NBA, NHL, WNBA, soccer, tennis, MMA, golf, NASCAR, Counter-Strike, League of Legends, Dota 2, Valorant …) and the **stat** (points, rebounds, passing yards, shots on goal, kills on maps 1+2, round strokes …). By default you get the main line of every prop; switch `includeAlternates` on and it also reads the **alternate-line ladder** of every prop that has one — the 19.5, 23.5, 29.5, 34.5 and 39.5 next to a 24.5 — each with its own multipliers and prices.

Use it as an **Underdog player props API**, an **Underdog odds scraper**, a **pick'em line-shopping feed** next to PrizePicks or a sportsbook, a **props database for models and backtests**, a **line-movement and closing-line-value tracker** when you schedule it, or a **Discord / Telegram alert source**. Export CSV, Excel, JSON or XML; call it from the Apify API, Make, Zapier, n8n, Google Sheets or an AI agent. Field names follow our PrizePicks and Sleeper Scrapers (`playerName`, `playerTeam`, `playerPosition`, `league`, `statType`, `lineScore`, `startTime`, `projectionId`, `matchup`, `overMultiplier` / `underMultiplier`), so the datasets join on player, stat and line. Prefer one row per side? Set `rowShape` to `side`.

### Table of contents

- [At a glance](#at-a-glance)
- [What you get](#what-you-get)
- [Who uses it — 14 workflows](#who-uses-it--14-workflows)
- [Cookbook — 15 ready-to-run inputs](#cookbook--15-ready-to-run-inputs)
- [Tutorial: your first run in the Apify Console](#tutorial-your-first-run-in-the-apify-console)
- [Tutorial: run it from the API](#tutorial-run-it-from-the-api)
- [Input reference](#input-reference)
- [Output data dictionary](#output-data-dictionary)
- [Real output sample](#real-output-sample)
- [How to read a row](#how-to-read-a-row)
- [Scale and speed (measured)](#scale-and-speed-measured)
- [What you pay](#what-you-pay)
- [Integrations](#integrations)
- [Performance tips](#performance-tips)
- [Troubleshooting: every status message explained](#troubleshooting-every-status-message-explained)
- [Honest limits](#honest-limits)
- [How it compares](#how-it-compares)
- [FAQ](#faq)
- [Glossary](#glossary)
- [Is it legal to scrape Underdog?](#is-it-legal-to-scrape-underdog)
- [Responsible use](#responsible-use)
- [Changelog](#changelog)
- [Support](#support)

### At a glance

| | |
|---|---|
| **Source** | The JSON Underdog's own web app loads: the Pick'em board (`/v1/over_under_lines`) and the alternate-line ladder of each prop (`/v3/over_unders/<id>/alternate_projections`). No browser, no login. |
| **Row** | One line with both sides in it (`over*` = Higher, `under*` = Lower) — 73 columns, flat, typed, every field nullable. `rowShape: "side"` gives one row per side instead. |
| **Board on 2026-10-29** | 9,744 main lines on 15 leagues (13,749 sides); 3,794 props carry an alternate-line ladder. |
| **Default run (main lines)** | 9,740 rows in 10 seconds; $2.44 at the free tier, $1.46 on Gold and above. |
| **With alternate lines** | Opt-in (`includeAlternates: true`). Biggest verified run: 24,995 unique line rows (9,744 main + 15,251 alternate lines, 35,473 sides), 0 duplicates, 6.6 minutes, 2026-10-29. |
| **Prefill run** | 300 rows in 9 seconds, first rows after 8 seconds. |
| **Odds formats** | American, decimal, fractional, implied and no-vig probability and payout multiplier — both sides, every row. |
| **Price** | Pay per result — from $0.25 per 1,000 rows, down to $0.15 per 1,000 on higher Store tiers. No start fee, no monthly rent. |

### What you get

Every row carries the context of its line, so one row is a complete selection. The columns come in eight groups; the full data dictionary with measured fill rates is further down.

- **Identity** — `rowId` (`underdog:line:<projectionId>`), `recordType`, `platform`, `lineKey` (the market: `<league>:<gameId>:<playerId>:<statKey>`), `projectionId` (the line), `overUnderId` (Underdog's prop id), `appearanceId`, `playerId`, `gameId`, `matchType`.
- **Player** — `playerName`, `playerTeam`, `playerTeamName`, `playerPosition`, `playerPositionName`, `jerseyNumber`, `playerCountry`, `playerImageUrl`, `isHome`.
- **Event** — `league`, `sportName`, `competition`, `gameTitle`, `matchup` (`AWAY @ HOME`), `opponent`, `homeTeam` / `awayTeam` and their full names, `homePlayerName` / `awayPlayerName`, `startTime`, `gameStatus`, `seasonType`, `venueName`, `venueType`, `broadcasts`.
- **Market and line** — `statType`, `statKey`, `propTitle`, `lineScore` (the number to beat), `mainLineScore`, `isAlternate`, `hasAlternates`, `lineProvider`, `sidesOffered`.
- **Both sides** — for `over` (Higher) and `under` (Lower): `…LineId`, `…Multiplier`, `…PayoutTier`, `…OddsAmerican`, `…OddsDecimal`, `…OddsFractional`, `…ImpliedProbability`, `…NoVigProbability`, the separate `…Fantasy…` price set and `…Status`, plus `marketOverround`.
- **Meta** — `updatedAt` (when Underdog last changed the line) and `scrapedAt`.
- **Side shape only** — with `rowShape: "side"` each side is its own row and carries `side`, `lineId`, `oppositeLineId`, `multiplier`, `payoutTier`, `oddsAmerican`, `impliedProbability` … (listed last in the data dictionary).

A real line from a run on 2026-10-29 is shown unmasked in [Real output sample](#real-output-sample) below.

### Who uses it — 14 workflows

#### 1. Pick'em players and line shoppers

Sort tonight's board by `statType` and `lineScore`, compare Underdog's number with the same player elsewhere, and use `overPayoutTier` / `underPayoutTier` to find boosted sides. With `includeAlternates` on, the ladder shows what the 19.5 and the 29.5 pay before you commit to the 24.5.

#### 2. Sports bettors comparing sportsbook props with pick'em lines

Join the export with the props you already collect on `playerName` + `statType` + `lineScore` and compare the implied and no-vig probabilities, computed the same way on every row.

#### 3. DFS content creators and newsletters

A daily "best boosted picks" post is a filter: `payoutTiers: ["boosted"]`, `minPayoutMultiplier: 1.15`, `startsWithinHours: 24`. Schedule it, push the dataset to a sheet; team names and headshot URLs are in the row.

#### 4. Sports analytics teams and modellers

Schedule the Actor, keep every run, and key rows by `rowId` (a line) and `lineKey` (a prop): line movement, multiplier drift and the moment a side turns `suspended` become time series.

#### 5. Odds comparison sites and aggregators

One row per line with both sides' American, decimal and fractional odds maps straight onto an odds-table schema; `league` + `startTime` + `matchup` place the event on your fixture list.

#### 6. Discord, Telegram and Slack alert bots

Run every few minutes with a narrow filter, diff against the previous run by `rowId`, and post when `lineScore` or a multiplier changes. A webhook on run success triggers the bot.

#### 7. Sports-data journalists

When a star is ruled out the boards react within minutes: pull a player's rows before and after and quote the movement with `updatedAt`.

#### 8. Fantasy-sports and prediction start-ups

Seed a product with real market lines: projection baselines, difficulty ratings or a "what the market expects" widget. The main `lineScore` is Underdog's market estimate; with alternates on, the ladder maps the distribution around it.

#### 9. Quantitative researchers

With `includeAlternates` on, the ladder is a discrete price curve: multiplier and price at 19.5, 23.5, 24.5, 29.5, 34.5, 39.5 for one stat. Fit a curve per prop or look for ladders whose neighbouring thresholds are mispriced.

#### 10. Esports analysts

Counter-Strike, League of Legends, Dota 2 and Valorant props (kills on maps 1+2, assists, deaths) share the board and the columns; a team's whole slate is one `teams` filter.

#### 11. Tennis, MMA, golf and NASCAR analysts

Individual-sport props (games played, aces, significant strikes, round strokes) come with the event in `gameTitle`, the tournament in `competition`, and `opponent` / `homePlayerName` / `awayPlayerName` for one-on-one events.

#### 12. Academic and market-efficiency researchers

A scheduled collection of a small market over a season is a dataset few papers have; flat, typed rows with a stable `rowId` suit studies of line efficiency and favourite–longshot bias.

#### 13. Risk, pricing and competitive-intelligence teams

See how a competitor prices the pairs you offer: group by `league` and `statType`, read the margin from `marketOverround`, measure how often lines move.

#### 14. AI agents, RAG and LLM pipelines

A small JSON input, a flat output and a SUMMARY record make a clean agent tool: "which boosted sides start in the next three hours?" is one call with `payoutTiers` and `startsWithinHours`.

### Cookbook — 15 ready-to-run inputs

Every input below is valid as written. Leave a field out to leave that filter off. Rows are returned soonest match first, so a small `maxItems` always gives you "what is coming up next".

#### 1. A quick look (what the Console pre-fills)

```json
{ "maxItems": 300 }
```

The soonest 300 rows of the whole board, all leagues: one request for the board, finished in seconds — the input the Store's daily health check uses.

#### 2. The whole live board — main lines (the default)

```json
{}
```

Every league that has lines right now, the main line of every prop: 9,740 rows in about 10 seconds on 2026-10-29. Raise "Maximum cost per run" if you want to be sure of the ceiling.

#### 3. Alternate lines too — the deep ladders

```json
{ "includeAlternates": true }
```

One more request per prop: 24,995 rows on 2026-10-29 (15,251 of them alternates), about 6.6 minutes. Narrow it with filters to make it quick and cheap.

#### 4. One league: NFL player props

```json
{ "leagues": ["NFL"] }
```

League codes are not case-sensitive; a few names work as aliases (`soccer` → `FIFA`, `golf` → `PGA`, `valorant` → `VAL`). With one to eight leagues the Actor downloads just those leagues.

#### 5. Tonight's NBA board: points, rebounds, assists

```json
{ "leagues": ["NBA"], "statTypes": ["Points", "Rebounds", "Assists"], "startsWithinHours": 24 }
```

`statTypes` matches the stat name, key or prop title.

#### 6. One player, everything Underdog offers

```json
{ "playerNames": ["Jokic"] }
```

Case-insensitive, accents ignored, partial names allowed — `Jokic` finds Jokić. Use several names for a watch-list.

#### 7. One team's whole slate

```json
{ "leagues": ["NFL"], "teams": ["KC", "Chiefs"] }
```

`teams` matches the player's team and either side of the matchup, by abbreviation or by name.

#### 8. Boosted sides only

```json
{ "payoutTiers": ["boosted"], "minPayoutMultiplier": 1.2, "startsWithinHours": 48, "includeAlternates": true }
```

Lines where a side pays more than a standard pick, starting within two days; the row shows both sides. Alternate lines are where the large multipliers live, so this one switches them on.

#### 9. Lower (under) sides that are discounted

```json
{ "side": "lower", "payoutTiers": ["discounted"] }
```

Lines whose Lower side pays less than a standard pick (multiplier below 1.0).

#### 10. Soccer: shots on target

```json
{ "leagues": ["FIFA"], "statTypes": ["Shots On Target"] }
```

Soccer is the `FIFA` league code on Underdog; home and away are resolved for soccer matchups.

#### 11. Esports

```json
{ "leagues": ["CS", "LOL", "DOTA", "VAL"] }
```

Counter-Strike, League of Legends, Dota 2 and Valorant. `playerTeam` holds the team name.

#### 12. Individual sports: tennis, MMA, golf

```json
{ "leagues": ["TENNIS", "MMA", "PGA"] }
```

No team here: `competition` holds the tournament, `homePlayerName` / `awayPlayerName` the two sides of one-on-one events.

#### 13. A single day

```json
{ "startDateFrom": "2026-10-31", "startDateTo": "2026-10-31" }
```

Both are inclusive UTC days (`YYYY-MM-DD`); a match at 01:00 UTC on the 1st is the evening of the 31st in the US.

#### 14. Scheduled line-movement tracking

```json
{ "leagues": ["NBA"], "statTypes": ["Points"], "maxItems": 5000 }
```

Schedule it every 15 minutes (Console → Schedules), keep every run's dataset and compare runs by `rowId`: the same line has the same `rowId` each run, so `lineScore`, `overMultiplier` and `overOddsAmerican` line up row by row; rows in one run only are new or removed lines.

#### 15. One row per side

```json
{ "rowShape": "side", "leagues": ["NBA"], "maxItems": 2000 }
```

Higher and Lower as two rows (`side`, `lineId`, `oppositeLineId`, `multiplier`, `oddsAmerican`, `impliedProbability` …) — twice the rows for the same lines, for tools that expect one selection per row.

### Tutorial: your first run in the Apify Console

1. Open the Actor and press **Try for free** or **Start**. The input form is pre-filled with `maxItems = 300` and every filter empty.
2. Leave the leagues empty to read every league that has lines right now, or pick leagues from the suggestions (you can also type any league code Underdog uses).
3. Optionally narrow the board: player names, teams, stat types, side, payout tiers, a multiplier range or a start window. **Row shape** chooses one row per line (default) or per side.
4. Switch **Include alternate lines** on if you want the whole ladder (more rows, minutes instead of seconds); it is off by default.
5. Press **Start**; the pre-filled input ends in seconds.
6. Open the **Output** tab. Views: *Board*, *Prices*, *Players*, *Matches* and *Ladders* (the lines of one prop next to each other). Export as CSV, Excel, JSON or JSONL.
7. **Storage → Key-value store → SUMMARY** has the outcome, the leagues on the board, the number of alternate-line requests and the routes used.

### Tutorial: run it from the API

#### 1. Get your token

Console → **Settings → API & Integrations** → copy the personal API token. Put it in an environment variable instead of in code.

#### 2. Run and get the data in one call (small runs)

```bash
curl -X POST "https://api.apify.com/v2/acts/logiover~underdog-fantasy-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"leagues":["NBA"],"statTypes":["Points"],"maxItems":200}'
```

The response is the dataset items as JSON. Add `&format=csv` or `&format=xlsx` for a file. Synchronous calls end after five minutes: use the next pattern for big boards.

#### 3. Start a run and fetch the data later (big runs)

```bash
## start
curl -X POST "https://api.apify.com/v2/acts/logiover~underdog-fantasy-scraper/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" -d '{"maxItems":100000}'
## poll: GET https://api.apify.com/v2/actor-runs/RUN_ID?token=$APIFY_TOKEN   (status SUCCEEDED)
## data: GET https://api.apify.com/v2/datasets/DATASET_ID/items?format=csv&token=$APIFY_TOKEN
```

#### 4. Node.js (apify-client)

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('logiover/underdog-fantasy-scraper').call({
    leagues: ['NFL'], payoutTiers: ['boosted'], startsWithinHours: 72, maxItems: 2000,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const r of items.slice(0, 5)) console.log(r.playerName, r.statType, r.lineScore, `over x${r.overMultiplier}`, `under x${r.underMultiplier}`);
```

#### 5. Python (apify-client)

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("logiover/underdog-fantasy-scraper").call(run_input={"leagues": ["NBA"], "statTypes": ["Points"], "startsWithinHours": 24})
rows = client.dataset(run["defaultDatasetId"]).list_items().items
for r in sorted((r for r in rows if r["overMultiplier"]), key=lambda r: -r["overMultiplier"])[:10]:
    print(r["playerName"], r["statType"], r["lineScore"], r["overMultiplier"], r["underMultiplier"])
```

#### 6. Read the run summary

```python
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("SUMMARY")["value"]
print(summary["outcome"], summary["rowsSaved"], summary["board"]["leagues"].keys())
```

`outcome` is `COMPLETED`, `PARTIAL`, `EMPTY_RESULT`, `BLOCKED`, `FETCH_FAILED` or `INVALID_INPUT` (see [Troubleshooting](#troubleshooting-every-status-message-explained)).

### Input reference

Everything is optional. Filters combine with AND.

| Field | Type | Default | What it does |
|---|---|---|---|
| `leagues` | list of text | all live leagues | Underdog league codes: `NFL`, `CFB`, `NBA`, `NHL`, `MLB`, `WNBA`, `CBB`, `FIFA` (soccer), `TENNIS`, `MMA`, `PGA`, `MOTORSPORTS`, `CS`, `LOL`, `DOTA`, `VAL`, `CFL`, `KBO` …; not case-sensitive. A league with no lines returns nothing. |
| `playerNames` | list of text | none | Keep only these players. Partial, case-insensitive, accent-insensitive. |
| `teams` | list of text | none | Keep only props of these teams or matches of these teams (abbreviation or name). |
| `statTypes` | list of text | none | Keep only these stats (name, stat key or prop title; case-insensitive substring). |
| `rowShape` | `line` / `side` | `line` | One row per line with both sides (default) or one row per side. |
| `side` | `both` / `higher` / `lower` | `both` | Keep lines whose Higher or Lower side passes the payout filters (and, in the side shape, only that side's rows). |
| `payoutTiers` | list of `boosted` / `standard` / `discounted` | none | Keep lines where a side is in these tiers. |
| `minPayoutMultiplier` | number | none | Keep lines where a side pays at least this multiplier. |
| `maxPayoutMultiplier` | number | none | Keep lines where a side pays at most this multiplier. |
| `includeAlternates` | true / false | `false` | Also read the alternate-line ladder of every prop that has one: one more request per prop, about 2.5 times the rows. Off = main lines only (one request, seconds). |
| `startsWithinHours` | whole number | none | Keep only matches starting within this many hours from now. |
| `startDateFrom` | text `YYYY-MM-DD` | none | Keep only matches starting on or after this UTC day. |
| `startDateTo` | text `YYYY-MM-DD` | none | Keep only matches starting on or before this UTC day. |
| `maxItems` | whole number | 100,000 | Upper bound on rows saved (each row is one billed result — a line, or a side in the side shape). The Console pre-fills 300. |
| `proxyConfiguration` | proxy editor | no proxy | Optional. Without it the Actor connects directly and falls back to the Apify datacenter proxy and, for small documents, a capped residential exit. |

Filters that depend on the prop (league, player, team, stat, time window) are applied before any alternate-line request is made, so a narrow input also makes few requests. Filters that depend on the side (`side`, `payoutTiers`, multiplier range) keep a line when at least one of its sides passes all of them; in the side shape they decide which side rows are saved.

### Output data dictionary

73 columns in the default shape, every one nullable. The **Filled** column is measured on the 24,995-row verified run of 2026-10-29 (the whole board with `includeAlternates: true`); a column that is empty for some rows is empty because the source has no value for them, never because the Actor dropped it. The columns of the **Side shape** group exist only with `rowShape: "side"`.

**Identity**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `rowId` | text | 100 % | Unique key: `underdog:line:<projectionId>` (a side row: `underdog:<its option id>`). | `underdog:line:0f900…` |
| `recordType` | text | 100 % | `line` (default) or `side`. | `line` |
| `platform` | text | 100 % | Always `underdog`; a discriminator for merging with the other pick'em Actors. | `underdog` |
| `lineKey` | text | 100 % | Stable key of the market (one prop): `<league>:<gameId>:<playerId>:<statKey>`; shared by the main line and its alternates. | `TENNIS:329778:0ee77…` |
| `projectionId` | text | 100 % | Underdog's id of the line (one threshold of a prop); same name as in the PrizePicks Actor. | `0f900c6e-2edc-4c06-…` |
| `overUnderId` | text | 100 % | Underdog's own prop id (player + stat + match). | `15b9456b-f660-48c3-…` |
| `appearanceId` | text | 100 % | Underdog's id of the player's appearance in the match. | `672c2c7e-7a53-40e4-…` |
| `playerId` | text | 100 % | Underdog's player id. | `0ee77440-ce67-48a6-…` |
| `gameId` | text | 100 % | Underdog's match id (team game or individual event). | `329778` |
| `matchType` | text | 100 % | `Game` (team match) or `SoloGame` (golf, tennis, MMA, NASCAR). | `SoloGame` |

**Player**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `playerName` | text | 100 % | Player name as Underdog shows it. | `Kaitlin Quevedo` |
| `playerTeam` | text | 97.7 % | Team abbreviation (`CLE`); team name for esports; null for individual sports and unresolved matchups. | `BIG` |
| `playerTeamName` | text | 97.7 % | Full team name when Underdog gives one, else the nickname. | `BIG` |
| `playerPosition` | text | 100 % | Position code (`SG`, `QB`, `FW`, `G`, `Gamer`, `TP`). | `TP` |
| `playerPositionName` | text | 100 % | Position written out (`Shooting Guard`, `Tight End`). | `Tennis Player` |
| `jerseyNumber` | text | 94.7 % | Jersey number when Underdog lists one. | `15` |
| `playerCountry` | text | 5.7 % | ISO country code when Underdog lists one (golf, tennis, soccer, MMA). | `ESP` |
| `playerImageUrl` | text | 100 % | Headshot URL on Underdog's asset host. | `https://assets.unde…` |
| `isHome` | true / false | 99.7 % | true when the player's side is the home side (tennis / MMA: the slot Underdog calls home). | `false` |

**Event**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `league` | text | 100 % | League code as Underdog uses it (`NFL`, `CFB`, `NHL`, `FIFA` = soccer, `TENNIS`, `MMA`, `CS`, `LOL` …); use it in `leagues`. | `TENNIS` |
| `sportName` | text | 100 % | Sport / game name from the match record (`Soccer`, `CS2`, `Dota 2`). | `Tennis` |
| `competition` | text | 6.1 % | Tournament or competition when there is one (`EPL`, `ATP Vienna`). | `WTA Guangzhou` |
| `gameTitle` | text | 100 % | Match title as Underdog prints it (`CLE @ BOS`, `MCI vs BHA`). | `Bondar vs Quevedo` |
| `matchup` | text | 100 % | `AWAY @ HOME` (the PrizePicks Actor's `opponent` shape); `A vs. B` for one-on-one events. | `A. Bondar vs. K. Qu…` |
| `opponent` | text | 99.5 % | The player's opponent: a team (name for esports) or, in tennis and MMA, the other competitor (as in the Sleeper Actor). | `Anna Bondar` |
| `homeTeam` | text | 97.7 % | Home team. `AWAY @ HOME` titles are exact; `vs` titles are resolved for soccer and esports only. | `BIG` |
| `awayTeam` | text | 97.7 % | Away team, same rules as `homeTeam`. | `Eternal Fire` |
| `homeTeamName` | text | 97.7 % | Home team's full name when the match record has one. | `BIG` |
| `awayTeamName` | text | 97.7 % | Away team's full name when the match record has one. | `Eternal Fire` |
| `homePlayerName` | text | 1.9 % | Home-side competitor in one-on-one events. | `Anna Bondar` |
| `awayPlayerName` | text | 1.8 % | Away-side competitor in one-on-one events. | `Kaitlin Quevedo` |
| `startTime` | text | 100 % | Scheduled start, ISO 8601 UTC. | `2026-10-29T10:10:00…` |
| `gameStatus` | text | 100 % | `scheduled`, or `scoring` in the last minutes before the start. Started matches are not on the board. | `scheduled` |
| `seasonType` | text | 97.7 % | `regular` or `post`, for league matches. | `regular` |
| `venueName` | text | 82.5 % | Venue, when the match record lists one. | `Center Court` |
| `venueType` | text | 76.9 % | `outdoor`, `indoor` … when listed (mostly NFL / college football). | `outdoor` |
| `weatherCondition` | text | 0 % (conditional) | Forecast condition. Conditional: empty on every match when measured, so the key is written only when it has a value. | `Sunny` |
| `weatherTemperature` | number | 0 % (conditional) | Forecast temperature. Conditional, same rule as `weatherCondition`. | `61` |
| `broadcasts` | list | 94.1 % | TV / streaming channels Underdog lists for the match. | `["Peacock", "USA"]` |

**Market**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `statType` | text | 100 % | The stat as Underdog displays it (`Points`, `Pass Yards`, `Kills on Maps 1+2`). | `1st Set Aces` |
| `statKey` | text | 100 % | Underdog's machine key of the stat (`points`, `rush_rec_tds`); tells apart stats that share a display name. | `period_1_aces` |
| `propTitle` | text | 100 % | Prop title as printed on the card. | `Kaitlin Quevedo 1st…` |

**Line**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `lineScore` | number | 100 % | The threshold of this line — the number to beat; same name as in the PrizePicks Actor. | `1.5` |
| `mainLineScore` | number | 100 % | The prop's main line; equals `lineScore` on main-line rows. | `1.5` |
| `isAlternate` | true / false | 100 % | false = main-board line, true = alternate line from the prop's ladder. | `false` |
| `hasAlternates` | true / false | 100 % | true when Underdog offers a ladder of alternate lines for the prop. | `false` |
| `nonDiscountedLineScore` | number | 0 % (conditional) | Original line when Underdog discounted the line. Conditional: empty everywhere when measured, so written only when it has a value. | `25.5` |
| `lineProvider` | text | 34.8 % | Who set the main line: `swish` (model feed) or `manual`; not given for alternates. | `swish` |
| `sidesOffered` | number | 100 % | Sides Underdog offers on this line: 2, or 1 (many lines are Higher-only). | `2` |

**Sides**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `overLineId` / `underLineId` | text | 99.8 % / 42.1 % | Option id of each side (the Sleeper Actor's column name). | `067fc5be-45e2-4c7e-…` / `9bb164e2-0c61-4ff4-…` |
| `overMultiplier` / `underMultiplier` | number | 99.8 % / 42.1 % | Pick'em payout multiplier (1.0 standard, above pays more, below less). | `1.28` / `0.76` |
| `overPayoutTier` / `underPayoutTier` | text | 99.8 % / 42.1 % | `boosted` (multiplier above 1.0), `standard` (1.0) or `discounted` (below). | `boosted` / `discounted` |
| `overOddsAmerican` / `underOddsAmerican` | number | 99.8 % / 42.1 % | American price Underdog serves (the leg price; equals the sportsbook-set price on ladder documents). | `162` / `-200` |
| `overOddsDecimal` / `underOddsDecimal` | number | 99.8 % / 42.1 % | Decimal price. | `2.62` / `1.5` |
| `overOddsFractional` / `underOddsFractional` | text | 99.8 % / 42.1 % | Reduced fraction (computed from the American price). | `81/50` / `1/2` |
| `overImpliedProbability` / `underImpliedProbability` | number | 99.8 % / 42.1 % | 1 / decimal price, 4 decimals (computed; includes the margin). | `0.3817` / `0.6667` |
| `overNoVigProbability` / `underNoVigProbability` | number | 41.9 % / 41.9 % | Share of the two implied probabilities, so the pair sums to 1 (two-sided lines). | `0.3641` / `0.6359` |
| `overFantasyOddsAmerican` / `underFantasyOddsAmerican` | number | 94.2 % / 36.5 % | American price of Underdog's separate `fantasy` price set. | `135` / `-228` |
| `overFantasyOddsDecimal` / `underFantasyOddsDecimal` | number | 94.2 % / 36.5 % | Decimal price of the `fantasy` set. | `2.35` / `1.44` |
| `overFantasyProbability` / `underFantasyProbability` | number | 94.2 % / 36.5 % | Probability Underdog attaches to the `fantasy` set, as a fraction (served as whole percent). | `0.39` / `0.66` |
| `overStatus` / `underStatus` | text | 99.8 % / 42.1 % | `active` or `suspended`. | `active` / `active` |
| `marketOverround` | number | 41.9 % | Implied over + implied under − 1: the margin of a two-sided line (computed). | `0.0484` |

**Meta**

| Column | Type | Filled | Meaning | Example |
|---|---|---|---|---|
| `updatedAt` | text | 100 % | When Underdog last changed the line (later of its two sides), ISO 8601 UTC. | `2026-10-28T15:47:45…` |
| `scrapedAt` | text | 100 % | When the board was read, ISO 8601 UTC; same for every row of a run. | `2026-10-29T10:02:46…` |

**Side shape** (only with `rowShape: "side"`: each side is its own row, which carries all the line columns above plus these, with the meaning of the matching `over…` / `under…` columns for the row's own side)

`side`, `sideLabel`, `lineId`, `oppositeLineId`, `multiplier`, `oppositeMultiplier`, `payoutTier`, `oddsAmerican`, `oddsDecimal`, `oddsFractional`, `impliedProbability`, `noVigProbability`, `fantasyOddsAmerican`, `fantasyOddsDecimal`, `fantasyProbability`, `status`, `pickLabel`, `choiceShort`. `lineId` is the option id of THIS side and `oppositeLineId` that of the other side (the Sleeper Actor's convention); `side` is `over` or `under`; `pickLabel` is the full pick text (`Higher 24.5 Points`) and `choiceShort` its short form (`25+`).

### Real output sample

Two rows from the verified run of 2026-10-29 (alternate lines on), unmasked, empty columns left out: a main line and an alternate line of the same prop.

**A main line (both sides in the row)**

```json
{
 "rowId": "underdog:line:286e9f16-17a2-4479-b1cb-38eabc835a2a",
 "recordType": "line",
 "platform": "underdog",
 "lineKey": "NBA:212020:046c78d0-6297-436a-92ab-e50ef6fb2aeb:three_points_made",
 "projectionId": "286e9f16-17a2-4479-b1cb-38eabc835a2a",
 "overUnderId": "87e2370e-ed7a-4867-8202-bcf7397b4333",
 "appearanceId": "f3fecb84-1da2-42d0-840c-9fe75feaaec8",
 "playerId": "046c78d0-6297-436a-92ab-e50ef6fb2aeb",
 "gameId": "212020",
 "matchType": "Game",
 "playerName": "Aaron Wiggins",
 "playerTeam": "ATL",
 "playerTeamName": "Atlanta Hawks",
 "playerPosition": "SG",
 "playerPositionName": "Shooting Guard",
 "jerseyNumber": "2",
 "playerImageUrl": "https://assets.underdogfantasy.com/player-images/nba/20261008_21-d3b28775-473d-471a-8452-e913f4347f0f.png",
 "isHome": true,
 "league": "NBA",
 "sportName": "NBA",
 "gameTitle": "CLE @ ATL",
 "matchup": "CLE @ ATL",
 "opponent": "CLE",
 "homeTeam": "ATL",
 "awayTeam": "CLE",
 "homeTeamName": "Atlanta Hawks",
 "awayTeamName": "Cleveland Cavaliers",
 "startTime": "2026-10-30T00:00:00.000Z",
 "gameStatus": "scheduled",
 "seasonType": "regular",
 "broadcasts": ["NBA TV", "DAZN-CLE", "Prime-ATL"],
 "statType": "3-Pointers Made",
 "statKey": "three_points_made",
 "propTitle": "Aaron Wiggins 3-Pointers Made O/U",
 "lineScore": 1.5,
 "mainLineScore": 1.5,
 "isAlternate": false,
 "hasAlternates": true,
 "lineProvider": "swish",
 "sidesOffered": 2,
 "overLineId": "a0f85185-0de2-465a-a895-064b0d5c48b1",
 "overMultiplier": 0.79,
 "overPayoutTier": "discounted",
 "overOddsAmerican": -179,
 "overOddsDecimal": 1.56,
 "overOddsFractional": "100/179",
 "overImpliedProbability": 0.641,
 "overFantasyOddsAmerican": -200,
 "overFantasyOddsDecimal": 1.5,
 "overFantasyProbability": 0.63,
 "overStatus": "active",
 "overNoVigProbability": 0.6109,
 "underLineId": "a7d3e8b1-09f3-420b-8192-ece437bac78f",
 "underMultiplier": 1.19,
 "underPayoutTier": "boosted",
 "underOddsAmerican": 145,
 "underOddsDecimal": 2.45,
 "underOddsFractional": "29/20",
 "underImpliedProbability": 0.4082,
 "underFantasyOddsAmerican": 120,
 "underFantasyOddsDecimal": 2.2,
 "underFantasyProbability": 0.42,
 "underStatus": "active",
 "underNoVigProbability": 0.3891,
 "marketOverround": 0.0492,
 "updatedAt": "2026-10-29T08:32:25.078Z",
 "scrapedAt": "2026-10-29T10:02:46.600Z"
}
```

**An alternate line of the same prop (columns identical to the row above are left out)**

```json
{
 "rowId": "underdog:line:bd0c4862-c5a9-420f-84de-445b87a0588a",
 "projectionId": "bd0c4862-c5a9-420f-84de-445b87a0588a",
 "playerName": "Aaron Wiggins",
 "statType": "3-Pointers Made",
 "lineScore": 2.5,
 "isAlternate": true,
 "overLineId": "a03716b5-4adb-499e-997d-b796686f58f1",
 "overMultiplier": 1.3,
 "overPayoutTier": "boosted",
 "overOddsAmerican": 165,
 "overOddsDecimal": 2.65,
 "overOddsFractional": "33/20",
 "overImpliedProbability": 0.3774,
 "overFantasyOddsAmerican": 138,
 "overFantasyOddsDecimal": 2.38,
 "overFantasyProbability": 0.38,
 "overNoVigProbability": 0.3615,
 "underLineId": "24079a1e-59b3-443b-8d30-4f3943a00e78",
 "underMultiplier": 0.76,
 "underPayoutTier": "discounted",
 "underOddsAmerican": -200,
 "underOddsDecimal": 1.5,
 "underOddsFractional": "1/2",
 "underImpliedProbability": 0.6667,
 "underFantasyOddsAmerican": -228,
 "underFantasyOddsDecimal": 1.44,
 "underFantasyProbability": 0.66,
 "underNoVigProbability": 0.6385,
 "marketOverround": 0.0441,
 "updatedAt": "2026-10-29T08:29:19.805Z"
}
```

**CSV / Excel columns, in order:** rowId, recordType, platform, lineKey, projectionId, overUnderId, appearanceId, playerId, gameId, matchType, playerName, playerTeam, playerTeamName, playerPosition, playerPositionName, jerseyNumber, playerCountry, playerImageUrl, isHome, league, sportName, competition, gameTitle, matchup, opponent, homeTeam, awayTeam, homeTeamName, awayTeamName, homePlayerName, awayPlayerName, startTime, gameStatus, seasonType, venueName, venueType, broadcasts, statType, statKey, propTitle, lineScore, mainLineScore, isAlternate, hasAlternates, lineProvider, sidesOffered, overLineId, overMultiplier, overPayoutTier, overOddsAmerican, overOddsDecimal, overOddsFractional, overImpliedProbability, overFantasyOddsAmerican, overFantasyOddsDecimal, overFantasyProbability, overStatus, overNoVigProbability, underLineId, underMultiplier, underPayoutTier, underOddsAmerican, underOddsDecimal, underOddsFractional, underImpliedProbability, underFantasyOddsAmerican, underFantasyOddsDecimal, underFantasyProbability, underStatus, underNoVigProbability, marketOverround, updatedAt, scrapedAt

### How to read a row

**One row is one line with both sides.** "Points 24.5" has a Higher side (`over…` columns) and usually a Lower side (`under…` columns), each with its own multiplier and prices. `projectionId` identifies the line, `lineKey` the prop — every line of a prop, main or alternate, shares it. When Underdog offers one side only, `sidesOffered` is 1 and the other side's columns are empty: 59 % of the main lines on 2026-10-29 were one-sided, almost all Higher-only. With `rowShape: "side"` the same line becomes two rows.

**The payout multiplier is the pick'em number.** Each pick scales the entry's payout by its own multiplier: 1.00 is standard, above 1.00 the side is *boosted* (pays more), below 1.00 *discounted* (pays less). In the verified run the multipliers ran from 0.51 to 19.91, median 1.28: 59 % of sides boosted, 14 % standard, 27 % discounted.

**Two price sets, both served by Underdog.** `overOddsAmerican`, `overOddsDecimal` (and the computed fractional, implied and no-vig probabilities; same for `under…`) are the price Underdog attaches to the side as a leg; on the alternate-line documents it is identical to the `sportsbook` price set served next to it — a sportsbook-style quote, not a payout you can collect on its own. The `…Fantasy…` columns are Underdog's separate `fantasy` set for the same side. They differ because they carry different margins; the Actor never blends them.

**The margin is in the row.** `…ImpliedProbability` is `1 / decimal odds` to four decimals; `…NoVigProbability` is each side's share of the two (the pair sums to 1) and `marketOverround` is implied over + implied under − 1, the margin built into the line.

**Alternate lines (opt-in).** `isAlternate = false` rows come from the main board, `true` rows from the prop's ladder (only with `includeAlternates: true`). `mainLineScore` repeats the main line on every row of a prop, so `lineScore - mainLineScore` is the distance from it; the further away, the more extreme the multiplier (largest in the verified run: 19.91).

**Times.** `startTime` is the scheduled start in ISO 8601 UTC, `updatedAt` when Underdog last changed the line, `scrapedAt` when the Actor read the board (the same for every row of a run). The board only carries matches that have not started: in the verified run no row had a start time earlier than the moment it was read.

### Scale and speed (measured)

All figures are from runs on the Apify platform on 2026-10-29, 1,024 MB memory (the default), default settings, direct connection first.

| Run | Rows | Time | Notes |
|---|---|---|---|
| Console pre-fill (`maxItems` 300, main lines) | 300 | 9 s | first rows after 8 s |
| 1,000 rows with alternates on | 1,000 | 25 s | soonest matches first |
| **Default run: main lines, whole board** | **9,740** | **10 s** | one request for the board, no ladders (13,742 rows in the side shape) |
| Whole board with every alternate line (`includeAlternates: true`) | 24,995 | 394 s (6.6 min) | 9,744 props, 3,794 ladder requests, 202 MB peak memory, 0 duplicate `rowId`; 35,473 sides |

#### How the board is read

The main board is one document of about 25 MB that Underdog serves in roughly a second; the Actor downloads it once and flattens it in memory. A default run stops there: that is why it takes seconds. Alternate lines are not in that document: for every prop flagged `hasAlternates` (3,794 props on 2026-10-29) Underdog's web app makes one more request for the ladder, and so does the Actor when `includeAlternates` is on. Those requests are small (a few kilobytes) but numerous, so they are **paced**: the direct connection is used at a modest rate and, if Underdog's edge refuses it for a while, Apify datacenter sessions (one IP each) carry the load. In the verified run the direct connection answered 3,794 ladder requests, the datacenter sessions 0, and 0 were refused. That is why the whole board with alternates takes 6.6 minutes — but rows are saved from the first seconds on, soonest match first, and `maxItems` stops the reading as soon as enough rows are in.

### What you pay

Pay per result, no start fee, no subscription: **$0.25 per 1,000 rows** on the free Store tier, falling to **$0.15 per 1,000** on the highest tiers (Bronze $0.217, Silver $0.183, Gold and above $0.15). A row is one line (both sides in it); in the side shape a row is one side.

| Run | Rows | Free tier | Gold tier and above |
|---|---|---|---|
| 1,000 rows | 1,000 | $0.25 | $0.15 |
| **Default run: whole main-line board** (2026-10-29) | 9,744 | **$2.44** | **$1.46** |
| With `includeAlternates: true`: whole board (2026-10-29) | 24,995 | $6.25 | $3.75 |
| 100,000 rows | 100,000 | $25.00 | $15.00 |

Alternates are opt-in because they multiply the rows and the run time (the default run is 39 % of the rows). Filters cut the bill directly — a filtered-out row is never saved or billed.

#### Maximum cost per run

Every run has a **Maximum cost per run** (Console → run options, or `maxTotalChargeUsd` in the API). The Actor reads the budget at the start, never saves more rows than it pays for, and ends the run as SUCCEEDED with a status message, for example *"Saved 230 rows — this run's maximum cost ($0.05) covers 230 results at the current price. Raise "Maximum cost per run" to get more."* Verified after pricing: a run capped at $0.05 saved 230 rows and was charged for exactly 230 (the number depends on the price of your tier). Set a small maximum while testing.

### Integrations

- **Apify API and clients** — `apify-client` for Node.js and Python (examples above), plain HTTP for everything else; the dataset as JSON, JSONL, CSV, XLSX, XML or HTML.
- **Schedules and webhooks** — Console → Schedules (any cron; every 15 minutes for line movement, once a day for a "boosted picks" sheet); a webhook fires on success, failure or timeout with the run and dataset ids.
- **Make, Zapier, n8n** — the Apify app / node: "Run Actor", then "Get dataset items"; map `playerName`, `statType`, `lineScore`, `overMultiplier`, `underMultiplier` into the next step.
- **Google Sheets, Airtable, Notion, Slack, Discord, Telegram** — through the Apify Google Sheets integration, Make / Zapier / n8n, or a small webhook handler.
- **Warehouses and lakes** — JSONL or CSV to S3 / GCS / Azure, or straight into BigQuery, Snowflake or Postgres. `rowId` is a stable primary key per line; `scrapedAt` is the snapshot key.
- **MCP, AI agents, Pandas / R / DuckDB** — the Apify MCP server exposes the Actor as a tool; typed columns (`lineScore`, `overMultiplier` are numbers, `startTime` is ISO 8601) need no cleaning.

### Performance tips

- **Start from the question.** League, player, team, stat and time filters are applied before any alternate-line request: "NBA points in the next 24 hours" needs a few dozen small requests, "everything" thousands.
- **Alternates are opt-in.** The default is one request for the whole board — 9,740 rows in about 10 seconds on 2026-10-29. Switch `includeAlternates` on when you need the ladders, and narrow it with filters.
- **Use `maxItems` as a safety rail.** Rows come out soonest match first; pair it with **Maximum cost per run** while testing.
- **Schedule instead of polling by hand**, with a narrow filter, and compare runs by `rowId`.
- **Do not start many full-board runs at the same moment.** Underdog's edge refuses bursts from one address; the Actor paces itself, but ten parallel full runs are ten times the pressure.

### Troubleshooting: every status message explained

The run's status message and the `SUMMARY` record in the key-value store always say what happened. `COMPLETED`, `PARTIAL` and `EMPTY_RESULT` end as *Succeeded*; `BLOCKED`, `FETCH_FAILED` and `INVALID_INPUT` end as *Failed* with the reason.

| Status message (start) | Meaning | What to do |
|---|---|---|
| `COMPLETED: saved N rows from L league(s)` (with alternates on: `… (K alternate-line rows)`) | Everything requested was read. May end with *the "maxItems" limit … was reached* or *this run's maximum cost … covers …*. | Nothing; raise `maxItems` or the maximum cost for more. |
| `PARTIAL: … the alternate lines of N prop(s) could not be read` (alternates on) | Underdog's edge refused or timed out some ladder requests even after retries on other exits. Those props keep their main line only; every other row is complete. | Run again later, or with fewer leagues. The SUMMARY lists the exits and refusals. |
| `PARTIAL: … could not read <LEAGUE>` | One requested league could not be downloaded; the others are complete. | Run again, or add a proxy. |
| `PARTIAL: … the run time limit was reached` | The timeout came before every prop was read; the rows saved are complete. | Raise the timeout or narrow the filters. |
| `EMPTY_RESULT: Underdog's board is empty …` | Underdog answered but no lines are posted (off-season, no games). | Try again later or choose other leagues. Off-season leagues (MLB, UFL, NPB on 2026-10-29) are empty by nature. |
| `EMPTY_RESULT: N main lines are on the board but none match these filters. Leagues posting lines now: …` | Your filters exclude everything; the message lists the leagues with lines. | Loosen a filter; check the spelling. |
| `The run's maximum cost … does not cover a single result` | The "Maximum cost per run" is below the price of one row. | Raise it. |
| `Saved N rows — this run's maximum cost … covers N results` | The cost ceiling was reached; the rows saved are complete and billed. | Raise "Maximum cost per run" to continue. |
| `FETCH_FAILED: Underdog answered HTTP 426 (upgrade required) …` | Underdog retired or changed the endpoint this Actor reads. | Open an issue; we update the Actor. |
| `BLOCKED: no board could be read …` / `FETCH_FAILED: no board could be read …` | Every route (direct, datacenter, capped residential) was refused or timed out — an access failure, not an empty board. | Retry in a few minutes; add `proxyConfiguration`. |
| `FETCH_FAILED: the board was read but no row could be completed …` | The time limit ended before the first rows. | Raise the timeout or leave alternates off. |
| `INVALID_INPUT: …` | A contradictory multiplier range, a date that is not `YYYY-MM-DD`, or a reversed date window. | Fix the input named in the message. |

SUMMARY `notes`: *Resumed after a platform restart* (rows already saved are not written or charged again); *… read league by league through the capped residential rescue route* (the board was unreachable directly).

### Honest limits

- **Pre-game lines only.** The public board lists matches that have not started. In the data of 2026-10-29 no row had a start time earlier than the moment it was read; 177 team games and individual events were on the board, all `scheduled` except one Counter-Strike match four minutes from its start (status `scoring`). A line leaves the board when its match begins. There is no in-play data and no results or grading.
- **A snapshot, not a tick history.** You get the board at the moment of the run. History is what you build by scheduling runs and keeping the datasets.
- **Pick'em prices are not sportsbook bets.** `…OddsAmerican` / `…OddsDecimal` are quotes Underdog attaches to a leg, not a price you can bet at; an entry pays by Underdog's rules and the multipliers. Treat them as a market signal.
- **The board and the ladders are read seconds apart;** each row carries its own `updatedAt`.
- **Alternate lines (opt-in) cost requests.** Ladders are read one prop at a time; a refused request is retried on another exit, and a prop whose ladder still cannot be read keeps its main line (the run ends `PARTIAL`).
- **Home / away for `vs` titles.** An `AWAY @ HOME` title is exact. For a `vs` title the Actor resolves soccer (home team first — checked against stadiums and squads) and esports (away team first — checked against the squads of League of Legends and Counter-Strike matches); for any other sport with a `vs` title it leaves `homeTeam`, `awayTeam` and `playerTeam` empty rather than guess.
- **Weather and sport-specific context.** On 2026-10-29 no match carried a forecast and MLB was out of season, so forecast columns are written only when they carry a value and MLB-specific context (probable pitchers) is not mapped; venue and broadcast are mapped where present (83 % and 94 % of rows).
- **Not on the public board:** pick popularity, player statistics and game logs, injury reports, live scores, the state-gated sportsbook and prediction-market products, contest or entry data.
- **Leagues come and go.** Off-season leagues return nothing; new leagues appear without a code change.
- **The source can change.** Underdog retires old API versions (the earlier ones answer HTTP 426). If the endpoint changes the Actor says so in plain words instead of returning an empty file.

### How it compares

| Option | Effort | Alternate lines | Upkeep | Cost |
|---|---|---|---|---|
| Copying lines from the app by hand | Hours per slate | Only the ones you open | None | Your time |
| Your own script against the web app's JSON | Days; six document parts to join | Needs the separate ladder endpoint | You fix it when Underdog changes versions | Servers + proxies |
| A general sports-odds API | Low | Usually sportsbook main lines, rarely pick'em multipliers | Vendor | Monthly subscription |
| **This Actor** | Minutes | Whole ladder of every prop, opt-in | We keep it current | Pay per row, no monthly fee |

The trade-off: this Actor reads one platform's public board; it is not a multi-book odds feed.

### FAQ

#### What is Underdog Fantasy Scraper?

An Apify Actor that reads the public Pick'em board of Underdog Fantasy (underdogfantasy.com / app.underdogsports.com) and returns one row per line with both sides in it — payout multipliers, American / decimal / fractional odds, implied and no-vig probabilities, player, team, matchup, start time, league and stat — with the alternate lines of each prop on request (or one row per side with `rowShape: "side"`).

#### Is there an official Underdog Fantasy API?

Underdog does not publish an API for its Pick'em board. Its web app loads the board from JSON endpoints that any visitor's browser can call, with no login, cookie or key; this Actor reads those endpoints. They are undocumented and not guaranteed, so the Actor says so plainly when one changes.

#### How do I export Underdog props to Excel or CSV?

Run the Actor, open the Output tab and press Export → Excel or CSV, or call `…/datasets/DATASET_ID/items?format=xlsx` (or `csv`) with your token.

#### What is the difference between the line shape and the side shape?

The default is one row per line with both sides in it (`overMultiplier` / `underMultiplier`, …): about half the rows and price of the side shape. `rowShape: "side"` writes Higher and Lower as two rows (`side`, `lineId`, `oppositeLineId`, `multiplier`, `oddsAmerican` …) for tools that expect one selection per row.

#### What is a payout multiplier on Underdog?

The factor by which a pick scales the entry's payout: 1.00 standard, above 1.00 pays more, below pays less. Each side of a line has its own: `overMultiplier` and `underMultiplier`.

#### What do boosted, standard and discounted mean?

A derived label per side (`overPayoutTier`, `underPayoutTier`): `boosted` above 1.0, `standard` at 1.0, `discounted` below 1.0 — it lines up with PrizePicks' demon / standard / goblin picks.

#### What are alternate lines and how do I get them?

Other thresholds of the same prop — a 24.5-point prop may also offer 19.5, 29.5, 34.5 — each with its own multipliers and prices. They are off by default (they multiply the rows and the run time); `includeAlternates: true` adds them as rows of their own, and `isAlternate` tells the two apart. An alternate can sit at the same number as the main line with different prices (58 NFL props on 2026-10-29).

#### Why does `hasAlternates` say true but I only see the main line?

`includeAlternates` is off (the default), a side / tier filter removed the alternates, or the ladder request for that prop failed (the run then ends `PARTIAL` and says how many).

#### How many props and rows are there?

9,744 main lines on 2026-10-29 (the default run), plus 15,251 alternate lines with `includeAlternates: true`: 24,995 line rows in the verified run (35,473 sides, which is what `rowShape: "side"` would give). The numbers move with the schedule.

#### Which leagues and sports are supported?

Whatever Underdog lists that day. On 2026-10-29: NFL (2,929 lines), CFB (3,149 lines), NHL (1,328 lines), FIFA (996 lines), NBA (315 lines), TENNIS (223 lines), CS (207 lines), MMA (193 lines), LOL (120 lines), WNBA (76 lines), MOTORSPORTS (59 lines), DOTA (58 lines), CFL (40 lines), VAL (30 lines), PGA (21 lines). Use the league code in `leagues`.

#### Why is MLB (or another league) empty?

Underdog posts no lines for it — off-season or no games. MLB, UFL, NPB and Formula 1 season props were empty on 2026-10-29.

#### Does it include live, in-play lines?

No. In the verified run no row had a start time earlier than the moment it was read; a match leaves the board when it begins.

#### How often do lines change, and how often should I run it?

`updatedAt` shows lines changing minute by minute on a busy day: every 5–15 minutes with a narrow filter for line movement, once in the morning for a newsletter.

#### How do I track line movement?

Schedule the Actor, keep every run's dataset and compare two runs by `rowId`: `lineScore`, `overMultiplier` and `overOddsAmerican` of the same row give the movement; rows in one run only are new or removed.

#### Is `rowId` stable between runs?

While the line exists, yes. On 2026-10-29, 98 % of the main-line ids of a board read 83 minutes earlier were still on the board.

#### Why are some lines one-sided?

Underdog offers one side on many lines — mostly Higher-only props such as "anytime touchdown". On 2026-10-29 59 % of main-line rows came from such lines; `sidesOffered` is 1 and the other side's columns are empty.

#### What is the difference between `overOddsAmerican` and `overFantasyOddsAmerican`?

Underdog serves two price sets per side. `…OddsAmerican` / `…OddsDecimal` are the leg price (identical to the sportsbook set on alternate-line documents); `…FantasyOdds…` is a separate set with its own margin. Both are output as served, never blended.

#### Are these real sportsbook odds?

They are Underdog's quotes for a pick'em leg in odds formats — good for comparison and modelling, not a bettable price (see [Honest limits](#honest-limits)).

#### How do I compare Underdog with PrizePicks or a sportsbook?

Join on `playerName` + `statType` + `lineScore` (sportsbooks also `startTime`). The names match our PrizePicks and Sleeper Scrapers (`playerName`, `playerTeam`, `playerPosition`, `league`, `statType`, `lineScore`, `startTime`, `projectionId`, `overMultiplier` / `underMultiplier`); the PrizePicks Actor's `opponent` string is `matchup` here, and `opponent` is the opposing team as in the Sleeper Actor.

#### How fast is it and how much does it cost?

Default (main lines): 9,740 rows in about 10 s; pre-fill: 300 rows in 9 s; the whole board with alternates on: 24,995 rows in 6.6 min. From $0.25 per 1,000 rows (free tier) down to $0.15: a main-line board is about $2.44, the whole board about $6.25 at the free tier.

#### How do I cap what I spend?

Set **Maximum cost per run**, use `maxItems`, or filter — a filtered-out row is never billed. The Actor stops exactly at the budget and says so in the status message.

#### Why did I get fewer rows than `maxItems`?

The board (after your filters) had fewer rows; the SUMMARY lists the leagues and counts on the board.

#### Why is `playerTeam` empty on some rows?

Individual sports (tennis, MMA, golf, NASCAR) have no team. For a team game with a `vs` title in a sport other than soccer or esports the Actor does not guess which side is home.

#### How are home and away decided?

`AWAY @ HOME` titles are exact. For `vs` titles, soccer lists the home team first and esports the away team first (checked against stadiums and squads); other sports are left empty.

#### Does it work from outside the US? Do I need a proxy?

Yes, and no. The Actor runs on Apify's infrastructure and reads Underdog's JSON from there. It connects directly, moves to the Apify datacenter proxy when Underdog's edge refuses the direct connection, and keeps a capped residential exit for small documents as a last resort; you can set your own proxy.

#### Can I get player stats, injuries, pick popularity, results or live scores?

No — they are not on the public board. The row has the player's position, jersey number, country and image.

#### Does it include Underdog's sportsbook or prediction markets?

No. Those are separate, state-gated products; this Actor reads the Pick'em board and its alternate lines.

#### Can I use the data in a commercial product?

That is governed by Underdog's terms, copyright and database law and your gambling and data-protection rules: read [Is it legal to scrape Underdog?](#is-it-legal-to-scrape-underdog) and take advice.

#### What happens when Underdog changes its API?

The Actor stops with a plain message (an upgrade-required answer) or returns `PARTIAL` with the reason instead of an empty file, and we update it.

#### Two stats share a display name — how do I tell them apart?

Use `statKey`. On 2026-10-29 only soccer "Assists" had two keys (`assists` and `period_1_2_assists`); period stats such as "1Q Rec Yards" have their own names.

### Glossary

- **Pick'em** — a contest format where you pick Higher or Lower on player stats and combine picks into an entry.
- **Higher / Lower** — Underdog's wording for over / under.
- **Line (`lineScore`)** — the threshold of a prop, e.g. 24.5 points.
- **Prop** — one player, one stat, one match (`overUnderId`).
- **Main line / alternate line / ladder** — the line on the main board, another threshold of the same prop, and all lines of one prop ordered by `lineScore`.
- **Payout multiplier** — the factor a pick applies to the entry's payout; 1.00 is standard. Above 1.00 is *boosted*, below is *discounted*.
- **American, decimal, fractional odds** — three ways to write one price (+104, 2.04, 26/25).
- **Implied probability** — the chance a price represents, `1 / decimal odds`; it includes the margin.
- **Margin (vig, juice)** — how far the implied probabilities of all sides add up past 1.
- **Line shopping / CLV** — comparing a prop across platforms; comparing the line you took with the closing line (needs repeated snapshots).
- **Solo game** — Underdog's name for an individual event (a golf round, a tennis match, a fight, a race).

### Is it legal to scrape Underdog?

The Actor reads data that Underdog serves to every anonymous visitor of its web app — it does not log in, bypass a paywall or use anyone's account, and it collects no personal data of Underdog's customers. The rows describe public matches, public athletes and the prices Underdog displays. That is not the same as permission: Underdog's website terms may restrict automated access or reuse, copyright and database rights may apply to the compiled board, and the rules for gambling-related data differ by country and state. You are responsible for how you use the output — check the terms and the law where you operate, take advice before building a commercial product on it, and do not use the data to harm the platform or to evade a restriction on you personally. Athlete names are personal data in a narrow sense; they are public figures and the data is sports statistics, but run your own data-protection assessment if you combine it with other personal data.

### Responsible use

This is a data tool for analytics, research and tooling. It does not place bets, enter contests or automate an account, and promises no profit; past lines and multipliers do not predict results. Betting and fantasy pick'em involve real financial risk and can be addictive — take part only where it is legal for you, only with money you can afford to lose, and use the limit and self-exclusion tools of any platform you play on. If gambling is a problem for you or someone you know, contact your national problem-gambling helpline.

### Changelog

- **1.0 — 2026-10-29** — first release: the whole Pick'em board (main lines by default, alternate-line ladders on request) as one row per line with both sides, or one row per side; filters for league, player, team, stat, side, tier, multiplier, start window and alternates; American / decimal / fractional odds, probabilities and multipliers on every row; paced reading with a capped residential rescue; pay per result with a maximum-cost guard; verified on a 24,995-row run, no duplicates.

### Support

Questions, a missing field, a wrong value, a league that is empty when you expect lines? Open an issue on the Actor page with the run link — **we reply within 24 hours**. If Underdog changes its web app and a run starts failing, send us the status message; source changes are the first thing we fix.

# Changelog

This Actor's version history is a separate document: https://apify.com/logiover/underdog-fantasy-scraper/changelog.md

# Actor input Schema

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

Underdog league codes to read, e.g. `NFL`, `CFB`, `NBA`, `NHL`, `FIFA` (soccer), `TENNIS`, `MMA`, `PGA`, `CS`, `LOL`. Pick from the list or type any code Underdog uses; codes are not case-sensitive. Leave empty to read every league that has lines right now. A league with no lines posted (off-season) simply returns nothing.

## `rowShape` (type: `string`):

**One row per line** (default): both sides of a line in one row — `overMultiplier` / `underMultiplier`, `overOddsAmerican` / `underOddsAmerican` and so on — the shape of the PrizePicks and Sleeper Actors and the cheapest to buy. **One row per side**: Higher and Lower as two rows with `side`, `multiplier`, `oddsAmerican` … (twice the rows, and twice the price per line).

## `playerNames` (type: `array`):

Keep only these players. Case-insensitive, accents ignored, partial names allowed (`Jokic` finds Jokić, `Lamar` finds Lamar Jackson).

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

Keep only props of these teams or games of these teams: abbreviation or name (`KC`, `Chiefs`, `Lakers`). Matches the player's team and either side of the matchup.

## `statTypes` (type: `array`):

Keep only these stats: `Points`, `Rebounds`, `Passing Yards`, `Shots On Goal`, `Kills`, `Strikeouts` … Case-insensitive; matches the stat name, the stat key (`pass_yds`) and the prop title.

## `side` (type: `string`):

Keep only lines whose Higher (over) or Lower (under) side passes the payout filters below, or both. In the one-row-per-side shape it also decides which side rows are saved.

## `payoutTiers` (type: `array`):

Keep only lines where a side is boosted (multiplier above 1.0), standard (exactly 1.0) or discounted (below 1.0). Leave empty for all.

## `minPayoutMultiplier` (type: `number`):

Keep only lines where a side pays at least this multiplier, e.g. `1.05`. Leave empty for no limit.

## `maxPayoutMultiplier` (type: `number`):

Keep only lines where a side pays at most this multiplier, e.g. `0.9`. Leave empty for no limit.

## `includeAlternates` (type: `boolean`):

Off (default): the main line of every prop — one request for the whole board, seconds. On: also the alternate lines Underdog posts next to a player's main line (a different threshold with its own multipliers), one more request per prop — roughly 2.5 times the rows and several minutes for the whole board, so narrow it with the filters.

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

Keep only matches that start within this many hours from now (e.g. `24` for today's board). Leave empty for no limit.

## `startDateFrom` (type: `string`):

Keep only matches starting on or after this day, `YYYY-MM-DD` (UTC).

## `startDateTo` (type: `string`):

Keep only matches starting on or before this day, `YYYY-MM-DD` (UTC).

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

Upper bound on rows saved — each row is one billed result (one line in the default shape, one side in the side shape). The whole main-line board is roughly 9,000–11,000 line rows on a busy day, about 25,000 with alternate lines on; soonest matches come first.

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

Optional. By default the Actor connects directly and falls back to the Apify datacenter proxy and, as a last resort, a capped US residential exit by itself. Set a proxy here only if you want every request to go through it.

## Actor input object example

```json
{
  "leagues": [],
  "rowShape": "line",
  "playerNames": [],
  "teams": [],
  "statTypes": [],
  "side": "both",
  "payoutTiers": [],
  "includeAlternates": false,
  "maxItems": 300,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All Underdog rows collected in 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": [],
    "maxItems": 300,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("logiover/underdog-fantasy-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": [],
    "maxItems": 300,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("logiover/underdog-fantasy-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": [],
  "maxItems": 300,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call logiover/underdog-fantasy-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,logiover/underdog-fantasy-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/0DuKWAV4sF7FH0Yr3/builds/MT8xFYPAu4z7lerrl/openapi.json
