Underdog Fantasy Player Props Scraper - Lines & Line Moves
Pricing
from $0.11 / 1,000 line returneds
Underdog Fantasy Player Props Scraper - Lines & Line Moves
For DFS and betting models: every Underdog Higher/Lower prop as a row - player, team, stat, line, payout multiplier, price, implied probability, start time. 5,607 pre-game lines, 11 leagues, one request (2026-09-22). Monitoring returns only lines that moved, with the earlier values.
Pricing
from $0.11 / 1,000 line returneds
Rating
0.0
(0)
Developer
NeverEmpty
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
6 days ago
Last modified
Categories
Share
For DFS and betting models: every Underdog Fantasy Higher/Lower player prop as one row - player, team, opponent, stat, the line itself, the payout multiplier for each side, American and decimal prices, Underdog's own implied probability and the game start time. On 2026-09-22 a single request returned 5,607 pre-game lines across 11 leagues, in 11.0 seconds. 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 never charged for the same unchanged line twice.
Export as JSON, CSV or Excel.
Why monitoring matters here
Underdog's board barely moves between runs. Measured by taking the whole board twice, 9 minutes 5 seconds apart, on 2026-09-22:
| lines | |
|---|---|
| on the board the second time | 5,542 |
| unchanged since the first read | 5,368 |
| changed (line, multiplier, price, odds or status) | 169 |
| newly opened | 5 |
| charged in monitoring mode | 174 (3.1%) |
| lines that had closed and were gone | 70 |
Reading the board costs the same either way - the $0.01 start below. With monitoring on, that run charges for 174 rows instead of 5,542 - about 1/32 of the rows - and each of those rows carries what the value was before.
Both snapshots are in this repository as test/fixtures/board-a-fingerprints.json and board-b-fingerprints.json (the key and the fingerprint of every line, produced by src/underdog.js itself), and npm test recounts every number above. The 45-line and 47-line samples in test/fixtures/lines-a.json and lines-b.json are cut from the same two reads.
How much moves depends entirely on the gap between runs, and one measurement is one measurement. Three more pairs of runs on the same day, all on the same 6,300-line board:
| gap between runs | lines changed |
|---|---|
| back to back (about a minute) | 2, then 1 |
| a few minutes | 256 |
| 25 minutes | 1,052 of 6,182 (17%) |
So the shorter the schedule, the smaller the bill per run - the opposite of paying full price for the whole board every time. 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 -
lineKey(stable across runs; use it to join runs together),lineId,overUnderId,propId(player + stat, shared by a line and its alternates),appearanceId - Player -
playerId,playerName,firstName,lastName,position,positionName,jerseyNumber - Teams -
teamId,teamName,opponentTeamId,opponentName,homeAway - Game -
matchType,gameId,gameTitle,gameShortTitle,gameStartsAt,gameStatus,gameVenue,competition - The prop -
league,leagueName,stat(Underdog's code, e.g.rush_rec_yds),statName(Rush + Rec Yards),propTitle,line,lineText,hasAlternateLines,sidesOffered - Each side (
higher...andlower...) -PayoutMultiplier,AmericanPrice,DecimalPrice,OddsAmerican,OddsDecimal,ImpliedProbabilityPercent,Status,PickText - The line's own state -
lineStatus,lineType,isLiveLine,providerId,lineUpdatedAt - Monitoring only -
isNewSinceLastRun,changeKinds(line/multiplier/price/odds/status/new),previousLine,lineMove,firstSeenLine,lineMoveSinceFirstSeen,previousHigherPayoutMultiplier,higherPayoutMultiplierMove,previousLowerPayoutMultiplier,lowerPayoutMultiplierMove,previousHigherOddsAmerican,previousLowerOddsAmerican,firstSeenAt,previouslySeenAt
Three different numbers that all look like a price
Underdog publishes three sets, and they do not agree with each other. They are kept in separate columns and none of them is derived from another:
| column | what Underdog calls it | example |
|---|---|---|
higherPayoutMultiplier | payout_multiplier - the multiplier shown on the pick | 1.11 |
higherAmericanPrice / higherDecimalPrice | american_price / decimal_price | +123 / 2.23 |
higherOddsAmerican / higherOddsDecimal / higherImpliedProbabilityPercent | odds.fantasy | +106 / 2.06 / 45 |
odds.sportsbook and odds.prediction were null on all 9,122 sides measured, so they are not columns.
Leagues
NFL, CFB (College Football), MLB, NHL, WNBA, TENNIS, MMA, CS (Counter-Strike), LOL, VAL (Valorant), F1SZN (Formula 1, season-long). Counts on 2026-09-22: MLB 2,105 / NFL 1,432 / CFB 868 / CS 389 / WNBA 244 / Tennis 237 / MMA 102 / NHL 89 / Valorant 79 / LoL 40 / F1 22. Which leagues are on the board depends on the day; an empty board is Underdog's own answer and comes back as a free row, not as a failure.
That list is what Underdog's board carried on the day this Actor was built, not a promise about what Underdog offers. Leave leagues empty and you get every league on the board, including any Underdog has added since. Naming a league that is not in the list above stops the run before it asks Underdog anything, and that run is free.
Input
Everything is optional. With an empty input the whole board comes back, up to maxLines.
| field | what it does |
|---|---|
leagues | keep only these 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 name or Underdog's stat code contains one of these |
teams | keep a line only if the team, the opponent or the matchup title contains one of these |
keywords / keywordMatch / excludeKeywords | keep or drop on any of the text columns; keywordMatch is any or all |
minPayoutMultiplier / maxPayoutMultiplier | keep a line only if a side pays inside this range. 0 = no limit |
onlyBoostedLines | only lines where a side pays something other than 1.0 |
onlyStandardLines | only lines where every side offered pays 1.0 |
onlyBothSides | drop lines that offer one side only |
startsWithinHours | only games starting within this many hours. 0 = no limit |
maxLines | how many rows this run may return (default 500) |
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 multiplier, price, odds or status moved |
minLineMove | monitoring only: the line must have moved at least this far |
minPayoutMultiplierMove | monitoring only: a payout multiplier must have moved at least this much |
useProxy | use an Apify proxy and retry a refused request from another address (on by default) |
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 - except teams, where a row with no team and no matchup title (Underdog's season-long props) is dropped, since naming a team means you do not want those.
Honest limits
- The feed is one big request, and its speed varies a lot. 16,026,326 bytes (1,421,724 over the wire with gzip). The same feed answered in 2.7s, 11.0s, 35.0s and 111.4s on 2026-09-22. Each attempt waits up to 180 seconds and the run tries up to 4 times, so a slow day costs time, not rows.
- Pre-game only. Underdog's live in-play lines need a logged-in account. This Actor never logs in, so it returns pre-game lines only: all 5,607 lines in the 2026-09-22 snapshot carried
live_event: falseandline_type: balanced(npm testrecounts both fromtest/fixtures/board-a-fingerprints.json). - Team names are not in this feed. Underdog publishes team ids and a matchup title. The title comes in two shapes:
Atlanta Falcons @ Green Bay PackersandEsport Academy Copenhagen vs 100 Thieves.A @ Bis away at home, soteamNameandopponentNameare filled in for those. For thevsshape it is not verified which side is home, so those rows leaveteamNameandopponentNameempty rather than guess -teamId,opponentTeamId,homeAwayandgameTitleare still there. - Some props have no game. Underdog's season-long props (89 NHL lines on 2026-09-22) are attached to a season, not a match, so every game column is empty. Formula 1 season props have no opponent.
- Some lines offer one side only. 2,092 of 5,607 lines carried a single side - 2,076 Higher-only and 16 Lower-only - so the missing side's columns are empty, not zero.
- A line Underdog gives no stable id, or repeats, is skipped rather than returned, because there would be no safe way to tell it apart from another line on the next run. The run says how many were skipped; on 2026-09-22 it was 0 of 5,607.
- Payout multipliers are Underdog's numbers, not a calculation. They ranged from 0.60 to 19.82 on 2026-09-22, and 3,483 of 9,122 sides were the standard 1.0.
- 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/minPayoutMultiplierMove/onlyLineMoves, its earlier values are deliberately kept. So a line that moves 0.5 four times does reach aminLineMoveof 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, keywords, multipliers and start time are about which lines you want at all, so widening one of them later does not dump the whole board on you as new.
- This is not advice about betting or investing. It returns what Underdog publishes, nothing more.
Rows that are never charged
Every run that returns nothing says why, in one free row:
status | meaning |
|---|---|
no-results | Underdog answered correctly and its board is empty |
no-filter-match | lines were read, the filters removed all of them |
no-line-move | monitoring mode: every line read is unchanged |
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 |
blocked | Underdog refused the request (403 / 429 / 503) from every address tried |
unreadable | the feed could not be read, or its shape changed |
unsupported-input | the input asks for something that cannot exist (an unknown league, two filters that contradict each other) - nothing is requested from Underdog |
no-results, no-line-move and blocked are deliberately three 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 Underdog answered with a board carrying at least one line | $0.01 |
line-scraped - per line returned | $0.15 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 Underdog refused it, when it timed out, when the board came back empty, or when it was stopped before asking Underdog anything (an unsupported league, or two filters that contradict each other). Filters that simply match nothing do not make a run free - the board was read, so the $0.01 start applies.
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.01 and returns one free row saying so. Following 20 lines every 5 minutes costs about $0.01 x 8,640 runs a month in start fees, plus the handful of rows that actually moved - decide the schedule with that in mind, and set the run's maximum total charge if you want a hard ceiling.
How it reads Underdog
One request to Underdog's public pick'em feed, https://api.underdogfantasy.com/v1/over_under_lines. No login, no paywall, no bot check. https://api.underdogfantasy.com/robots.txt was fetched twice on 2026-09-22 and contains a single comment line with no rules at all.
The response is large (16,026,326 bytes; 1,421,724 over the wire with gzip) and its speed varies: 2.7s, 11.0s, 35.0s and 111.4s were all measured on 2026-09-22. Of 12 Apify datacentre addresses tested that day, 11 answered HTTP 200 and 1 answered 403, so a refused or timed-out request is retried up to four times - from a different address when a proxy is on - before the run gives up and writes a free blocked row.
Unofficial. Public data only.
Not affiliated with, endorsed by or connected to Underdog Fantasy or any sportsbook. It reads only what Underdog publishes without a login, and returns no personal data: the people in the output are professional athletes, and the columns are their public playing statistics.