The Problem with Polling
Every sports odds API I tried had the same architecture: you poll an endpoint every few seconds and hope you don't miss anything.
For live betting, that's a problem. Odds shift in milliseconds. A +EV opportunity at -110 becomes -115 before your next poll. You're always behind.
I wanted push-based delivery — the server tells you when odds change, not the other way around. So I built SharpAPI around Server-Sent Events (SSE).
Why SSE Over WebSocket?
I evaluated three approaches:
| Approach | Pros | Cons |
|---|---|---|
| REST Polling | Simple, stateless | Delayed, wasteful, misses short-lived lines |
| WebSocket | Full-duplex, fast | Complex reconnection, proxy issues, stateful |
| SSE | Push-based, auto-reconnect, HTTP-native | One-directional (fine for odds) |
SSE won because:
Auto-reconnect is built into the browser spec. If the connection drops,
EventSourcereconnects automatically with theLast-Event-IDheader. With WebSocket, you build this yourself.It's just HTTP. Works through corporate proxies, load balancers, and CDNs without special configuration. WebSocket upgrade requests get blocked more than you'd think.
One-directional is fine. Clients don't need to send odds data — they just need to receive it. SSE is purpose-built for this.
The Architecture
Sportsbook scrapers (Python)
↓
Redis Pub/Sub
↓
Node.js SSE server
↓
Client (EventSource)
Scrapers collect odds from 20+ sportsbooks every few seconds and publish changes to Redis channels.
SSE server subscribes to Redis, maintains client connections, and pushes only the deltas — not the full snapshot every time.
Clients connect once and receive a stream of updates:
const es = new EventSource(
'https://api.sharpapi.io/api/v1/stream/odds?league=NBA',
{ headers: { 'Authorization': 'Bearer sk_live_...' } }
);
es.addEventListener('odds:update', (event) => {
const data = JSON.parse(event.data);
console.log(data.sportsbook, data.selection, data.odds_american);
});
es.addEventListener('ev:alert', (event) => {
const data = JSON.parse(event.data);
console.log(`+EV: ${data.ev_percent}% on ${data.selection}`);
});
The +EV Detection Layer
Raw odds are table stakes. The real value is knowing which odds are mispriced.
I use Pinnacle as the sharp reference line. Pinnacle is a low-margin sportsbook that prices markets efficiently — if DraftKings is offering +150 and Pinnacle's no-vig fair price is +130, that's a +EV bet.
The math:
fair_probability = 1 / pinnacle_decimal_no_vig
implied_probability = 1 / draftkings_decimal
ev_percent = (fair_probability / implied_probability - 1) * 100
Every odds response includes ev_percent, fair_odds, and no_vig_probability so developers don't have to build this themselves.
Lessons Learned
1. Redis Pub/Sub doesn't buffer. If a client disconnects and reconnects, they miss everything published during the gap. I added a snapshot endpoint so clients can hydrate state on reconnect, then switch to the stream for deltas.
2. SSE connections are long-lived. You need proper connection management — heartbeats every 15s, graceful cleanup on disconnect, and connection limits per API key to prevent abuse.
3. Rate limiting streaming is different. You can't just count requests per minute when a client holds one connection for hours. I rate-limit by concurrent streams per tier instead.
4. Free tiers matter. My free tier (12 req/min REST, no credit card) drives 80% of sign-ups. Developers want to build first, pay later. The ones who find value upgrade on their own.
Results
- Sub-89ms P50 latency from odds change to client delivery
- 20+ sportsbooks streaming simultaneously
- Growing organically — ChatGPT is my #1 external traffic source (people ask it for sports odds APIs and it recommends SharpAPI)
Try It
Free tier: 12 requests/minute, 2 sportsbooks, all major sports. No credit card.
→ sharpapi.io
→ Docs
If you're building anything with sports data, I'd love to hear what features you'd want. Drop a comment or find me on Discord.
Top comments (0)