/** * College Football Data (CFBD) — advanced college analytics. * * 100% free API key. Covers historical betting lines, player usage, team * talent composites, advanced efficiency (PPA), recruiting. nba_api * doesn't cover college; CFBD fills the gap for NCAAB and NCAAFB props. * * Note: CFBD's primary product is college football. College *basketball* * coverage is via the /cbb endpoints. We expose both via the sport-aware * functions below. */ const axios = require('axios'); const { createLimiter, createCircuitBreaker } = require('../../utils/rateLimiter'); const { cacheGet, cacheSet } = require('../../utils/redis'); const SOURCE = 'cfbd'; const HTTP_TIMEOUT_MS = 10_000; const CACHE_TTL_SECONDS = 6 * 60 * 60; // 6h — most CFBD data is daily-fresh const BASE_URL = process.env.CFBD_BASE_URL || 'https://api.collegefootballdata.com'; // Generous free tier — 10 req/min keeps us well under documented limits. const limiter = createLimiter({ tokensPerInterval: 10, interval: 60_000 }); const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 }); function configured() { return !!process.env.CFBD_KEY; } async function fetchWithGuards(url, params, cacheKey) { if (!configured()) return null; const cached = await cacheGet(cacheKey); if (cached) return cached; await limiter.waitForToken(); try { const data = await breaker.call(async () => { const res = await axios.get(url, { params, headers: { Authorization: `Bearer ${process.env.CFBD_KEY}` }, timeout: HTTP_TIMEOUT_MS, validateStatus: (s) => (s >= 200 && s < 300) || s === 429, }); if (res.status === 429) { const err = new Error('cfbd rate limited'); err.code = 'CFBD_429'; throw err; } return res.data; }); await cacheSet(cacheKey, data, CACHE_TTL_SECONDS); return data; } catch (err) { if (err?.code === 'CIRCUIT_OPEN') return null; console.warn(`[${SOURCE}] fetch failed for ${cacheKey}:`, err?.message); return null; } } async function getTeamStats(team, year) { const cacheKey = `cfbd:teamstats:${team}:${year}`; const data = await fetchWithGuards(`${BASE_URL}/stats/season`, { year, team }, cacheKey); return Array.isArray(data) ? data : []; } async function getPlayerUsage(player, team, year) { const cacheKey = `cfbd:usage:${player}:${team}:${year}`; const data = await fetchWithGuards( `${BASE_URL}/player/usage`, { year, team, player }, cacheKey ); return Array.isArray(data) ? data : []; } async function getTalentComposite(team, year) { const cacheKey = `cfbd:talent:${team}:${year}`; const data = await fetchWithGuards(`${BASE_URL}/talent`, { year }, cacheKey); if (!Array.isArray(data)) return null; return data.find((row) => (row.school || row.team) === team) || null; } async function getHistoricalLines(team, year) { const cacheKey = `cfbd:lines:${team}:${year}`; const data = await fetchWithGuards(`${BASE_URL}/lines`, { year, team }, cacheKey); return Array.isArray(data) ? data : []; } module.exports = { configured, getTeamStats, getPlayerUsage, getTalentComposite, getHistoricalLines, __internals: { limiter, breaker, BASE_URL }, };