Codex commited on
Commit
45751a3
·
1 Parent(s): 262b7bd

Reduce odds API load and preserve scan sources

Browse files
src/config.js CHANGED
@@ -9,6 +9,11 @@ export function getConfig() {
9
  const oddsApiBaseUrl = process.env.ODDS_API_BASE_URL?.trim() || 'https://api.the-odds-api.com/v4';
10
  const oddsApiSportKey = process.env.ODDS_API_SPORT_KEY?.trim() || 'baseball_mlb';
11
  const oddsApiRegions = process.env.ODDS_API_REGIONS?.trim() || 'us';
 
 
 
 
 
12
  const oddsApiMarkets = (process.env.ODDS_API_MARKETS?.trim()
13
  || 'batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts')
14
  .split(',')
@@ -63,6 +68,7 @@ export function getConfig() {
63
  oddsApiBaseUrl,
64
  oddsApiSportKey,
65
  oddsApiRegions,
 
66
  oddsApiMarkets,
67
  circaDropboxUrl,
68
  scanReportChannelId,
 
9
  const oddsApiBaseUrl = process.env.ODDS_API_BASE_URL?.trim() || 'https://api.the-odds-api.com/v4';
10
  const oddsApiSportKey = process.env.ODDS_API_SPORT_KEY?.trim() || 'baseball_mlb';
11
  const oddsApiRegions = process.env.ODDS_API_REGIONS?.trim() || 'us';
12
+ const oddsApiBookmakers = (process.env.ODDS_API_BOOKMAKERS?.trim()
13
+ || 'fanduel,draftkings,betmgm,williamhill_us')
14
+ .split(',')
15
+ .map((value) => value.trim())
16
+ .filter(Boolean);
17
  const oddsApiMarkets = (process.env.ODDS_API_MARKETS?.trim()
18
  || 'batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts')
19
  .split(',')
 
68
  oddsApiBaseUrl,
69
  oddsApiSportKey,
70
  oddsApiRegions,
71
+ oddsApiBookmakers,
72
  oddsApiMarkets,
73
  circaDropboxUrl,
74
  scanReportChannelId,
src/db.js CHANGED
@@ -920,6 +920,10 @@ export class BetStore {
920
  entryCount: Number(snapshotRow.entry_count),
921
  entries: entryResult.rows.map((entry) => ({
922
  marketKey: entry.market_key,
 
 
 
 
923
  playerName: entry.player_name,
924
  team: entry.team,
925
  marketType: entry.market_type,
 
920
  entryCount: Number(snapshotRow.entry_count),
921
  entries: entryResult.rows.map((entry) => ({
922
  marketKey: entry.market_key,
923
+ source: 'circa',
924
+ book: 'Circa',
925
+ eventName: 'Circa MLB',
926
+ eventId: null,
927
  playerName: entry.player_name,
928
  team: entry.team,
929
  marketType: entry.market_type,
src/market-scanner.js CHANGED
@@ -491,9 +491,10 @@ export function normalizeOddsApiEntries(payload = []) {
491
  point: outcome.point,
492
  });
493
  const lineValue = normalizeLine(outcome.point, side, market.key);
 
494
  const entry = {
495
- source: bookmaker.title === 'Circa' ? 'circa' : 'odds_api',
496
- book: bookmaker.title,
497
  eventName: `${event.away_team} @ ${event.home_team}`,
498
  eventId: event.id,
499
  team: null,
@@ -2631,7 +2632,11 @@ export async function fetchOddsApiEntries(config) {
2631
  for (const event of events) {
2632
  const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
2633
  oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
2634
- oddsUrl.searchParams.set('regions', config.oddsApiRegions);
 
 
 
 
2635
  oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
2636
  oddsUrl.searchParams.set('oddsFormat', 'american');
2637
  oddsUrl.searchParams.set('dateFormat', 'iso');
@@ -2640,6 +2645,12 @@ export async function fetchOddsApiEntries(config) {
2640
  if (response.status === 404 || response.status === 422) {
2641
  continue;
2642
  }
 
 
 
 
 
 
2643
  if (!response.ok) {
2644
  throw new Error(`Odds API request failed with ${response.status}`);
2645
  }
 
491
  point: outcome.point,
492
  });
493
  const lineValue = normalizeLine(outcome.point, side, market.key);
494
+ const bookName = normalizeWhitespace(bookmaker.title || bookmaker.key || 'Unknown book');
495
  const entry = {
496
+ source: 'odds_api',
497
+ book: bookName,
498
  eventName: `${event.away_team} @ ${event.home_team}`,
499
  eventId: event.id,
500
  team: null,
 
2632
  for (const event of events) {
2633
  const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
2634
  oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
2635
+ if (Array.isArray(config.oddsApiBookmakers) && config.oddsApiBookmakers.length > 0) {
2636
+ oddsUrl.searchParams.set('bookmakers', config.oddsApiBookmakers.join(','));
2637
+ } else {
2638
+ oddsUrl.searchParams.set('regions', config.oddsApiRegions);
2639
+ }
2640
  oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
2641
  oddsUrl.searchParams.set('oddsFormat', 'american');
2642
  oddsUrl.searchParams.set('dateFormat', 'iso');
 
2645
  if (response.status === 404 || response.status === 422) {
2646
  continue;
2647
  }
2648
+ if (response.status === 429) {
2649
+ if (payload.length > 0) {
2650
+ break;
2651
+ }
2652
+ throw new Error('Odds API request failed with 429. Narrow ODDS_API_BOOKMAKERS or reduce scan frequency.');
2653
+ }
2654
  if (!response.ok) {
2655
  throw new Error(`Odds API request failed with ${response.status}`);
2656
  }
test/market-scanner.test.js CHANGED
@@ -44,6 +44,8 @@ test('normalizes odds api entries into shared market keys', () => {
44
  assert.equal(entries.length, 1);
45
  assert.equal(entries[0].marketKey, buildMarketKey(entries[0]));
46
  assert.equal(entries[0].side, 'over');
 
 
47
  });
48
 
49
  test('parses circa OCR lines into normalized entries', () => {
@@ -394,6 +396,7 @@ test('fetches odds api entries from event-level endpoints', async () => {
394
  oddsApiSportKey: 'baseball_mlb',
395
  oddsApiKey: 'test-key',
396
  oddsApiRegions: 'us',
 
397
  oddsApiMarkets: ['batter_total_bases'],
398
  });
399
 
@@ -401,6 +404,78 @@ test('fetches odds api entries from event-level endpoints', async () => {
401
  assert.equal(entries[0].playerName, 'Aaron Judge');
402
  assert.ok(calls.some((url) => url.includes('/events?')));
403
  assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  } finally {
405
  global.fetch = originalFetch;
406
  }
 
44
  assert.equal(entries.length, 1);
45
  assert.equal(entries[0].marketKey, buildMarketKey(entries[0]));
46
  assert.equal(entries[0].side, 'over');
47
+ assert.equal(entries[0].source, 'odds_api');
48
+ assert.equal(entries[0].book, 'FanDuel');
49
  });
50
 
51
  test('parses circa OCR lines into normalized entries', () => {
 
396
  oddsApiSportKey: 'baseball_mlb',
397
  oddsApiKey: 'test-key',
398
  oddsApiRegions: 'us',
399
+ oddsApiBookmakers: ['fanduel', 'draftkings', 'betmgm', 'williamhill_us'],
400
  oddsApiMarkets: ['batter_total_bases'],
401
  });
402
 
 
404
  assert.equal(entries[0].playerName, 'Aaron Judge');
405
  assert.ok(calls.some((url) => url.includes('/events?')));
406
  assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
407
+ assert.ok(calls.some((url) => url.includes('bookmakers=fanduel%2Cdraftkings%2Cbetmgm%2Cwilliamhill_us')));
408
+ } finally {
409
+ global.fetch = originalFetch;
410
+ }
411
+ });
412
+
413
+ test('returns partial odds results when later event requests hit 429', async () => {
414
+ const originalFetch = global.fetch;
415
+ const calls = [];
416
+ global.fetch = async (input) => {
417
+ const asString = String(input);
418
+ calls.push(asString);
419
+
420
+ if (asString.includes('/events?')) {
421
+ return {
422
+ ok: true,
423
+ status: 200,
424
+ json: async () => ([
425
+ { id: 'event-1', away_team: 'Yankees', home_team: 'Red Sox' },
426
+ { id: 'event-2', away_team: 'Dodgers', home_team: 'Padres' },
427
+ ]),
428
+ };
429
+ }
430
+
431
+ if (asString.includes('/events/event-1/odds')) {
432
+ return {
433
+ ok: true,
434
+ status: 200,
435
+ json: async () => ({
436
+ id: 'event-1',
437
+ away_team: 'Yankees',
438
+ home_team: 'Red Sox',
439
+ bookmakers: [
440
+ {
441
+ title: 'FanDuel',
442
+ markets: [
443
+ {
444
+ key: 'batter_total_bases',
445
+ outcomes: [
446
+ { name: 'Aaron Judge', description: 'Over', point: 1.5, price: -110 },
447
+ ],
448
+ },
449
+ ],
450
+ },
451
+ ],
452
+ }),
453
+ };
454
+ }
455
+
456
+ if (asString.includes('/events/event-2/odds')) {
457
+ return {
458
+ ok: false,
459
+ status: 429,
460
+ };
461
+ }
462
+
463
+ throw new Error(`Unexpected fetch URL: ${asString}`);
464
+ };
465
+
466
+ try {
467
+ const entries = await fetchOddsApiEntries({
468
+ oddsApiBaseUrl: 'https://api.the-odds-api.com/v4',
469
+ oddsApiSportKey: 'baseball_mlb',
470
+ oddsApiKey: 'test-key',
471
+ oddsApiRegions: 'us',
472
+ oddsApiBookmakers: ['fanduel'],
473
+ oddsApiMarkets: ['batter_total_bases'],
474
+ });
475
+
476
+ assert.equal(entries.length, 1);
477
+ assert.equal(entries[0].playerName, 'Aaron Judge');
478
+ assert.ok(calls.some((url) => url.includes('/events/event-2/odds')));
479
  } finally {
480
  global.fetch = originalFetch;
481
  }