#!/usr/bin/env node /** * ParlayAPI historical pull. * * BULK operation — runs for hours, not seconds. Free tier is 1,000 credits * per month, so we walk one (sport, date) at a time with a checkpoint in * Redis so an interrupted run can resume right where it stopped. * * Usage: * node scripts/pull-parlayapi-history.js nba 2025 # one sport / season * node scripts/pull-parlayapi-history.js all # all active sports * node scripts/pull-parlayapi-history.js --resume # continue from checkpoint * node scripts/pull-parlayapi-history.js --dry-run # don't insert * node scripts/pull-parlayapi-history.js --yes # skip confirmation * * Pace: 2 requests / minute. Set PARLAYAPI_PULL_RATE in env to override. */ if (require.main !== module) { throw new Error('Run directly: node scripts/pull-parlayapi-history.js'); } const path = require('path'); const readline = require('readline'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const { getSupabaseServiceClient } = require('../src/utils/supabase'); const { getRedisClient } = require('../src/utils/redis'); const parlayApi = require('../src/services/adapters/parlayApiAdapter'); const { getActiveSports } = require('../src/config/sports'); const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const resume = args.includes('--resume'); const skipConfirm = args.includes('--yes'); const positional = args.filter((a) => !a.startsWith('--')); const sportArg = positional[0]; const seasonArg = positional[1]; const RATE_MS = Number(process.env.PARLAYAPI_PULL_RATE_MS) || 30_000; // 2/min by default const SEASON_DEFAULT = new Date().getFullYear(); function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function confirm(question) { if (skipConfirm) return true; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((r) => rl.question(question, r)); rl.close(); return /^y(es)?$/i.test(answer.trim()); } function* dateRange(season) { // Walk a season's worth of days. ParlayAPI's date arg is YYYY-MM-DD. // For each season we cover Aug 1 → Jul 31 of the following calendar // year — that umbrella catches every sport's annual span. const start = new Date(Date.UTC(season, 7, 1)); const end = new Date(Date.UTC(season + 1, 6, 31)); const cursor = new Date(start); while (cursor <= end) { yield cursor.toISOString().slice(0, 10); cursor.setUTCDate(cursor.getUTCDate() + 1); } } async function checkpointKey(sport, season) { return `parlayapi:checkpoint:${sport}:${season}`; } async function readCheckpoint(sport, season) { const redis = getRedisClient(); return redis.get(await checkpointKey(sport, season)); } async function writeCheckpoint(sport, season, date) { const redis = getRedisClient(); await redis.set(await checkpointKey(sport, season), date, 'EX', 30 * 24 * 60 * 60); } async function processSportSeason(sport, season) { console.log(`[parlayapi-pull] ${sport} ${season} starting (rate=${RATE_MS}ms)`); let totalInserted = 0; let lastSeen = null; if (resume) { lastSeen = await readCheckpoint(sport, season); if (lastSeen) console.log(`[parlayapi-pull] resuming after ${lastSeen}`); } for (const date of dateRange(season)) { if (lastSeen && date <= lastSeen) continue; let lines; try { lines = await parlayApi.getClosingLines(sport, date); } catch (err) { console.warn(`[parlayapi-pull] ${date} failed: ${err.message}`); await sleep(RATE_MS); continue; } if (lines.length && !dryRun) { const supabase = getSupabaseServiceClient(); const rows = lines.map((l) => ({ sport, game_date: date, player_name: l.player ?? l.player_name ?? 'unknown', stat_type: l.stat_type ?? l.market ?? 'unknown', line: Number(l.line ?? l.point ?? 0), closing_line: Number(l.closing_line ?? l.close ?? null) || null, result: l.result ?? l.outcome ?? null, source: 'parlayapi', })); const { error } = await supabase.from('historical_props').insert(rows); if (error) { console.warn(`[parlayapi-pull] insert failed for ${date}: ${error.message}`); } else { totalInserted += rows.length; } } await writeCheckpoint(sport, season, date); if (dryRun) { console.log(`[parlayapi-pull] dry-run ${date} → ${lines.length} lines`); } await sleep(RATE_MS); } console.log(`[parlayapi-pull] ${sport} ${season} done — inserted ${totalInserted} rows`); return totalInserted; } async function main() { if (!parlayApi.configured()) { console.error('PARLAYAPI_KEY is not set'); process.exit(2); } const sportList = sportArg && sportArg !== 'all' ? [sportArg] : getActiveSports().map((s) => s.key); const season = Number(seasonArg) || SEASON_DEFAULT; if (!dryRun) { const ok = await confirm( `This will insert ParlayAPI history for ${sportList.join(', ')} ${season} into ${process.env.SUPABASE_URL || '(unknown)'}. Continue? (y/n) ` ); if (!ok) { console.log('aborted'); process.exit(0); } } for (const sport of sportList) { try { await processSportSeason(sport, season); } catch (err) { console.error(`[parlayapi-pull] ${sport} fatal: ${err.message}`); } } } main().catch((err) => { console.error('[parlayapi-pull] fatal:', err.message); process.exit(1); });