NFL & MLB Odds Movement Tracker
Pricing
from $2.80 / 1,000 game market snapshot delivereds
NFL & MLB Odds Movement Tracker
Get NFL and MLB moneyline, spread and total odds from ESPN's DraftKings feed. Export current game markets and compare prices with a previous snapshot for sports research and reporting.
Pricing
from $2.80 / 1,000 game market snapshot delivereds
Rating
0.0
(0)
Developer
Tim Zinin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Get current NFL and MLB moneyline, spread, and total prices as structured market rows, then compare a later observation with your own compatible snapshot.


This Actor reads the public ESPN Site API scoreboard responses for the UTC dates you select. When ESPN includes a DraftKings odds block, it normalizes complete numeric quotes into one Dataset row per game × provider × market. Every row keeps all required outcomes together: home and away for moneyline, both teams for spread or run line, and over and under for total. It also records event identity, teams, the source request, observation time, and optional movement from a previous snapshot.
Use it for research, reporting, monitoring, and data operations. Output is observed market data, not betting advice or a promise that a price remains available.
1. What you get
Each paid result-found event is a game market snapshot: one odds_market Dataset row with one game, provider, market, and all required outcomes. Incomplete source blocks are skipped.
Each market row includes:
- a stable
rowKeyinleague:eventId:providerId:marketform; - normalized league, event, home team, away team, provider, and market identity;
- current American odds and, where ESPN supplies them, opening odds and opening lines;
- the ESPN context fields that help explain the quote, including display details, spread, total, link, header, footer, and favorite flags when present;
- provenance with the ESPN Site API URL, requested scoreboard date, and retrieval timestamp;
- a collection timestamp shared by the observation;
movement,previous,previousObservedAt, and field-level numericchangeswhen you provide a compatible priorSNAPSHOT;- billing intent at the time the row was written.
The default Dataset can also contain free diagnostic rows. Diagnostics explain conditions such as a successful source response with no odds, events excluded by the UTC boundary, a partially failed source window, a buyer row limit, or a verified budget stop. A diagnostic is operational evidence; it is not an game market snapshot and does not request a result-found charge.
Two key-value-store records complete the run:
OUTPUTis the reconciliation summary. It states the terminal outcome, selected and delivered row counts, source and delivery completeness, budget mode, observed UTC window, request receipts, and theSNAPSHOTkey.SNAPSHOTis a compact comparison baseline containing only numeric market values from Dataset rows whose delivery was acknowledged. It omits large source links and display text. It is bounded to 1,000 rows and is designed to fit the 2 MiBpreviousSnapshotinput boundary.
The first run is useful without a snapshot. Those rows use movement: "first_run". On a later same-scope run, a matching row can be unchanged or changed; a current row absent from the supplied baseline is new. A missing current event is not emitted as canceled. Source failure, source truncation, lack of odds, schedule movement, and a genuinely removed event are different possibilities, so the Actor does not collapse them into a false cancellation claim.

