'use strict'; /** * scripts/propline-audit.js (Session 56) — the data-source audit. * * Inventories what our providers CAN send (from code) and what the FREE * settled-result APIs (MLB Stats, ESPN) actually return, so we can map every * prop stat_type to its box-score field. PropLine live fetch runs only when * PROPLINE_API_KEY_* are present (they aren't in dev) — otherwise we report the * authoritative code inventory (MARKETS × MARKET_MAP). * * Usage: node scripts/propline-audit.js (writes JSON to stdout) */ require('dotenv').config({ quiet: true }); const propline = require('../src/services/adapters/proplineAdapter'); const { MARKET_MAP } = require('../src/utils/oddsNormalizer'); const out = { generatedAt: new Date().toISOString(), sections: {} }; async function getJson(url, headers) { const r = await fetch(url, { headers: headers || {}, signal: AbortSignal.timeout(12000) }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); } // 1. PropLine code inventory — the markets we REQUEST per sport + the stat_type // each normalizes to (or "UNMAPPED" if MARKET_MAP has no entry → silent zero). function proplineInventory() { const { MARKETS, SPORT_KEYS } = propline.__internals; const map = MARKET_MAP || {}; const inv = {}; for (const [sport, markets] of Object.entries(MARKETS)) { inv[sport] = { sportKey: SPORT_KEYS[sport], requested: markets.map((m) => ({ market: m, stat_type: map[m] || 'UNMAPPED' })), }; } // Also list every MARKET_MAP entry (what we CAN normalize even if not requested). inv._allMappedMarkets = Object.entries(map).map(([m, s]) => ({ market: m, stat_type: s })); inv._hasKeys = propline.hasKeys(); return inv; } // 2. The Odds API active sports (FREE — /v4/sports does not spend quota). async function oddsApiSports() { const key = process.env.ODDS_API_KEY; if (!key) return { skipped: 'no ODDS_API_KEY' }; try { const data = await getJson(`https://api.the-odds-api.com/v4/sports?apiKey=${key}`); return (data || []) .filter((s) => s.active) .map((s) => ({ key: s.key, group: s.group, title: s.title })); } catch (e) { return { error: e.message }; } } // 3. MLB Stats API — a recent FINAL game's boxscore field keys + a game-log row. async function mlbBoxscore() { try { // Yesterday's schedule. const d = new Date(Date.now() - 24 * 3600 * 1000).toISOString().slice(0, 10); const sched = await getJson(`https://statsapi.mlb.com/api/v1/schedule?sportId=1&date=${d}`); const games = (sched.dates?.[0]?.games) || []; const final = games.find((g) => g.status?.abstractGameState === 'Final') || games[0]; if (!final) return { note: `no games ${d}` }; const box = await getJson(`https://statsapi.mlb.com/api/v1/game/${final.gamePk}/boxscore`); // Pull one batter + one pitcher stat object. const sampleTeam = box.teams?.away || box.teams?.home || {}; const players = Object.values(sampleTeam.players || {}); const batter = players.find((p) => p.stats?.batting && Object.keys(p.stats.batting).length); const pitcher = players.find((p) => p.stats?.pitching && Object.keys(p.stats.pitching).length); return { date: d, gamePk: final.gamePk, matchup: final.teams?.away?.team?.name + ' @ ' + final.teams?.home?.team?.name, battingFields: batter ? Object.keys(batter.stats.batting) : [], pitchingFields: pitcher ? Object.keys(pitcher.stats.pitching) : [], sampleBatting: batter ? batter.stats.batting : null, samplePitching: pitcher ? pitcher.stats.pitching : null, }; } catch (e) { return { error: e.message }; } } // 4. ESPN WNBA — today's scoreboard status + a completed game's boxscore labels. async function espnWnba() { try { const sb = await getJson('https://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard'); const events = (sb.events || []).map((e) => ({ name: e.name, status: e.status?.type?.description, date: e.date })); const final = (sb.events || []).find((e) => e.status?.type?.completed); let boxLabels = null; if (final) { const summary = await getJson(`https://site.api.espn.com/apis/site/v2/sports/basketball/wnba/summary?event=${final.id}`); const teamStats = summary.boxscore?.players?.[0]?.statistics?.[0]; boxLabels = teamStats ? { labels: teamStats.labels, names: teamStats.names } : null; } return { count: events.length, events, completedBoxLabels: boxLabels }; } catch (e) { return { error: e.message }; } } // 5. ESPN soccer — check a common league scoreboard for activity. async function espnSoccer() { try { const sb = await getJson('https://site.api.espn.com/apis/site/v2/sports/soccer/usa.1/scoreboard'); const events = (sb.events || []).map((e) => ({ name: e.name, status: e.status?.type?.description })); return { league: 'usa.1 (MLS)', count: events.length, events: events.slice(0, 6) }; } catch (e) { return { error: e.message }; } } (async () => { out.sections.proplineInventory = proplineInventory(); out.sections.oddsApiActiveSports = await oddsApiSports(); out.sections.mlbBoxscore = await mlbBoxscore(); out.sections.espnWnba = await espnWnba(); out.sections.espnSoccer = await espnSoccer(); process.stdout.write(JSON.stringify(out, null, 2) + '\n'); })();