Session 20: Provider intelligence — quota tracker, gateway with fallback cascade, admin quota dashboard (1476 tests)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provider registry (Session 20).
|
||||
*
|
||||
* Every external data provider VYNDR talks to is enumerated here.
|
||||
* Each entry declares:
|
||||
* - envKey — the environment variable holding the API key
|
||||
* (presence = the provider is configured)
|
||||
* - quotaType — 'monthly' | 'daily' | 'per_minute'
|
||||
* - quotaLimit — calls allowed per quotaType period
|
||||
* - resetDay — for monthly quotas, day-of-month the counter
|
||||
* resets (1 = first of the month). null otherwise.
|
||||
* - sports — which sport keys this provider covers
|
||||
* - capabilities — what kinds of data it can return
|
||||
* - priority — 1 = primary for its capability set; higher
|
||||
* numbers are fallbacks
|
||||
*
|
||||
* The quotaTracker keys off provider IDs from this map. Wiring a
|
||||
* new provider = adding it here + having its adapter route through
|
||||
* providerGateway.fetch(providerId, callback, opts).
|
||||
*
|
||||
* IMPORTANT: keep quotaLimit conservative. Over-counting throttles
|
||||
* the platform under-load; under-counting blows the budget. If a
|
||||
* provider's actual limit changes (e.g. plan upgrade), update this
|
||||
* number — the tracker re-reads it each call.
|
||||
*/
|
||||
|
||||
const PROVIDERS = {
|
||||
// === ODDS / LINES ===
|
||||
'odds-api': {
|
||||
name: 'The Odds API',
|
||||
envKey: 'ODDS_API_KEY',
|
||||
quotaType: 'monthly',
|
||||
quotaLimit: 500,
|
||||
resetDay: 1,
|
||||
sports: ['nba', 'wnba', 'mlb', 'soccer_wc', 'nfl', 'nhl'],
|
||||
capabilities: ['odds', 'props', 'lines', 'spreads'],
|
||||
priority: 1,
|
||||
},
|
||||
'oddspapi': {
|
||||
name: 'ODDSPAPI',
|
||||
envKey: 'ODDSPAPI_KEY',
|
||||
quotaType: 'monthly',
|
||||
quotaLimit: 1000,
|
||||
resetDay: 1,
|
||||
sports: ['nba', 'wnba', 'mlb', 'nfl'],
|
||||
capabilities: ['odds', 'props'],
|
||||
priority: 2,
|
||||
},
|
||||
'parlayapi': {
|
||||
name: 'ParlayAPI',
|
||||
envKey: 'PARLAYAPI_KEY',
|
||||
quotaType: 'monthly',
|
||||
quotaLimit: 1000,
|
||||
resetDay: 1,
|
||||
sports: ['nba', 'wnba', 'mlb', 'nfl'],
|
||||
capabilities: ['odds', 'parlays', 'correlations'],
|
||||
priority: 3,
|
||||
},
|
||||
|
||||
// === STATS / BOX SCORES ===
|
||||
'tank01': {
|
||||
name: 'Tank01 (RapidAPI)',
|
||||
envKey: 'RAPID_API_KEY',
|
||||
quotaType: 'monthly',
|
||||
quotaLimit: 1000,
|
||||
resetDay: 1,
|
||||
sports: ['nba', 'mlb'],
|
||||
capabilities: ['box_scores', 'schedules', 'player_stats', 'bvp'],
|
||||
priority: 1,
|
||||
},
|
||||
|
||||
// === SOCCER ===
|
||||
'api-football': {
|
||||
name: 'API-Football',
|
||||
envKey: 'API_FOOTBALL_KEY',
|
||||
quotaType: 'daily',
|
||||
quotaLimit: 100,
|
||||
resetDay: null,
|
||||
sports: ['soccer_wc', 'soccer'],
|
||||
capabilities: ['lineups', 'player_stats', 'match_events', 'live_scores'],
|
||||
priority: 1,
|
||||
},
|
||||
'football-data': {
|
||||
name: 'Football-Data.org',
|
||||
envKey: 'FOOTBALL_DATA_API_KEY',
|
||||
quotaType: 'per_minute',
|
||||
quotaLimit: 10,
|
||||
resetDay: null,
|
||||
sports: ['soccer_wc', 'soccer'],
|
||||
capabilities: ['standings', 'fixtures', 'scorers'],
|
||||
priority: 2,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Threshold constants — shared by quotaTracker and providerGateway
|
||||
* so the WARN/BLOCK lines stay in lockstep.
|
||||
*/
|
||||
const THRESHOLDS = Object.freeze({
|
||||
WARN_PCT: 0.80,
|
||||
BLOCK_PCT: 0.95,
|
||||
});
|
||||
|
||||
function getProvider(providerId) {
|
||||
return PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
function listProviderIds() {
|
||||
return Object.keys(PROVIDERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of providers whose envKey is set. Logged at startup; used
|
||||
* by the admin dashboard to render only providers the operator has
|
||||
* actually wired up.
|
||||
*/
|
||||
function getConfiguredProviders() {
|
||||
return Object.entries(PROVIDERS)
|
||||
.filter(([, cfg]) => !!process.env[cfg.envKey])
|
||||
.map(([id, cfg]) => ({ id, ...cfg }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback chain for a capability + sport, in priority order,
|
||||
* excluding `excludeId`. Used by the gateway to walk down to the
|
||||
* next provider when the primary is exhausted.
|
||||
*/
|
||||
function getFallbackChain(capability, sport, excludeId) {
|
||||
return Object.entries(PROVIDERS)
|
||||
.filter(([id, cfg]) =>
|
||||
id !== excludeId &&
|
||||
cfg.capabilities.includes(capability) &&
|
||||
(!sport || cfg.sports.includes(sport)) &&
|
||||
!!process.env[cfg.envKey],
|
||||
)
|
||||
.sort((a, b) => a[1].priority - b[1].priority)
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROVIDERS,
|
||||
THRESHOLDS,
|
||||
getProvider,
|
||||
listProviderIds,
|
||||
getConfiguredProviders,
|
||||
getFallbackChain,
|
||||
};
|
||||
Reference in New Issue
Block a user