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(','));
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