1. Use Batch Endpoints
Always use the multi-odds endpoint when fetching odds for multiple events:
Bad:
// ❌ Makes 10 API requests
for ( const eventId of eventIds ) {
const odds = await fetchOdds ( eventId );
}
Good:
// ✅ Makes 1 API request
const eventIds = '123456,123457,123458' ; // Up to 10 events
const odds = await fetchMultiOdds ( eventIds );
2. Implement Caching
Cache responses based on data type and freshness requirements:
const cache = new Map ();
function getCacheKey ( endpoint , params ) {
return ` ${ endpoint } : ${ JSON . stringify ( params ) } ` ;
}
async function cachedFetch ( endpoint , params , ttl = 60000 ) {
const key = getCacheKey ( endpoint , params );
const cached = cache . get ( key );
if ( cached && Date . now () - cached . timestamp < ttl ) {
return cached . data ;
}
const data = await fetch ( endpoint , params );
cache . set ( key , { data , timestamp: Date . now () });
return data ;
}
// Usage
const sports = await cachedFetch ( '/sports' , {}, 3600000 ); // Cache for 1 hour
const events = await cachedFetch ( '/events' , { sport: 'football' }, 300000 ); // 5 minutes
const odds = await cachedFetch ( '/odds' , { eventId: 123 }, 30000 ); // 30 seconds
Recommended Cache TTLs:
Data Type TTL Reason Sports list 1 hour+ Rarely changes Leagues list 1 hour Rarely changes Pre-match events 5-10 minutes Updated periodically Pre-match odds 30-60 seconds Changes frequently Live events 10-30 seconds Changes very frequently Live odds 5-10 seconds Changes in real-time
3. Select Bookmakers Wisely
Don’t fetch odds from all 250+ bookmakers:
// ✅ Select relevant bookmakers for your region
const popularEUBookmakers = [
'Bet365' ,
'Unibet' ,
'William Hill' ,
'Betway' ,
'Bwin'
];
const popularUSBookmakers = [
'FanDuel' ,
'DraftKings' ,
'BetMGM' ,
'Caesars'
];
// Fetch odds only from relevant bookmakers
const odds = await fetchOdds ( eventId , popularEUBookmakers . join ( ',' ));
4. Filter Events by Bookmaker
When you only care about events available at specific bookmakers, use the bookmaker parameter on the events endpoint. This returns only events that have odds from that bookmaker:
// ✅ Only get events with Bet365 odds
const response = await fetch (
`https://api.odds-api.io/v3/events?apiKey= ${ apiKey } &sport=football&bookmaker=Bet365`
);
const events = await response . json ();
// All returned events are guaranteed to have Bet365 odds
This is much more efficient than fetching all events and then filtering client-side. It reduces response size and ensures you only process relevant events.
Rate Limit Management
1. Implement Rate Limiting
Track and respect API rate limits:
class RateLimiter {
constructor ( requestsPerMinute ) {
this . limit = requestsPerMinute ;
this . requests = [];
}
async waitForSlot () {
const now = Date . now ();
const minute = 60 * 1000 ;
// Remove requests older than 1 minute
this . requests = this . requests . filter ( time => now - time < minute );
// Wait if at limit
if ( this . requests . length >= this . limit ) {
const oldestRequest = this . requests [ 0 ];
const waitTime = minute - ( now - oldestRequest );
await new Promise ( resolve => setTimeout ( resolve , waitTime ));
return this . waitForSlot ();
}
this . requests . push ( now );
}
async execute ( fn ) {
await this . waitForSlot ();
return fn ();
}
}
// Usage
const limiter = new RateLimiter ( 30 ); // 30 requests per minute
async function fetchOdds ( eventId ) {
return limiter . execute (() =>
fetch ( `https://api.odds-api.io/v3/odds?apiKey= ${ apiKey } &eventId= ${ eventId } ` )
);
}
2. Handle 429 Responses
Implement exponential backoff for rate limit errors:
async function fetchWithRetry ( url , maxRetries = 3 ) {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
try {
const response = await fetch ( url );
if ( response . status === 429 ) {
const retryAfter = response . headers . get ( 'Retry-After' );
const delay = retryAfter ? parseInt ( retryAfter ) * 1000 : Math . pow ( 2 , i ) * 1000 ;
console . log ( `Rate limited. Retrying in ${ delay } ms...` );
await new Promise ( resolve => setTimeout ( resolve , delay ));
continue ;
}
if ( ! response . ok ) {
throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` );
}
return response . json ();
} catch ( error ) {
if ( i === maxRetries - 1 ) throw error ;
}
}
}
3. Monitor Usage
Track your API usage to avoid hitting limits:
class UsageTracker {
constructor () {
this . requestCount = 0 ;
this . startTime = Date . now ();
}
recordRequest () {
this . requestCount ++ ;
}
getStats () {
const elapsed = ( Date . now () - this . startTime ) / 1000 / 60 ; // minutes
const requestsPerMinute = this . requestCount / elapsed ;
return {
totalRequests: this . requestCount ,
elapsedMinutes: elapsed . toFixed ( 2 ),
requestsPerMinute: requestsPerMinute . toFixed ( 2 )
};
}
reset () {
this . requestCount = 0 ;
this . startTime = Date . now ();
}
}
const tracker = new UsageTracker ();
async function monitoredFetch ( url ) {
tracker . recordRequest ();
const response = await fetch ( url );
// Log usage every 100 requests
if ( tracker . requestCount % 100 === 0 ) {
console . log ( 'Usage stats:' , tracker . getStats ());
}
return response . json ();
}
Error Handling
1. Graceful Degradation
Handle missing data gracefully:
function getBestOdds ( oddsData , market = 'ML' ) {
try {
const bookmakerOdds = Object . entries ( oddsData . bookmakers )
. map (([ bookmaker , markets ]) => {
const targetMarket = markets . find ( m => m . name === market );
if ( ! targetMarket ?. odds ?.[ 0 ]) return null ;
return {
bookmaker ,
... targetMarket . odds [ 0 ]
};
})
. filter ( Boolean );
if ( bookmakerOdds . length === 0 ) {
return null ; // No odds available
}
// Find best odds for each outcome
return {
home: bookmakerOdds . reduce (( best , current ) =>
parseFloat ( current . home || 0 ) > parseFloat ( best . home || 0 ) ? current : best
),
away: bookmakerOdds . reduce (( best , current ) =>
parseFloat ( current . away || 0 ) > parseFloat ( best . away || 0 ) ? current : best
)
};
} catch ( error ) {
console . error ( 'Error finding best odds:' , error );
return null ;
}
}
2. Validate Responses
Always validate API responses:
function validateOddsResponse ( data ) {
if ( ! data || typeof data !== 'object' ) {
throw new Error ( 'Invalid response format' );
}
if ( ! data . id || ! data . home || ! data . away ) {
throw new Error ( 'Missing required fields' );
}
if ( ! data . bookmakers || typeof data . bookmakers !== 'object' ) {
throw new Error ( 'Invalid bookmakers data' );
}
return true ;
}
async function fetchOdds ( eventId ) {
try {
const response = await fetch (
`https://api.odds-api.io/v3/odds?apiKey= ${ apiKey } &eventId= ${ eventId } &bookmakers=Bet365`
);
if ( ! response . ok ) {
throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` );
}
const data = await response . json ();
validateOddsResponse ( data );
return data ;
} catch ( error ) {
console . error ( 'Failed to fetch odds:' , error );
throw error ;
}
}
3. Timeout Handling
Set timeouts for API requests:
async function fetchWithTimeout ( url , timeout = 10000 ) {
const controller = new AbortController ();
const id = setTimeout (() => controller . abort (), timeout );
try {
const response = await fetch ( url , {
signal: controller . signal
});
clearTimeout ( id );
return response ;
} catch ( error ) {
clearTimeout ( id );
if ( error . name === 'AbortError' ) {
throw new Error ( 'Request timeout' );
}
throw error ;
}
}
Security Best Practices
1. Secure API Keys
Never expose API keys in client-side code!
// ❌ Bad - API key exposed in browser
const apiKey = 'sk_live_1234567890' ;
fetch ( `https://api.odds-api.io/v3/odds?apiKey= ${ apiKey } ` );
// ✅ Good - API key stays on server
// Frontend
fetch ( '/api/proxy/odds?eventId=123456' );
// Backend
app . get ( '/api/proxy/odds' , async ( req , res ) => {
const apiKey = process . env . ODDS_API_KEY ;
const response = await fetch (
`https://api.odds-api.io/v3/odds?apiKey= ${ apiKey } &eventId= ${ req . query . eventId } `
);
const data = await response . json ();
res . json ( data );
});
Always validate user input before using it in API calls:
function validateEventId ( eventId ) {
const id = parseInt ( eventId );
if ( isNaN ( id ) || id <= 0 ) {
throw new Error ( 'Invalid event ID' );
}
return id ;
}
function validateBookmakers ( bookmakers ) {
const validBookmakers = bookmakers . split ( ',' )
. map ( b => b . trim ())
. filter ( b => b . length > 0 );
if ( validBookmakers . length === 0 || validBookmakers . length > 30 ) {
throw new Error ( 'Invalid bookmakers list (1-30 required)' );
}
return validBookmakers . join ( ',' );
}
// Usage
app . get ( '/api/odds' , async ( req , res ) => {
try {
const eventId = validateEventId ( req . query . eventId );
const bookmakers = validateBookmakers ( req . query . bookmakers );
const data = await fetchOdds ( eventId , bookmakers );
res . json ( data );
} catch ( error ) {
res . status ( 400 ). json ({ error: error . message });
}
});
Data Management
1. Database Schema
Store odds data efficiently:
-- Events table
CREATE TABLE events (
id INTEGER PRIMARY KEY ,
sport VARCHAR ( 50 ),
league VARCHAR ( 100 ),
home_team VARCHAR ( 100 ),
away_team VARCHAR ( 100 ),
event_date TIMESTAMP ,
status VARCHAR ( 20 ),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Odds table (time-series data)
CREATE TABLE odds (
id SERIAL PRIMARY KEY ,
event_id INTEGER REFERENCES events(id),
bookmaker VARCHAR ( 50 ),
market VARCHAR ( 50 ),
home_odds DECIMAL ( 10 , 2 ),
draw_odds DECIMAL ( 10 , 2 ),
away_odds DECIMAL ( 10 , 2 ),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_event_time (event_id, timestamp )
);
-- Optimize for time-series queries
CREATE INDEX idx_odds_timestamp ON odds( timestamp DESC );
CREATE INDEX idx_odds_event_bookmaker ON odds(event_id, bookmaker, market);
2. Store Historical Data
Keep historical odds for analysis:
async function storeOdds ( eventId , oddsData ) {
const timestamp = new Date ();
for ( const [ bookmaker , markets ] of Object . entries ( oddsData . bookmakers )) {
for ( const market of markets ) {
if ( ! market . odds [ 0 ]) continue ;
await db . query (
`INSERT INTO odds (event_id, bookmaker, market, home_odds, draw_odds, away_odds, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7)` ,
[
eventId ,
bookmaker ,
market . name ,
market . odds [ 0 ]. home ,
market . odds [ 0 ]. draw ,
market . odds [ 0 ]. away ,
timestamp
]
);
}
}
}
3. Cleanup Old Data
Regularly clean up stale data:
// Delete odds older than 90 days
async function cleanupOldOdds () {
await db . query (
`DELETE FROM odds WHERE timestamp < NOW() - INTERVAL '90 days'`
);
}
// Run daily cleanup
setInterval ( cleanupOldOdds , 24 * 60 * 60 * 1000 );
Monitoring & Logging
1. Log Important Events
class APILogger {
log ( level , message , meta = {}) {
const entry = {
timestamp: new Date (). toISOString (),
level ,
message ,
... meta
};
console . log ( JSON . stringify ( entry ));
// Send to logging service (e.g., Winston, Pino, CloudWatch)
}
info ( message , meta ) {
this . log ( 'info' , message , meta );
}
error ( message , error , meta ) {
this . log ( 'error' , message , {
... meta ,
error: error . message ,
stack: error . stack
});
}
metric ( name , value , unit = 'count' ) {
this . log ( 'metric' , name , { value , unit });
}
}
const logger = new APILogger ();
// Usage
async function fetchOdds ( eventId ) {
const startTime = Date . now ();
try {
logger . info ( 'Fetching odds' , { eventId });
const data = await fetch ( `...` );
const duration = Date . now () - startTime ;
logger . metric ( 'odds_fetch_duration' , duration , 'ms' );
return data ;
} catch ( error ) {
logger . error ( 'Failed to fetch odds' , error , { eventId });
throw error ;
}
}
2. Set Up Alerts
Monitor critical metrics:
class AlertSystem {
constructor ( thresholds ) {
this . thresholds = thresholds ;
this . metrics = {};
}
recordMetric ( name , value ) {
if ( ! this . metrics [ name ]) {
this . metrics [ name ] = [];
}
this . metrics [ name ]. push ({
value ,
timestamp: Date . now ()
});
// Keep only last 100 values
if ( this . metrics [ name ]. length > 100 ) {
this . metrics [ name ]. shift ();
}
this . checkThreshold ( name );
}
checkThreshold ( name ) {
const threshold = this . thresholds [ name ];
if ( ! threshold ) return ;
const recent = this . metrics [ name ]. slice ( - 10 );
const average = recent . reduce (( sum , m ) => sum + m . value , 0 ) / recent . length ;
if ( average > threshold ) {
this . sendAlert ( name , average , threshold );
}
}
sendAlert ( metric , value , threshold ) {
console . error ( `ALERT: ${ metric } is ${ value } , threshold: ${ threshold } ` );
// Send to alerting service (PagerDuty, Slack, etc.)
}
}
const alerts = new AlertSystem ({
error_rate: 0.05 , // 5% error rate
response_time: 2000 , // 2 second response time
rate_limit_hits: 5 // 5 rate limit hits in 10 requests
});
Testing
1. Unit Tests
const { describe , it , expect } = require ( '@jest/globals' );
describe ( 'Odds API Client' , () => {
it ( 'should fetch odds for an event' , async () => {
const eventId = 123456 ;
const odds = await fetchOdds ( eventId );
expect ( odds ). toBeDefined ();
expect ( odds . id ). toBe ( eventId );
expect ( odds . bookmakers ). toBeDefined ();
});
it ( 'should handle invalid event ID' , async () => {
await expect ( fetchOdds ( 'invalid' )). rejects . toThrow ();
});
it ( 'should cache responses' , async () => {
const eventId = 123456 ;
const start1 = Date . now ();
await getCachedOdds ( eventId );
const time1 = Date . now () - start1 ;
const start2 = Date . now ();
await getCachedOdds ( eventId );
const time2 = Date . now () - start2 ;
expect ( time2 ). toBeLessThan ( time1 );
});
});
2. Integration Tests
describe ( 'Odds API Integration' , () => {
it ( 'should fetch and compare odds' , async () => {
// Fetch events
const events = await fetchEvents ( 'football' );
expect ( events . length ). toBeGreaterThan ( 0 );
// Fetch odds for first event
const odds = await fetchOdds ( events [ 0 ]. id );
expect ( odds . bookmakers ). toBeDefined ();
// Find best odds
const bestOdds = findBestOdds ( odds );
expect ( bestOdds . home ). toBeDefined ();
expect ( bestOdds . away ). toBeDefined ();
});
});
Summary Checklist
✅ Implement rate limiting logic
✅ Handle 429 responses with backoff
✅ Monitor and track usage
✅ Use multi-endpoints efficiently
✅ Validate all responses
✅ Handle missing data gracefully
✅ Implement request timeouts
✅ Log errors properly
✅ Never expose API keys client-side
✅ Use environment variables
✅ Validate all user input
✅ Implement server-side proxy
✅ Design efficient database schema
✅ Store historical data when needed
✅ Clean up old data regularly
✅ Index time-series queries