2. Who uses it
Sports data analysts use the rows as a compact observation table. The stable key makes it practical to join two runs, while current, previous, and changes prevent every downstream notebook from reimplementing movement logic.
Editorial and research teams use event names, UTC start times, market values, and source provenance to prepare internal briefs. A producer can filter to movement == "changed", sort by kickoff time, and review the exact values that changed. The Actor supplies evidence for that review; it does not write a headline or decide that movement is material.
Automation builders use the Dataset and OUTPUT contract to route complete results, partial observations, empty windows, and failures differently. This matters when a workflow should update a table only after complete collection but should open an incident when some ESPN date requests fail.
Modeling and QA teams use the normalized outcomes to test parsers, implied-probability calculations, dashboards, or feature pipelines. American odds remain American odds. The Actor does not remove bookmaker margin, calculate fair probability, make a forecast, or mark a wager as attractive.
Operations teams use the predefined task inputs as repeatable templates. The three catalog tasks cover NFL moneyline for the coming UTC week, MLB moneyline for the current UTC day, and NFL spreads plus totals for the coming UTC week. They are saved input configurations, not schedules. Running a task again creates a fresh observation and fresh billable market rows.
Agents and MCP clients can run the Actor, inspect OUTPUT, retrieve the Dataset, and preserve SNAPSHOT for the next call. The small, explicit input surface is suitable for an automated caller: two enum arrays, a bounded date horizon, a row cap, and an optional bounded snapshot object.
It does not provide order books, streaming ticks, scores, injuries, broad bookmaker coverage, history, arbitrage alerts, or recommendations. It exposes DraftKings-labeled odds when ESPN returns them.
3. How to run
For a useful first run, select a league, select one or more markets, choose the UTC horizon, and set a maximum number of delivered market rows. No API key for ESPN and no buyer-supplied secret are required.
{"leagues": ["nfl"],"markets": ["moneyline"],"daysAhead": 7,"maxItems": 100}
daysAhead is inclusive. A value of 0 means events starting from today at 00:00:00Z up to, but excluding, tomorrow at 00:00:00Z. A value of 7 includes today and seven additional UTC dates. The Actor filters event timestamps against this half-open interval even when the ESPN scoreboard response contains a game outside it.
For movement, download the entire SNAPSHOT record from the first run. Submit it as previousSnapshot on the later run and keep leagues, markets, and daysAhead exactly the same. maxItems may differ because it controls current delivery rather than comparison identity.
{"leagues": ["nfl"],"markets": ["moneyline"],"daysAhead": 7,"maxItems": 100,"previousSnapshot": {"schemaVersion": "1.0","scope": {"leagues": ["nfl"],"markets": ["moneyline"],"daysAhead": 7},"observedAt": "2026-09-10T18:54:09.441Z","window": {"from": "2026-09-10","through": "2026-09-17"},"rows": [{"rowKey": "nfl:401872657:100:moneyline","current": {"home": {"oddsAmerican": -198, "openOddsAmerican": -155},"away": {"oddsAmerican": 164, "openOddsAmerican": 130}}}]}}
Use the SNAPSHOT exactly as returned. Do not merge baselines from different scopes, change row keys, add source text, or replace absent numbers with zero. The Actor rejects unknown snapshot structure, duplicate row keys, invalid or future timestamps, incompatible scope, nonfinite values, unsupported outcome names, and oversized payloads before source collection.
Three public task templates use these exact catalog inputs:
| Task | Input | Local live evidence captured 2026-09-10 UTC |
|---|---|---|
nfl-moneyline-odds-this-week | NFL, moneyline, daysAhead: 7, maxItems: 100 | 15 market rows and 1 free diagnostic |
mlb-moneyline-odds-today | MLB, moneyline, daysAhead: 0, maxItems: 100 | 3 market rows and 1 free diagnostic |
nfl-spreads-totals-this-week | NFL, spread and total, daysAhead: 7, maxItems: 100 | 30 market rows and 1 free diagnostic |
The same NFL moneyline snapshot was supplied to a repeat local live collection. All 15 matching rows were unchanged. That proves the repeat comparison route and also illustrates that unchanged observations remain useful delivered market rows.
4. Pricing
The pricing noun is game market snapshot. The base price is $0.004 per delivered game market snapshot, or $4.00 per 1,000 rows before plan discounts. One row already contains all outcomes for that market; you are not charged separately for the home and away sides or for over and under.
| Apify plan | Price per game market snapshot | Price per 1,000 rows |
|---|---|---|
| Free | $0.0040 | $4.00 |
| Bronze | $0.0036 | $3.60 |
| Silver | $0.0032 | $3.20 |
| Gold | $0.0028 | $2.80 |
| Platinum | $0.0028 | $2.80 |
| Diamond | $0.0028 | $2.80 |
There is no Actor start fee. The pricing configuration has one billable event, result-found, for a delivered game market snapshot. Diagnostic rows are free. Valid empty windows are free. Rows skipped because a complete quote is unavailable are free. The Actor does not use the automatic Dataset-item charging event.
Repeat observations and unchanged rows are billable because each is a newly collected, timestamped current market observation with a delivery receipt. first_run, new, and changed rows are billed by the same rule. Movement labels do not alter the price.
maxItems caps how many game market snapshots the run attempts to deliver. Apify's Max total charge setting provides a separate dollar guard. The Actor checks the verified platform budget before source requests and before each paid write. A verified zero budget stops before ESPN collection. If the remaining platform budget cannot cover another row, the run stops, emits a free BUDGET_STOPPED diagnostic, saves only the acknowledged rows in SNAPSHOT, and reports outcome: "budget_stopped".
If maxItems is smaller than the set of normalized rows, the Actor delivers the cap, emits ITEM_LIMIT_REACHED, and reports a partial result. This makes truncation visible; completed never means that rows were silently withheld by the requested item cap.
The local live evidence in this document shows confirmedResultEvents: 0 and budgetMode: "local_no_charging" because those captures ran outside the Apify platform. A cloud run reconciles confirmed result-found events in OUTPUT. The billing object on a Dataset row states the intent known at write time; the run-level counters are the correct place to reconcile final confirmed events.
5. Input contract
The root input is closed: unknown properties are rejected. Defaults are both leagues, all three markets, seven days ahead, and 100 delivered rows.
| Field | Type | Default | Bounds and meaning |
|---|---|---|---|
leagues | array of strings | ["nfl", "mlb"] | 1–2 unique values; only nfl and mlb |
markets | array of strings | ["moneyline", "spread", "total"] | 1–3 unique values; only the three listed values |
daysAhead | integer | 7 | 0–14; inclusive UTC date horizon |
maxItems | integer | 100 | 1–1,000 delivered game market snapshots |
previousSnapshot | object | omitted | optional exact SNAPSHOT; at most 1,000 rows and 2 MiB serialized input |
League and market order does not create a different semantic scope. The Actor normalizes the selection for comparison. A previous snapshot must name exactly the same league set, market set, and daysAhead. This prevents a narrow run from being compared with a broad baseline and prevents missing rows from being interpreted as market movement.
previousSnapshot.schemaVersion must be 1.0. observedAt must be a real ISO timestamp that is not in the future. Each row must have a unique bounded rowKey and an exact numeric current quote shape appropriate to the market encoded in that key. Allowed numeric leaves are American odds, line, opening American odds, and opening line. Prices and lines must be finite numbers. Links, footers, arbitrary nested objects, and buyer-supplied movement labels do not belong in the comparison baseline.
The input size and snapshot row count are both checked. A generated maximum 1,000-row snapshot is tested to remain below the accepted 2 MiB input boundary. Keeping only numeric comparison values also means a source URL wording change cannot create false odds movement.
Choose maxItems with the row model in mind. If 15 games each have complete moneyline, spread, and total blocks for one provider, selecting all three markets can yield 45 market rows. Availability varies by date and sport, and incomplete blocks reduce the count. The Actor does not manufacture a fixed number of rows to fill the requested limit.
The total source-request bound is 32: two leagues × the maximum 15 requested UTC dates plus one previous scoreboard date for each league. Requests use concurrency 3, an eight-second per-attempt timeout, at most two retries after the first transient failure, a 120-second overall collection deadline, JSON content-type validation, rejected redirects, and a streamed 5 MiB response cap.
6. Real happy, partial, and failure output
The first pair is an ACCEPTED CLOUD CANARY and the second is LOCAL LIVE evidence. Both were captured against the live ESPN Site API on 2026-09-10 UTC. Event identities and prices are observations from those timestamps and may no longer match the source.
ACCEPTED CLOUD CANARY example A — NFL moneyline input
{"leagues": ["nfl"],"markets": ["moneyline"],"daysAhead": 7,"maxItems": 100}
ACCEPTED CLOUD CANARY example A — one exact delivered Dataset row
{"schemaVersion": "1.0","recordType": "odds_market","rowKey": "nfl:401872657:100:moneyline","league": "nfl","event": {"id": "401872657","name": "San Francisco 49ers at Los Angeles Rams","shortName": "SF VS LAR","startTimeUtc": "2026-09-11T00:35:00.000Z","status": {"state": "pre", "completed": false, "description": "Scheduled", "detail": "Thu, September 10th at 8:35 PM EDT"}},"teams": {"away": {"id": "25", "name": "San Francisco 49ers", "abbreviation": "SF"},"home": {"id": "14", "name": "Los Angeles Rams", "abbreviation": "LAR"}},"provider": {"id": "100", "name": "DraftKings", "displayName": "DraftKings"},"market": "moneyline","current": {"home": {"oddsAmerican": -198, "openOddsAmerican": -155},"away": {"oddsAmerican": 164, "openOddsAmerican": 130}},"sourceContext": {"details": "LAR -3.5","spread": -3.5,"overUnder": 48.5,"link": {"href": "https://sportsbook.draftkings.com/gateway?s=__s__&wpcid=__wpcid__&wpsrc=413&wpcn=ESPN&wpscn=Widget&wpcrn=BetSlipDeepLink&wpscid=__wpscid__&wpcrid=xx&preurl=https%3A%2F%2Fsportsbook.draftkings.com%2Fevent%2F34107951","text": "See More Odds","shortText": "Game"},"header": "Game Odds","footer": "GAMBLING PROBLEM? CALL 1-800-GAMBLER or 1-800-MY-RESET, (800) 327-5050 or visit gamblinghelplinema.org (MA). Call 877-8-HOPENY/text HOPENY (467369) (NY). Call 888-789-7777/visit ccpg.org (CT), or visit www.mdgamblinghelp.org (MD), 1-800-981-0023 (PR). 21+ and present in most states. (18+ DC/KY/NH/PR/WY). Void in ONT. Eligibility restrictions apply. Terms: draftkings.com/sportsbook. On behalf of Boot Hill Casino (KS). Pass-thru of per wager tax may apply in IL.","homeTeamOdds": {"favorite": true, "underdog": false, "favoriteAtOpen": true},"awayTeamOdds": {"favorite": false, "underdog": true, "favoriteAtOpen": false}},"provenance": {"source": "ESPN Site API","url": "https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=20260910","requestedDate": "2026-09-10","retrievedAt": "2026-09-10T19:05:30.150Z"},"collectedAt": "2026-09-10T19:05:30.092Z","previous": null,"previousObservedAt": null,"changes": [],"movement": "first_run","billing": {"resultEventRequested": true, "settlement": "unconfirmed_at_write", "platformStartChargeMayApply": true}}
Cloud run b1ITJ3T53GLVLygwW used build 0.1.1 (g9C2TeaFPVCcQz1ib). It delivered 15 market rows, emitted one free source-warning diagnostic, and reported outcome: "completed", sourceComplete: true, deliveryComplete: true, and validEmpty: false. Final settlement confirmed 15 result-found events, no start charge event, and no Dataset auto-charge event. The root acceptance also matched all 15 source identities, game times, and teams to the native run. This owner canary was validation traffic, not customer revenue.
LOCAL LIVE example B — MLB moneyline input
{"leagues": ["mlb"],"markets": ["moneyline"],"daysAhead": 0,"maxItems": 100}
LOCAL LIVE example B — field excerpt from one delivered row (fields omitted)
{"recordType": "odds_market","rowKey": "mlb:401816887:100:moneyline","event": {"id": "401816887","name": "Texas Rangers at Seattle Mariners","startTimeUtc": "2026-09-10T20:10:00.000Z"},"provider": {"id": "100", "name": "DraftKings"},"market": "moneyline","current": {"home": {"oddsAmerican": -122, "openOddsAmerican": -132},"away": {"oddsAmerican": 101, "openOddsAmerican": 109}},"provenance": {"source": "ESPN Site API","requestedDate": "2026-09-10"},"movement": "first_run"}
The complete MLB run found five ESPN events on the requested scoreboard date, three with complete moneyline blocks. It delivered three market rows, emitted one free SOURCE_WARNINGS diagnostic for missing odds, and reported a complete source and complete delivery. Events without a complete quote were not charged.
Partial output
A partial run preserves every successfully delivered row and makes the coverage limit explicit. Common triggers are one or more failed date requests or maxItems truncation. A representative tested summary has this shape:
{"schemaVersion": "1.0","outcome": "partial","deliveredRows": 1,"selectedRows": 15,"sourceComplete": true,"deliveryComplete": false,"itemLimitReached": true,"missingRowsTreatedAsCanceled": false,"leagues": ["nfl"],"markets": ["moneyline"],"daysAhead": 7,"maxItems": 1,"snapshotKey": "SNAPSHOT"}
When a source date fails, sourceComplete is false and the Dataset includes a free SOURCE_PARTIAL diagnostic with failed request receipts. Neither form of partial output creates cancellation rows.
Failure output
Invalid input and incompatible snapshots fail before source collection and before paid delivery. A total source failure writes a free SOURCE_FAILED diagnostic when delivery remains certain, saves a failed OUTPUT when possible, and fails the run. Delivery uncertainty is fail-closed: the Actor does not retry a paid Dataset write whose settlement cannot be established.
{"schemaVersion": "1.0","outcome": "failed","code": "SNAPSHOT_SCOPE_MISMATCH","sourceComplete": false,"deliveryComplete": false,"missingRowsTreatedAsCanceled": false,"deliveredRows": 0,"selectedRows": 0}
Treat the code as a routing key and the run status as authoritative. Do not reinterpret a failed or partial result as a complete zero-market observation.
7. Field dictionary
Odds market rows
| Field | Meaning |
|---|---|
schemaVersion | Output contract version, currently 1.0. |
recordType | odds_market for a useful paid row; diagnostic for a free operational record. |
rowKey | Stable join key: league, ESPN event ID, provider ID, and market. |
league | nfl or mlb. |
event.id | ESPN event identifier used in the stable key. |
event.name | Long away-at-home event label from the source. |
event.shortName | Compact source event label. |
event.startTimeUtc | Parsed ISO event start inside the requested UTC window. |
event.status | Source state, completion flag, description, and detail at collection time. |
teams.away, teams.home | Team ID, name, and abbreviation. Home/away identity determines outcome names. |
provider | Source provider ID, name, and display name. Current live evidence shows DraftKings ID 100. |
market | moneyline, spread, or total. |
current | Exact normalized numeric outcomes for this observation. |
previous | Matching numeric baseline values, or null on first run/new row. |
previousObservedAt | Timestamp of the supplied baseline, or null. |
changes | Numeric leaf changes with path, before, after, and delta. Empty when no comparison difference exists. |
movement | first_run, new, unchanged, or changed. |
sourceContext | Additional ESPN display and betting context retained without treating it as the comparison identity. |
provenance | Source name, exact request URL/date, and retrieval time. |
collectedAt | Observation time for this Actor collection. |
billing | Row-level billing request and settlement state known at write time. |
Current outcome shapes
Moneyline:
{"home": {"oddsAmerican": -198, "openOddsAmerican": -155},"away": {"oddsAmerican": 164, "openOddsAmerican": 130}}
Spread or MLB run line:
{"home": {"oddsAmerican": -112, "line": -3.5, "openOddsAmerican": -120, "openLine": -2.5},"away": {"oddsAmerican": -108, "line": 3.5, "openOddsAmerican": 100, "openLine": 2.5}}
Total:
{"over": {"oddsAmerican": -105, "line": 48.5, "openOddsAmerican": -110, "openLine": 48.5},"under": {"oddsAmerican": -115, "line": 48.5, "openOddsAmerican": -110, "openLine": 48.5}}
Opening values are included only when ESPN supplies real numeric values. Their absence does not cause the current market to be invented or rejected when all required current values are present.
Change entries
A change compares one numeric leaf by identity. For example:
{"path": "home.oddsAmerican","before": -198,"after": -205,"delta": -7}
Paths remain tied to named outcomes, so ordering cannot swap home with away or over with under. Opening values may also appear in changes if the source changes them. That is source-observed data; the Actor does not claim why it changed.
OUTPUT fields
outcome distinguishes completed, empty, partial, budget_stopped, and failed. selectedRows counts normalized rows before the buyer delivery cap; deliveredRows counts acknowledged useful rows. datasetWritesAcknowledged can exceed delivered rows because it includes free diagnostics. confirmedResultEvents records confirmed paid events on-platform. sourceComplete, deliveryComplete, deliveryUncertain, validEmpty, and itemLimitReached let a caller decide whether coverage is safe to use. missingRowsTreatedAsCanceled is deliberately false.
sourceRequests is a bounded provenance ledger. Each successful receipt can include league, requested date, URL, status, attempts, byte count, SHA-256 digest, retrieval time, event count, odds-block count, and normalized row count. Failed receipts carry a stable error code and attempts. The digest identifies bytes observed by that request; it is not a claim that ESPN will retain those bytes.
8. Evidence and boundaries
The implementation was verified against live public scoreboard responses on 2026-09-10 UTC and against captured compact fixtures. The live NFL scoreboard for 2026-09-13 returned 13 events with 13 odds blocks attributed to DraftKings. The live MLB scoreboard for 2026-09-10 returned five events, three of them with odds blocks. Those observations produced the three catalog-task results documented above: 15 NFL moneyline rows, three MLB moneyline rows, and 30 NFL spread/total rows across the inclusive UTC week.
A repeat NFL moneyline capture supplied the prior SNAPSHOT with the same scope. It returned 15 unchanged market rows and preserved the earlier previousObservedAt. Tests also cover first run, changed values, new keys, incompatible snapshots, partial collection, HTTP 403, HTTP 429 retry, timeouts, malformed JSON shape, missing odds, zero budget, one-row budget, one-row item cap, replay protection, content type, redirect refusal, and streamed response limits.
The source is ESPN's public Site API scoreboard route. ESPN currently embeds DraftKings-labeled odds in the observed response. This Actor is independent software; ESPN and DraftKings do not sponsor or endorse it. Source structure, availability, geographic presentation, provider identity, event coverage, and values can change without notice.
This is a current observation collector. It does not stream. It does not maintain server-side odds history. It does not preserve a hidden shared baseline. Movement exists only when the buyer supplies the previous compatible snapshot. The snapshot comparison is one step back, not a time-series database.
The Actor does not promise multiple bookmakers. Although the row identity includes provider so the contract stays unambiguous, current product evidence covers the DraftKings provider delivered through ESPN. A future source response with no supported complete block yields no paid row for that market.
UTC handling is explicit. ESPN scoreboard date grouping can reflect a US sports day, while event starts are timestamps. To recover a late US game that crosses into the requested UTC day, the Actor also reads the previous scoreboard date. It then filters every event to the exact UTC interval. As a result, SOURCE_WARNINGS can include NO_ODDS observations from the recovery date or OUTSIDE_UTC_WINDOW rows that were correctly excluded. Those warnings do not make a complete requested window partial by themselves.
Missing current rows do not prove cancellation. The Actor never emits a synthetic canceled state based on absence. If a business process needs cancellations, confirm them against an authoritative schedule/status source and define its own evidence rule.
Odds can change between collection and use. A source deep link may have eligibility, jurisdiction, age, or availability restrictions. Do not treat a stored value as an offer, execution guarantee, recommendation, or financial outcome.
9. Decision routing
Start with the run status, then read OUTPUT, then process Dataset records by recordType.
| Condition | Recommended machine action |
|---|---|
outcome == "completed" and both completeness flags true | Accept delivered market rows for the selected scope. Preserve SNAPSHOT if another comparison is planned. |
outcome == "empty" and validEmpty == true | Record a valid no-event/no-complete-market observation. Do not open a source incident solely because row count is zero. |
outcome == "partial" | Keep rows as observed facts, mark coverage incomplete, and prevent downstream “full slate” claims. Review diagnostics and request receipts. |
outcome == "budget_stopped" | Keep acknowledged rows, increase the platform charge cap or narrow the input if full delivery is required. |
outcome == "failed" | Quarantine the run from normal ingestion. Route by code; correct input or retry only when the failure is safely retryable. |
deliveryUncertain == true | Reconcile the platform run and Dataset before any retry. Do not assume a paid write failed. |
itemLimitReached == true | Treat the Dataset as a buyer-capped prefix, not a complete market universe. |
SOURCE_PARTIAL diagnostic | Inspect failed dates. Never convert absent rows to canceled or unchanged. |
SOURCE_WARNINGS only, complete summary | Use market rows, but retain warnings as provenance about skipped/ineligible source content. |
Within a complete run, route market rows by movement:
first_run: initialize a baseline or populate the current table;new: flag a currently present row that was absent from the supplied snapshot;changed: send to numeric materiality rules, preserving the complete before/after evidence;unchanged: update the last-observed timestamp if freshness matters; remember that this fresh observation is billable;- absent current key: make no automatic decision because the Actor intentionally emits no removed/canceled row.
A sensible downstream rule uses both coverage and movement. For example, “notify an analyst when an NFL spread line changes by at least 1.0 point” should run only when sourceComplete and deliveryComplete are true, then inspect changes entries whose path ends in .line. The notification should include event, current values, prior timestamp, current timestamp, and provenance. The Actor itself does not send that notification.
10. Commercial playbooks
Weekly NFL market sheet
Run nfl-moneyline-odds-this-week, export odds_market rows keyed by rowKey, and retain event time, current/opening prices, and provenance. Save SNAPSHOT; on the next same-input cycle filter changed and new. Review OUTPUT before replacing the prior sheet.
MLB day board
Run mlb-moneyline-odds-today for the UTC business day. daysAhead: 0 is a UTC day, which may differ from a local editorial day. Display event.startTimeUtc in the presentation timezone and retain no-odds diagnostics.
Spread and total review queue
Run nfl-spreads-totals-this-week. An eligible game can create a spread row and a total row. Route by market, retaining the shared row key and collection time: paired outcomes are the observed and billed unit.
Change audit
Store delivered rows append-only with collectedAt and retain OUTPUT. Exclude partial or budget-stopped observations from rules requiring full coverage, while keeping them as factual point observations.
Data-product ingestion
Use (rowKey, collectedAt) as the observation identity. Keep current as an object and branch on market; this preserves side semantics and optional opening values.
Quality monitoring
Aggregate sourceRequests by league, date, status, attempts, bytes, and odds blocks. More NO_ODDS, retries, or malformed content can signal an upstream change. Retain URL and digest for investigation.
These playbooks do not assert revenue, predictive edge, or saved labor; the Actor does not measure those outcomes.
11. Integration recipes
JavaScript: run, reconcile, and retain the next snapshot
This Node.js example uses bearer authentication. Run start accepts waitForFinish only up to 60 seconds, so it starts once and polls that ID for at most four minutes.
const token = process.env.APIFY_TOKEN;if (!token) throw new Error('APIFY_TOKEN is required');const actorId = 'zinin~nfl-mlb-odds-movement-tracker';const api = 'https://api.apify.com/v2';const input = {leagues: ['nfl'],markets: ['moneyline'],daysAhead: 7,maxItems: 100,};const headers = { authorization: `Bearer ${token}` };const terminal = new Set(['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT']);const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));async function fetchJson(url, options = {}, timeoutMs = 15_000) {const controller = new AbortController();const timer = setTimeout(() => controller.abort(), timeoutMs);try {const response = await fetch(url, { ...options, signal: controller.signal });if (!response.ok) throw new Error(`${options.method ?? 'GET'} ${response.status}: ${url}`);return response.json();} finally {clearTimeout(timer);}}// Do not retry this POST: if its result is uncertain, inspect the platform first.const started = await fetchJson(`${api}/acts/${actorId}/runs?waitForFinish=60&memory=512&maxTotalChargeUsd=0.50`,{method: 'POST',headers: {authorization: `Bearer ${token}`,'content-type': 'application/json',},body: JSON.stringify(input),},70_000,);let run = started.data;if (!run?.id) throw new Error('run start returned no run ID');const deadline = Date.now() + 240_000;while (!terminal.has(run.status)) {const remaining = deadline - Date.now();if (remaining <= 0) throw new Error(`run ${run.id} did not become terminal before the 240-second deadline`);await sleep(Math.min(2_000, remaining));const afterSleep = deadline - Date.now();if (afterSleep <= 0) throw new Error(`run ${run.id} did not become terminal before the 240-second deadline`);const polled = await fetchJson(`${api}/actor-runs/${run.id}`, { headers }, Math.min(15_000, afterSleep));run = polled.data;}if (run.status !== 'SUCCEEDED') throw new Error(`run ended ${run.status}`);const output = await fetchJson(`${api}/key-value-stores/${run.defaultKeyValueStoreId}/records/OUTPUT`, { headers },);if (output.outcome !== 'completed' || !output.sourceComplete || !output.deliveryComplete) {throw new Error(`coverage is not complete: ${JSON.stringify(output)}`);}const records = await fetchJson(`${api}/datasets/${run.defaultDatasetId}/items?clean=true&format=json`, { headers },);const marketRows = records.filter((row) => row.recordType === 'odds_market');const snapshot = await fetchJson(`${api}/key-value-stores/${run.defaultKeyValueStoreId}/records/SNAPSHOT`, { headers },);console.log({ marketRows: marketRows.length, observedAt: snapshot.observedAt });
Do not log the token or put it in the URL. Store the snapshot as JSON under your own access controls, then send it as previousSnapshot with the same scope on the next run.
Python: collect changed rows safely
import jsonimport osimport timeimport urllib.requestTOKEN = os.environ["APIFY_TOKEN"]ACTOR = "zinin~nfl-mlb-odds-movement-tracker"API = "https://api.apify.com/v2"PRIOR_SNAPSHOT_PATH = os.environ.get("PRIOR_SNAPSHOT_PATH")NEXT_SNAPSHOT_PATH = os.environ.get("NEXT_SNAPSHOT_PATH", "snapshot.json")TERMINAL = {"SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"}def request(url, method="GET", body=None, timeout=15):headers = {"Authorization": f"Bearer {TOKEN}"}data = Noneif body is not None:data = json.dumps(body).encode("utf-8")headers["Content-Type"] = "application/json"req = urllib.request.Request(url, data=data, headers=headers, method=method)with urllib.request.urlopen(req, timeout=timeout) as response:if not 200 <= response.status < 300:raise RuntimeError(f"{method} returned HTTP {response.status}")return json.load(response)actor_input = {"leagues": ["nfl"],"markets": ["spread", "total"],"daysAhead": 7,"maxItems": 100,}if PRIOR_SNAPSHOT_PATH:# This file must be the complete SNAPSHOT from the same leagues, markets, and daysAhead.with open(PRIOR_SNAPSHOT_PATH, encoding="utf-8") as prior_file:actor_input["previousSnapshot"] = json.load(prior_file)# Do not retry this POST. A timeout after submission requires platform reconciliation.run = request(f"{API}/acts/{ACTOR}/runs?waitForFinish=60&memory=512&maxTotalChargeUsd=0.50",method="POST",body=actor_input,timeout=70,)["data"]deadline = time.monotonic() + 240while run["status"] not in TERMINAL:remaining = deadline - time.monotonic()if remaining <= 0:raise TimeoutError(f"run {run['id']} did not become terminal within 240 seconds")time.sleep(min(2, remaining))remaining = deadline - time.monotonic()if remaining <= 0:raise TimeoutError(f"run {run['id']} did not become terminal within 240 seconds")run = request(f"{API}/actor-runs/{run['id']}", timeout=min(15, remaining))["data"]if run["status"] != "SUCCEEDED":raise RuntimeError(f"run ended {run['status']}")store_id = run["defaultKeyValueStoreId"]dataset_id = run["defaultDatasetId"]output = request(f"{API}/key-value-stores/{store_id}/records/OUTPUT")if (output["outcome"] != "completed" or not output["sourceComplete"]or not output["deliveryComplete"]):raise RuntimeError("do not treat incomplete coverage as a full slate")records = request(f"{API}/datasets/{dataset_id}/items?clean=true&format=json")snapshot = request(f"{API}/key-value-stores/{store_id}/records/SNAPSHOT")with open(NEXT_SNAPSHOT_PATH, "w", encoding="utf-8") as next_file:json.dump(snapshot, next_file, separators=(",", ":"))changed = [row for row in recordsif row.get("recordType") == "odds_market" and row.get("movement") == "changed"]for row in changed:print(row["event"]["name"], row["market"], row["changes"])
Set PRIOR_SNAPSHOT_PATH only after a complete same-scope run; without it, no row is changed. The script writes NEXT_SNAPSHOT_PATH (default snapshot.json) after all three reads succeed.
SQL warehouse shape
Keep run and row truth separate. A practical design uses an odds_runs table keyed by Actor run ID and an odds_market_observations table keyed by run ID plus rowKey. Store current, previous, changes, sourceContext, and provenance as JSON if your warehouse supports it. Materialize numeric columns only after branching on market.
selectrow_key,event_name,market,movement,collected_at,current_json,previous_jsonfrom odds_market_observationswhere run_id = :run_idand record_type = 'odds_market'and movement in ('new', 'changed')order by start_time_utc, row_key;
Agent or MCP workflow
Give the caller a narrow procedure: run one saved task or the Actor with bounded input; wait for a terminal run; read OUTPUT; stop on failed, partial, budget-stopped, or uncertain delivery unless the user explicitly accepts incomplete coverage; fetch only odds_market Dataset rows; and save SNAPSHOT for a later same-scope call. An agent should report the observation timestamp and source completeness alongside any market change.
Webhook workflow
An Apify run-finished webhook can trigger your own endpoint. Use the run ID from the webhook to retrieve OUTPUT and the Dataset with server-side credentials. Webhook delivery means the run reached a terminal state; it does not mean the outcome was complete. Apply the same decision table before updating a CRM, sheet, database, or notification queue.
12. Operating guide
Before a first run: decide whether UTC today matches the business question. Pick only the markets you will use. Set maxItems high enough for the expected game × market combinations but within the desired spend. Set Apify Max total charge as a monetary backstop.
After a run: inspect terminal status and OUTPUT before consuming Dataset rows. Separate diagnostics from market rows. Check sourceComplete, deliveryComplete, itemLimitReached, and deliveryUncertain. Preserve provenance. Download SNAPSHOT only if you intend to compare another run.
Before a comparison run: keep the same league set, market set, and daysAhead. Use the unmodified prior snapshot. Confirm it is less than 2 MiB and has at most 1,000 rows. Remember that the requested window advances with UTC today, so a same-scope comparison can contain new games as the horizon moves.
When zero rows arrive: distinguish validEmpty from no complete odds, partial source collection, budget stop, and failure. A quiet offseason or empty date can be a valid result. A schedule with missing odds can produce a complete source run with NO_CURRENT_ODDS or NO_ODDS diagnostics. Neither case should be rewritten as “all games canceled.”
When rows are partial: retain their factual status but mark the batch incomplete. If itemLimitReached is true, raise maxItems or narrow league/market scope. If source requests failed, review their codes. HTTP 429, server errors, timeouts, and network failures receive bounded retries; HTTP 403 and malformed responses fail without wasteful repeated requests.
When budget stops: increase Max total charge only after checking expected row volume and price. The saved snapshot contains only acknowledged rows, so it is safe as a record of delivered values but represents partial coverage.
When a write is uncertain: do not immediately rerun. Inspect the Apify run, Dataset, event charges, OUTPUT, and claim record. Replay protection rejects an existing processing claim or confirmed result count rather than risking duplicate paid delivery.
When ESPN changes: keep the raw request receipts and diagnostics. Wrong JSON content type, redirects, oversized response, malformed root, or no valid requests are explicit failures. Report the source URL, date, error code, and run ID when requesting support; do not include your API token.
For recurring use: scheduling belongs to the buyer. Choose a cadence appropriate to the question and expected row cost. This Actor does not create, own, or silently modify schedules. Every scheduled observation that delivers rows is billable, including unchanged rows.
For responsible display: show American odds with their sign, keep team side labels, show UTC or clearly named converted timezone, and retain observation time. Avoid language that promises availability, profit, or prediction accuracy.
13. FAQ
Which sportsbooks are included?
Current verified output contains DraftKings-labeled odds supplied through ESPN's public scoreboard response. The Actor does not promise multiple bookmakers or bookmaker comparison.
Does it scrape DraftKings directly?
No. It requests ESPN Site API scoreboard endpoints and parses the odds blocks ESPN returns. A DraftKings deep link and responsible-gambling footer can appear in sourceContext because they are part of the source block.
Are odds live or streaming?
No. Each run is a timestamped collection. Values may change immediately afterward. Run again for another observation.
Does the Actor keep my history?
No shared persistent history is used. SNAPSHOT is returned in the run's key-value store. You choose whether to retain and supply it later. For longer history, append Dataset rows to your own warehouse.
Why is an unchanged row charged?
It is a new current observation that was fetched, normalized, timestamped, compared, and delivered. Pricing is per delivered game market snapshot, independent of movement label.
What counts as one row?
One game × provider × market. A moneyline row contains home and away. A spread row contains both teams and lines. A total row contains over and under. Outcomes are never billed as separate rows.
Why are some scheduled games missing?
ESPN may not include an odds block, or a selected market may lack complete numeric outcomes. The event can also fall outside the strict UTC interval even if it appears in a queried scoreboard date. Check free diagnostics and sourceRequests.
Does a missing row mean the game was canceled?
No. Missing events and markets are never classified as canceled. Use an authoritative status source if cancellation detection is required.
What does daysAhead: 0 mean?
The current UTC day: from 00:00:00Z inclusive to the next UTC midnight exclusive. It is not the browser's local day.
Why does the Actor request the previous scoreboard date?
US evening games can begin after UTC midnight while remaining grouped under the previous US sports date. The recovery request prevents those games from being missed, after which strict event-time filtering enforces the requested UTC window.
Why can a complete run contain SOURCE_WARNINGS?
Warnings can describe source content that was safely skipped, such as a recovery-date event without odds or a row outside the UTC window. SOURCE_PARTIAL and sourceComplete: false indicate failed request coverage; warnings alone do not.
Can I compare snapshots with different markets?
No. League set, market set, and daysAhead must match exactly. This rule prevents misleading change and absence interpretations.
Can I pass only some snapshot rows?
The structure may technically contain fewer than 1,000 rows, but it should be the complete SNAPSHOT returned by the prior run. Hand-editing it can turn previously observed rows into false new rows.
Are opening odds always available?
No. They appear only when the source provides numeric opening values. Current required values remain the gate for delivering a market.
What happens in the offseason?
A valid empty schedule is reported as outcome: "empty" with validEmpty: true and no paid market rows. A response containing events but no complete selected odds is distinguished with diagnostics.
Can I cap spend?
Yes. Use maxItems for a row cap and Apify Max total charge for a dollar cap. The Actor verifies the platform budget before collection and each paid write.
Is this betting advice?
No. It is structured source observation for research and operations. It does not estimate fair value, predict winners, recommend wagers, or guarantee availability.
Can I use the source link to place a wager?
The source may include a provider deep link, but availability, age, location, account, and other restrictions apply. The link is context, not an execution contract.
What happens when maxItems is reached?
The Actor delivers up to the cap, emits a free diagnostic, marks itemLimitReached: true, sets deliveryComplete: false, and reports a partial outcome.
How do I identify retries or upstream changes?
Inspect OUTPUT.sourceRequests. Successful receipts include attempts, bytes, digest, event count, odds-block count, row count, and retrieval time. Failed receipts carry an error code and attempt count.
Where do I find the reusable snapshot?
In the run's default key-value store under SNAPSHOT. The summary names that key. The Dataset is the paid row delivery channel; the snapshot is a compact comparison baseline, not a second uncharged result export.
14. Sources and rights
The source endpoints are public ESPN Site API scoreboard routes:
- NFL:
https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=YYYYMMDD - MLB:
https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard?dates=YYYYMMDD
Observed responses identify the provider as DraftKings and can include market details, American odds, opening values, spreads, totals, deep links, display headers, favorite flags, and responsible-gambling text. The Actor keeps source attribution and request provenance so buyers can understand where each observation came from.
ESPN, NFL, MLB, DraftKings, team names, league names, and related marks belong to their respective owners. This Actor is an independent data-transformation tool and is not affiliated with, endorsed by, or sponsored by those organizations. The icon and product diagrams are product artwork, not official league, team, ESPN, or DraftKings branding.
Source availability and permitted uses can depend on the source's current terms, the buyer's location, and the buyer's purpose. Buyers are responsible for evaluating their use, retention, redistribution, and display obligations. The Actor does not bypass authentication, paywalls, robots controls, geographic restrictions, or access controls; it uses bounded requests to the public scoreboard endpoints.
American odds and source display text are factual observations at a recorded time. They are not guaranteed offers and may be corrected or withdrawn upstream. Responsible-gambling and eligibility language supplied by the source is retained in sourceContext.footer when present. If you display or act on gambling-related data, follow applicable age, jurisdiction, licensing, and responsible-use requirements.
For reproducibility, retain the Actor run ID, OUTPUT, the relevant Dataset row, and its provenance. The source-request digest records the bytes observed by the Actor, while the normalized row records the values delivered. Neither establishes future access to the original response. Use the evidence within its stated time and coverage boundaries.