'use strict'; /** * Odds-cache prewarmer (Session 22). * * On the free 500-credit/month odds-api tier this script is a * negative-EV operation: a single fetchAllOdds() call costs * 1 (events lookup) + N (per-event odds) credits, and a 1h-TTL * warmer for one sport would spend ~24 credits/day, blowing the * monthly budget across multiple sports. It only makes sense * once the account is on a higher tier. * * Therefore: gated by `ODDS_PREWARM=1`. The script REFUSES to run * when the flag is unset — operators opt in explicitly per * deployment. The flag check is the first thing main() does so * accidental cron invocations no-op before any provider call. * * Usage (CLI): * node scripts/odds-prefetch.js --sports=nba,mlb,wnba * ODDS_PREWARM=1 node scripts/odds-prefetch.js --dry-run * * Returns a summary object with the per-sport outcome and the * total cost in odds-api credits (read from the tracker delta). * Designed to be wrapped by an internal HTTP endpoint or a cron; * not auto-mounted to either. */ function parseArgs(argv) { const flags = { sports: ['nba', 'wnba', 'mlb'], dryRun: false }; for (const a of argv.slice(2)) { if (a === '--dry-run') flags.dryRun = true; else if (a.startsWith('--sports=')) { const list = a.slice('--sports='.length).split(',').map((s) => s.trim()).filter(Boolean); if (list.length) flags.sports = list; } } return flags; } async function main(argv = process.argv) { const enabled = process.env.ODDS_PREWARM === '1'; const args = parseArgs(argv); const summary = { enabled, dryRun: args.dryRun, sports: {}, creditsSpent: 0, startedAt: new Date().toISOString(), }; if (!enabled) { console.warn('[odds-prefetch] disabled — set ODDS_PREWARM=1 to enable'); summary.skipped = 'not_enabled'; return summary; } // Lazy-require so the script can be loaded for inspection (or // imported by tests) without dragging in axios + redis just to // hit the env-flag short-circuit above. const { getOdds } = require('../src/services/oddsService'); const quotaTracker = require('../src/services/quotaTracker'); const before = await quotaTracker.getQuotaStatus('odds-api'); console.log(`[odds-prefetch] starting — sports=${args.sports.join(',')} dry_run=${args.dryRun} quota_before=${before.used}/${before.limit}`); for (const sport of args.sports) { if (args.dryRun) { summary.sports[sport] = { skipped: 'dry_run' }; console.log(`[odds-prefetch] ${sport}: dry-run`); continue; } // Pre-flight: if quota is already blocked, bail out for the // remaining sports too — no point spending the next sport's // credits on a check that the tracker already failed. const status = await quotaTracker.getQuotaStatus('odds-api'); if (!status.allowed) { summary.sports[sport] = { skipped: 'quota_blocked', pct: status.pct }; console.warn(`[odds-prefetch] ${sport}: skipped — quota ${(status.pct * 100).toFixed(0)}%`); continue; } try { const result = await getOdds(sport); const propCount = Array.isArray(result.props) ? result.props.length : 0; summary.sports[sport] = { source: result.source || 'live', props: propCount, quota_remaining: result.quota_remaining, }; console.log(`[odds-prefetch] ${sport}: ${result.source} (${propCount} props, quota=${result.quota_remaining})`); } catch (err) { summary.sports[sport] = { error: err && err.message ? err.message : String(err) }; console.warn(`[odds-prefetch] ${sport}: failed — ${err && err.message}`); } } const after = await quotaTracker.getQuotaStatus('odds-api'); summary.creditsSpent = Math.max(0, after.used - before.used); summary.quota = { before: before.used, after: after.used, limit: after.limit, pct: after.pct }; console.log(`[odds-prefetch] done — credits_spent=${summary.creditsSpent} quota=${after.used}/${after.limit} (${(after.pct * 100).toFixed(0)}%)`); return summary; } if (require.main === module) { main().then((s) => { process.exit(s.skipped === 'not_enabled' ? 2 : 0); }).catch((err) => { console.error('[odds-prefetch] fatal:', err); process.exit(1); }); } module.exports = { main, __internals: { parseArgs }, };