Sports Bookmakers Odds Scraper
Pricing
from $2.99 / 1,000 results
Go to Apify Store
Sports Bookmakers Odds Scraper
Scrape sportsbook odds with opening and current lines — moneyline, spreads, totals — for NFL, NBA, MLB, NHL, soccer, UFC, and golf.
Sports Bookmakers Odds Scraper
Pricing
from $2.99 / 1,000 results
Scrape sportsbook odds with opening and current lines — moneyline, spreads, totals — for NFL, NBA, MLB, NHL, soccer, UFC, and golf.
FROM apify/actor-node-puppeteer-chrome# Check preinstalled packagesRUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json# to speed up the build using Docker layer cache.COPY package*.json ./
# Install NPM packages, skip optional and development dependencies to# keep the image small. Avoid logging too much and print the dependency# tree for debuggingRUN npm --quiet set progress=false \ && npm install --omit=dev --omit=optional \ && echo "Installed NPM packages:" \ && (npm list --omit=dev --all || true) \ && echo "Node.js version:" \ && node --version \ && echo "NPM version:" \ && npm --version \ && rm -r ~/.npm
# Next, copy the remaining files and directories with the source code.# Since we do this after NPM install, quick build will be really fast# for most source file changes.COPY . ./
# Run the image.CMD npm start --silent{ "actorSpecification": 1, "name": "sports-bookmakers-odds-scraper", "title": "Empty JavaScript project", "description": "Empty project in JavaScript.", "version": "0.0", "buildTag": "latest", "meta": { "templateId": "js-empty" }, "input": "./input_schema.json", "dockerfile": "./Dockerfile", "storages": { "dataset": "./dataset_schema.json" }, "output": "./output_schema.json"}{ "title": "Sports Bookmakers Odds Scraper Input", "description": "Scrape sportsbook odds (moneyline, spreads, totals) for NFL, NBA, MLB, NHL, soccer, UFC, and golf. One row per outcome quote.", "type": "object", "schemaVersion": 1, "properties": { "leagues": { "title": "Leagues", "type": "array", "description": "League keys. Valid: nfl, ncaaf, nba, wnba, ncaam, ncaaw, mlb, nhl, mls, epl, laliga, seriea, bundesliga, ligue1, ucl, uel, ufc, pga, dpworld, liv. Pass multiple to scrape several leagues in one run.", "editor": "stringList", "prefill": ["nfl"], "default": ["nfl"], "sectionCaption": "Leagues" }, "dates": { "title": "Dates", "type": "string", "description": "Optional. YYYYMMDD for a single day or YYYYMMDD-YYYYMMDD for a UTC date range. Leave empty for today's slate. Golf uses markets currently listed on the board.", "editor": "textfield", "prefill": "", "sectionCaption": "Dates" }, "oddsFormat": { "title": "Odds format", "type": "string", "description": "Format for the primary price and openPrice fields. American and decimal columns are always included on each row.", "editor": "select", "enum": ["american", "decimal"], "enumTitles": ["American", "Decimal"], "default": "american", "prefill": "american", "sectionCaption": "Odds" }, "maxItems": { "title": "Max items", "type": "integer", "description": "Maximum rows (quotes) to return. 0 = no cap. Each row is one outcome from one bookmaker for one market.", "minimum": 0, "maximum": 100000, "default": 0, "prefill": 0, "editor": "number", "sectionCaption": "Limits" } }, "required": []}1import { Actor } from 'apify';2
3/**4 * Sports bookmakers odds via public ESPN Site + Core APIs.5 * One dataset row per outcome quote (event × bookmaker × market × outcome).6 */7
8const SITE = 'https://site.api.espn.com/apis/site/v2/sports';9const CORE = 'https://sports.core.api.espn.com/v2/sports';10
11const LEAGUES = {12 nfl: { sport: 'football', league: 'nfl', name: 'NFL', kind: 'team' },13 ncaaf: { sport: 'football', league: 'college-football', name: 'NCAA Football', kind: 'team' },14 nba: { sport: 'basketball', league: 'nba', name: 'NBA', kind: 'team' },15 wnba: { sport: 'basketball', league: 'wnba', name: 'WNBA', kind: 'team' },16 ncaam: { sport: 'basketball', league: 'mens-college-basketball', name: "NCAA Men's Basketball", kind: 'team' },17 ncaaw: { sport: 'basketball', league: 'womens-college-basketball', name: "NCAA Women's Basketball", kind: 'team' },18 mlb: { sport: 'baseball', league: 'mlb', name: 'MLB', kind: 'team' },19 nhl: { sport: 'hockey', league: 'nhl', name: 'NHL', kind: 'team' },20 mls: { sport: 'soccer', league: 'usa.1', name: 'MLS', kind: 'soccer' },21 epl: { sport: 'soccer', league: 'eng.1', name: 'English Premier League', kind: 'soccer' },22 laliga: { sport: 'soccer', league: 'esp.1', name: 'La Liga', kind: 'soccer' },23 seriea: { sport: 'soccer', league: 'ita.1', name: 'Serie A', kind: 'soccer' },24 bundesliga: { sport: 'soccer', league: 'ger.1', name: 'Bundesliga', kind: 'soccer' },25 ligue1: { sport: 'soccer', league: 'fra.1', name: 'Ligue 1', kind: 'soccer' },26 ucl: { sport: 'soccer', league: 'uefa.champions', name: 'UEFA Champions League', kind: 'soccer' },27 uel: { sport: 'soccer', league: 'uefa.europa', name: 'UEFA Europa League', kind: 'soccer' },28 ufc: { sport: 'mma', league: 'ufc', name: 'UFC', kind: 'mma' },29 pga: { sport: 'golf', league: 'pga', name: 'PGA Tour', kind: 'golf' },30 dpworld: { sport: 'golf', league: 'eur', name: 'DP World Tour', kind: 'golf' },31 liv: { sport: 'golf', league: 'liv', name: 'LIV Golf', kind: 'golf' },32};33
34const VALID_LEAGUE_KEYS = new Set(Object.keys(LEAGUES));35
36class SportsBookmakersOddsScraper {37 async sleep(ms) {38 await new Promise((r) => setTimeout(r, ms));39 }40
41 async fetchJson(url, { retries = 3 } = {}) {42 let lastError;43 for (let i = 1; i <= retries; i += 1) {44 try {45 const response = await fetch(url, {46 headers: {47 accept: 'application/json',48 'user-agent':49 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',50 },51 });52 if (response.status === 429 || response.status >= 500) {53 await this.sleep(400 * i);54 continue;55 }56 if (response.status === 404) return null;57 if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);58 return response.json();59 } catch (error) {60 lastError = error;61 await this.sleep(300 * i);62 }63 }64 throw lastError || new Error(`Failed ${url}`);65 }66
67 async resolveRef(obj) {68 if (!obj || typeof obj !== 'object') return obj;69 if (obj.$ref && Object.keys(obj).length === 1) {70 return this.fetchJson(obj.$ref);71 }72 if (obj.$ref && !obj.provider && !obj.homeTeamOdds && !obj.awayTeamOdds && !obj.homeAthleteOdds) {73 return this.fetchJson(obj.$ref);74 }75 return obj;76 }77
78 /**79 * @returns {string[] | null} null = omit dates param (ESPN default slate)80 */81 parseDates(raw) {82 const s = String(raw || '').trim();83 if (!s) return null;84 const range = s.match(/^(\d{8})\s*-\s*(\d{8})$/);85 if (range) {86 const start = range[1];87 const end = range[2];88 if (start > end) throw new Error(`Invalid dates range: ${s}`);89 const out = [];90 let cur = start;91 while (cur <= end) {92 out.push(cur);93 if (out.length > 60) throw new Error('Date range too large (max 60 days)');94 cur = this.addDaysYyyymmdd(cur, 1);95 }96 return out;97 }98 if (!/^\d{8}$/.test(s)) throw new Error(`Invalid dates: use YYYYMMDD or YYYYMMDD-YYYYMMDD, got "${s}"`);99 return [s];100 }101
102 addDaysYyyymmdd(yyyymmdd, days) {103 const y = Number(yyyymmdd.slice(0, 4));104 const m = Number(yyyymmdd.slice(4, 6)) - 1;105 const d = Number(yyyymmdd.slice(6, 8));106 const dt = new Date(Date.UTC(y, m, d));107 dt.setUTCDate(dt.getUTCDate() + days);108 const yy = dt.getUTCFullYear();109 const mm = String(dt.getUTCMonth() + 1).padStart(2, '0');110 const dd = String(dt.getUTCDate()).padStart(2, '0');111 return `${yy}${mm}${dd}`;112 }113
114 normalizeLeagues(value) {115 const list = Array.isArray(value) ? value : value ? [value] : ['nfl'];116 const keys = [];117 for (const item of list) {118 const k = String(item || '')119 .trim()120 .toLowerCase();121 if (!k) continue;122 if (!VALID_LEAGUE_KEYS.has(k)) {123 console.warn(`Skipping unknown league key: ${k}`);124 continue;125 }126 if (!keys.includes(k)) keys.push(k);127 }128 return keys.length ? keys : ['nfl'];129 }130
131 parseAmerican(raw) {132 if (raw == null || raw === '') return null;133 if (typeof raw === 'number' && Number.isFinite(raw)) return raw;134 const s = String(raw).trim().replace(/^\+/, '');135 const n = Number(s);136 return Number.isFinite(n) ? n : null;137 }138
139 parsePoint(raw) {140 if (raw == null || raw === '') return null;141 if (typeof raw === 'number' && Number.isFinite(raw)) return raw;142 const m = String(raw).trim().match(/([+-]?\d+(?:\.\d+)?)/);143 return m ? Number(m[1]) : null;144 }145
146 americanToDecimal(american) {147 if (american == null || !Number.isFinite(american) || american === 0) return null;148 if (american > 0) return Number((1 + american / 100).toFixed(4));149 return Number((1 + 100 / Math.abs(american)).toFixed(4));150 }151
152 pickAmerican(node) {153 if (node == null) return null;154 if (typeof node === 'number') return this.parseAmerican(node);155 if (typeof node === 'string') return this.parseAmerican(node);156 if (typeof node === 'object') {157 return this.parseAmerican(node.american ?? node.alternateDisplayValue ?? node.value);158 }159 return null;160 }161
162 pickDecimal(node, americanFallback) {163 if (node && typeof node === 'object' && node.decimal != null) {164 const d = Number(node.decimal);165 if (Number.isFinite(d)) return Number(d.toFixed(4));166 }167 return this.americanToDecimal(americanFallback);168 }169
170 pickPointFromOddsSide(side) {171 if (!side || typeof side !== 'object') return null;172 return this.parsePoint(side.pointSpread?.alternateDisplayValue ?? side.pointSpread?.american);173 }174
175 buildRow({176 leagueKey,177 meta,178 event,179 bookmaker,180 marketKey,181 outcomeName,182 priceAmerican,183 point,184 openPriceAmerican,185 openPoint,186 details,187 oddsFormat,188 fetchedAt,189 }) {190 const priceDecimal = this.americanToDecimal(priceAmerican);191 const openPriceDecimal = this.americanToDecimal(openPriceAmerican);192 const price =193 oddsFormat === 'decimal' ? priceDecimal : priceAmerican;194 const openPrice =195 oddsFormat === 'decimal' ? openPriceDecimal : openPriceAmerican;196
197 let priceMovement = null;198 if (priceDecimal != null && openPriceDecimal != null) {199 priceMovement = Number((priceDecimal - openPriceDecimal).toFixed(4));200 }201 let pointMovement = null;202 if (point != null && openPoint != null) {203 pointMovement = Number((point - openPoint).toFixed(4));204 }205
206 return {207 league: leagueKey,208 leagueName: meta.name,209 eventId: String(event.eventId),210 eventName: event.eventName,211 commenceTime: event.commenceTime,212 status: event.status,213 completed: event.completed,214 venue: event.venue,215 homeTeam: event.homeTeam,216 homeTeamAbbr: event.homeTeamAbbr,217 homeScore: event.homeScore,218 awayTeam: event.awayTeam,219 awayTeamAbbr: event.awayTeamAbbr,220 awayScore: event.awayScore,221 bookmaker: bookmaker || null,222 marketKey,223 outcomeName,224 price,225 priceAmerican,226 priceDecimal,227 point: point ?? null,228 openPrice,229 openPriceAmerican: openPriceAmerican ?? null,230 openPriceDecimal,231 openPoint: openPoint ?? null,232 priceMovement,233 pointMovement,234 oddsFormat,235 details: details || null,236 fetchedAt,237 };238 }239
240 extractEventMeta(event, competition) {241 const comps = competition || (event.competitions || [])[0] || {};242 const competitors = comps.competitors || [];243 const home = competitors.find((c) => c.homeAway === 'home') || competitors[0] || {};244 const away = competitors.find((c) => c.homeAway === 'away') || competitors[1] || {};245
246 const homeName =247 home.team?.displayName ||248 home.athlete?.displayName ||249 home.displayName ||250 null;251 const awayName =252 away.team?.displayName ||253 away.athlete?.displayName ||254 away.displayName ||255 null;256 const homeAbbr =257 home.team?.abbreviation || home.athlete?.abbreviation || home.abbreviation || null;258 const awayAbbr =259 away.team?.abbreviation || away.athlete?.abbreviation || away.abbreviation || null;260
261 const statusType = comps.status?.type || event.status?.type || {};262 const homeScoreRaw = home.score;263 const awayScoreRaw = away.score;264 const statusName = statusType.name || statusType.description || null;265 const scheduled = /SCHEDULED|STATUS_SCHEDULED|PRE/i.test(String(statusName || ''));266 const toScore = (raw) => {267 if (scheduled) return null;268 if (raw === '' || raw == null) return null;269 const n = Number(raw);270 return Number.isFinite(n) ? n : null;271 };272
273 return {274 eventId: comps.id || event.id,275 parentEventId: event.id,276 eventName: event.name || event.shortName || `${awayName || '?'} at ${homeName || '?'}`,277 commenceTime: event.date || comps.date || null,278 status: statusName,279 completed: !!statusType.completed,280 venue: comps.venue?.fullName || event.venue?.fullName || null,281 homeTeam: homeName,282 homeTeamAbbr: homeAbbr,283 homeScore: toScore(homeScoreRaw),284 awayTeam: awayName,285 awayTeamAbbr: awayAbbr,286 awayScore: toScore(awayScoreRaw),287 competitionId: comps.id || event.id,288 };289 }290
291 flattenOddsItem(oddsItem, ctx) {292 const rows = [];293 const { leagueKey, meta, event, oddsFormat, fetchedAt } = ctx;294 const bookmaker = oddsItem.provider?.name || oddsItem.provider?.displayName || null;295 const details = oddsItem.details || null;296
297 const homeSide = oddsItem.homeTeamOdds || oddsItem.homeAthleteOdds || null;298 const awaySide = oddsItem.awayTeamOdds || oddsItem.awayAthleteOdds || null;299 const draw = oddsItem.drawOdds || null;300
301 const homeName = event.homeTeam || 'Home';302 const awayName = event.awayTeam || 'Away';303
304 // --- h2h / moneyline ---305 if (homeSide || awaySide || draw) {306 const pushMl = (outcomeName, side) => {307 if (!side) return;308 const curNode = side.current?.moneyLine || side.moneyLine;309 const openNode = side.open?.moneyLine;310 const priceAmerican =311 this.pickAmerican(typeof curNode === 'object' ? curNode : { american: curNode }) ??312 this.parseAmerican(side.moneyLine);313 if (priceAmerican == null) return;314 const openPriceAmerican = this.pickAmerican(openNode);315 rows.push(316 this.buildRow({317 leagueKey,318 meta,319 event,320 bookmaker,321 marketKey: 'h2h',322 outcomeName,323 priceAmerican,324 point: null,325 openPriceAmerican,326 openPoint: null,327 details,328 oddsFormat,329 fetchedAt,330 }),331 );332 };333 pushMl(awayName, awaySide);334 pushMl(homeName, homeSide);335 if (draw && meta.kind === 'soccer') {336 const priceAmerican = this.parseAmerican(draw.moneyLine ?? draw.current?.moneyLine);337 if (priceAmerican != null) {338 const openPriceAmerican = this.pickAmerican(draw.open?.moneyLine);339 rows.push(340 this.buildRow({341 leagueKey,342 meta,343 event,344 bookmaker,345 marketKey: 'h2h',346 outcomeName: 'Draw',347 priceAmerican,348 point: null,349 openPriceAmerican,350 openPoint: null,351 details,352 oddsFormat,353 fetchedAt,354 }),355 );356 }357 }358 }359
360 // --- spreads ---361 if (homeSide || awaySide) {362 const spreadHome = oddsItem.spread;363 const pushSpread = (outcomeName, side, pointSignFlip) => {364 if (!side) return;365 const curSpreadNode = side.current?.spread;366 const openSpreadNode = side.open?.spread;367 const priceAmerican =368 this.pickAmerican(curSpreadNode) ?? this.parseAmerican(side.spreadOdds);369 if (priceAmerican == null) return;370
371 let point = this.pickPointFromOddsSide(side.current) ?? this.pickPointFromOddsSide(side);372 if (point == null && spreadHome != null) {373 // ESPN `spread` is typically home line; away is opposite374 point = pointSignFlip ? -Number(spreadHome) : Number(spreadHome);375 }376 const openPoint = this.pickPointFromOddsSide(side.open);377 const openPriceAmerican = this.pickAmerican(openSpreadNode);378
379 rows.push(380 this.buildRow({381 leagueKey,382 meta,383 event,384 bookmaker,385 marketKey: 'spreads',386 outcomeName,387 priceAmerican,388 point,389 openPriceAmerican,390 openPoint,391 details,392 oddsFormat,393 fetchedAt,394 }),395 );396 };397 // homeTeamOdds.spread point is home line; away is opposite398 pushSpread(homeName, homeSide, false);399 pushSpread(awayName, awaySide, true);400 }401
402 // --- totals ---403 const overUnder = this.parsePoint(oddsItem.overUnder);404 const openTotal = this.parsePoint(oddsItem.open?.total?.alternateDisplayValue ?? oddsItem.open?.total?.american);405 const overAmerican =406 this.pickAmerican(oddsItem.current?.over) ?? this.parseAmerican(oddsItem.overOdds);407 const underAmerican =408 this.pickAmerican(oddsItem.current?.under) ?? this.parseAmerican(oddsItem.underOdds);409 const openOver = this.pickAmerican(oddsItem.open?.over);410 const openUnder = this.pickAmerican(oddsItem.open?.under);411
412 if (overUnder != null && overAmerican != null) {413 rows.push(414 this.buildRow({415 leagueKey,416 meta,417 event,418 bookmaker,419 marketKey: 'totals',420 outcomeName: 'Over',421 priceAmerican: overAmerican,422 point: overUnder,423 openPriceAmerican: openOver,424 openPoint: openTotal,425 details,426 oddsFormat,427 fetchedAt,428 }),429 );430 }431 if (overUnder != null && underAmerican != null) {432 rows.push(433 this.buildRow({434 leagueKey,435 meta,436 event,437 bookmaker,438 marketKey: 'totals',439 outcomeName: 'Under',440 priceAmerican: underAmerican,441 point: overUnder,442 openPriceAmerican: openUnder,443 openPoint: openTotal,444 details,445 oddsFormat,446 fetchedAt,447 }),448 );449 }450
451 return rows;452 }453
454 async flattenPropBets(oddsItem, ctx) {455 const rows = [];456 const pb = oddsItem.propBets;457 if (!pb) return rows;458
459 let data = pb;460 if (pb.$ref) data = await this.fetchJson(pb.$ref);461 const items = data?.items || (Array.isArray(data) ? data : []);462 const bookmaker = oddsItem.provider?.name || oddsItem.provider?.displayName || null;463
464 for (const raw of items) {465 const item = await this.resolveRef(raw);466 if (!item) continue;467 const typeName = item.type?.name || '';468 const lower = typeName.toLowerCase();469 let marketKey = 'props';470 if (/ko|tko|submission|decision|method/i.test(lower)) marketKey = 'method_of_victory';471 else if (/distance|go the distance/i.test(lower)) marketKey = 'props';472
473 const american = this.parseAmerican(item.odds?.american?.value);474 if (american == null) continue;475 const openAmerican = this.parseAmerican(item.odds?.american?.open);476
477 // Prop items often come in Yes/No pairs without labels — use type name + american sign as label478 const outcomeName = typeName || 'Prop';479 rows.push(480 this.buildRow({481 leagueKey: ctx.leagueKey,482 meta: ctx.meta,483 event: ctx.event,484 bookmaker,485 marketKey,486 outcomeName,487 priceAmerican: american,488 point: this.parsePoint(item.odds?.total?.value),489 openPriceAmerican: openAmerican,490 openPoint: null,491 details: oddsItem.details || typeName,492 oddsFormat: ctx.oddsFormat,493 fetchedAt: ctx.fetchedAt,494 }),495 );496 }497 return rows;498 }499
500 async fetchOddsForCompetition(metaCfg, eventMeta) {501 const { sport, league } = metaCfg;502 const eid = eventMeta.parentEventId;503 const cid = eventMeta.competitionId;504 const url = `${CORE}/${sport}/leagues/${league}/events/${eid}/competitions/${cid}/odds`;505 const listing = await this.fetchJson(url);506 if (!listing?.items?.length) return [];507
508 const resolved = [];509 for (const item of listing.items) {510 const full = await this.resolveRef(item);511 if (full) resolved.push(full);512 await this.sleep(40);513 }514 return resolved;515 }516
517 async collectLeagueDay(leagueKey, date, oddsFormat, maxItems, fetchedAt, state) {518 const meta = LEAGUES[leagueKey];519 const qs = date ? `?dates=${date}` : '';520 const url = `${SITE}/${meta.sport}/${meta.league}/scoreboard${qs}`;521 const board = await this.fetchJson(url);522 const events = board?.events || [];523 console.log(`[${leagueKey}] ${date || 'default-slate'}: ${events.length} event(s)`);524
525 for (const event of events) {526 if (state.done) return;527 const competitions =528 meta.kind === 'mma' || meta.kind === 'golf'529 ? event.competitions || []530 : [(event.competitions || [])[0]].filter(Boolean);531
532 for (const competition of competitions) {533 if (state.done) return;534 const eventMeta = this.extractEventMeta(event, competition);535
536 // Golf stroke-play boards rarely expose bookmaker game odds — skip quietly537 if (meta.kind === 'golf') {538 // Still try once per competition; empty is fine539 }540
541 let oddsItems = [];542 try {543 oddsItems = await this.fetchOddsForCompetition(meta, eventMeta);544 } catch (error) {545 console.warn(`Odds failed ${leagueKey} ${eventMeta.eventId}: ${error.message}`);546 continue;547 }548
549 const ctx = { leagueKey, meta, event: eventMeta, oddsFormat, fetchedAt };550 for (const oddsItem of oddsItems) {551 if (state.done) return;552 const baseRows = this.flattenOddsItem(oddsItem, ctx);553 let propRows = [];554 if (meta.kind === 'mma') {555 try {556 propRows = await this.flattenPropBets(oddsItem, ctx);557 } catch (error) {558 console.warn(`Props failed ${eventMeta.eventId}: ${error.message}`);559 }560 }561 for (const row of [...baseRows, ...propRows]) {562 if (maxItems > 0 && state.count >= maxItems) {563 state.done = true;564 return;565 }566 await Actor.pushData(row);567 state.count += 1;568 }569 }570 await this.sleep(60);571 }572 }573 }574
575 async run(input) {576 const leagues = this.normalizeLeagues(input.leagues);577 const dates = this.parseDates(input.dates);578 const dateList = dates || [null];579 const oddsFormat = String(input.oddsFormat || 'american').toLowerCase() === 'decimal' ? 'decimal' : 'american';580 const maxItems = Math.max(0, Number(input.maxItems ?? 0) || 0);581 const fetchedAt = new Date().toISOString();582
583 const datesLabel = dates584 ? `${dates[0]}${dates.length > 1 ? `..${dates[dates.length - 1]}` : ''}`585 : 'default-slate';586 console.log(587 `leagues=${leagues.join(',')}, dates=${datesLabel}, format=${oddsFormat}, maxItems=${maxItems || '∞'}`,588 );589
590 const state = { count: 0, done: false };591 for (const leagueKey of leagues) {592 if (state.done) break;593 for (const date of dateList) {594 if (state.done) break;595 // Golf: one board pass (ignore multi-day expansion)596 if (LEAGUES[leagueKey].kind === 'golf' && date !== dateList[0]) continue;597 try {598 await this.collectLeagueDay(leagueKey, date, oddsFormat, maxItems, fetchedAt, state);599 } catch (error) {600 console.warn(`League day failed ${leagueKey} ${date || 'default'}: ${error.message}`);601 }602 await this.sleep(120);603 }604 }605
606 console.log(`Done. Saved ${state.count} odds row(s).`);607 }608}609
610await Actor.init();611
612Actor.main(async () => {613 const input = (await Actor.getInput()) || {614 leagues: ['nfl'],615 oddsFormat: 'american',616 maxItems: 50,617 };618 await new SportsBookmakersOddsScraper().run(input);619});# configurations.idea
# crawlee and apify storage foldersapify_storagecrawlee_storagestorage
# installed filesnode_modules
# git folder.gitroot = true
[*]indent_style = spaceindent_size = 4charset = utf-8trim_trailing_whitespace = trueinsert_final_newline = trueend_of_line = lf{ "extends": "@apify", "root": true}# This file tells Git which files shouldn't be added to source control
.DS_Store.idea.zednode_modulesstorage{ "name": "js-scrape-single-page", "version": "0.0.1", "type": "module", "description": "This is an example of an Apify actor.", "engines": { "node": ">=18.0.0" }, "dependencies": { "apify": "^3.4.4", "axios": "^1.7.7", "node-fetch": "3", "cheerio": "^1.0.0", "crypto-js": "^4.1.1", "ioredis": "^5.4.1", "puppeteer-real-browser": "^1.4.4" }, "scripts": { "start": "node ./src/main.js", "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1" }, "author": "It's not you it's me", "license": "ISC"}