realtimeodds
v0.5.2
Published
Real-time betting odds SDK — multi-bookmaker, sport-discriminated, Node + Browser.
Maintainers
Readme
realtimeodds
Real-time betting odds SDK — multi-bookmaker, sport-discriminated, Node + Browser.
The SDK is a strict replica of the gateway's internal stores: same shapes, same fields, same getters and read-side methods. Only the mutation surface (with*, cloneWith*, toJSON) is hidden.
Install
npm install realtimeoddsQuickstart
import { createClient } from 'realtimeodds'
const client = createClient({
url: 'wss://api.realtimeodds.xyz',
apiKey: process.env.REALTIMEODDS_API_KEY!
})
client.on('sportEvent:added', ({ sportEvent }) => {
if (sportEvent.sport === 'basketball') {
console.log(`${sportEvent.name} (${sportEvent.bookmaker})`)
}
})
client.on('odds:changed', ({ bookmaker, selectionId, quote }) => {
console.log(`${bookmaker} ${selectionId} → ${quote.price}`)
})
await client.connect()API
| API | Behaviour |
|---|---|
| createClient({ url, apiKey, reconnect? }) | Construct a client. |
| client.connect({ signal? }) | Open the WebSocket. Resolves on first successful connection; rejects on invalid apiKey, incompatible protocol, or exhausted reconnect attempts. Transient errors keep retrying — the promise stays pending. Concurrent calls return the same promise. Optional AbortSignal cancels an in-flight attempt. |
| client.disconnect() | Close and stop reconnecting. Idempotent. Rejects an in-flight connect(). |
| client.snapshot() | Returns { sportEvents: ReadonlyMap<SportEventId, SportEvent>, stale: boolean }. Each SportEvent.markets and Market.selections are also Maps, mirroring the gateway's internal stores. |
| client.getSportEvent(id) | Single lookup by id. Returns null if unknown. |
| client.on(event, cb) / client.off(event, cb) | Subscribe / unsubscribe. |
| client.connectionState | { status, lastError? }. Use the lifecycle events for reactive flows. |
Events
| Event | Payload | Notes |
|---|---|---|
| connected | undefined | Handshake complete. |
| disconnected | { willReconnect, code, reason } | code is the WS close code (4001/4002/4003 → auth fatal). |
| reconnecting | { attempt, delayMs } | Per-attempt delay reflects the actual backoff + jitter. |
| error | { message, fatal } | Fatal stops the client. Non-fatal is informative. |
| sportEvent:added | { sportEvent, receivedAt } | First time a sport event is observed (per source). |
| sportEvent:updated | { sportEvent, receivedAt } | Sport event reference changed (metadata, market list, or any odds update — the internal store is immutable so prices changes produce a new instance). |
| sportEvent:removed | { bookmaker, sportEventId, receivedAt } | No longer reported. |
| odds:changed | { bookmaker, sportEventId, marketId, selectionId, quote, receivedAt } | Per-selection price update. Fires alongside sportEvent:updated for the parent. |
Entities
The SDK exposes the same class instances the gateway holds, with all read-side getters and methods preserved:
SportEvent(BasketballMatch | FootballMatch | TennisMatch):id,kind,bookmaker,sport,competition,sportRegion,startDate(luxonDateTime),matchUrl,name,markets: ReadonlyMap, plusgetMarket(id),getSelection(id).Market(6 variants discriminated bykind):id,kind,selectionKind,isSynthetic,bookmaker,marketName,sportEventName,sport,category,isAvailable,isFullyAvailable,numberOfPossibleResults,selections: ReadonlyMap, plusgetSelection(id),getSelectionByResult(result),getFairOdd(result),calculateMargin(), etc.Selection:id,kind,result,quote?,orderBook?,bookmaker,isAvailable,price(throws if unavailable).Quote:price,size?,timestamp,impliedProbability.OrderBook:bids,asks,timestamp,bestBid,bestAsk,spread,midPrice,availableSizeUpTo(maxPrice).
Sport-specific fields (homeTeam/awayTeam/competitor1/competitor2/period/handicap/scope/cut/playerName/propType) are present on the relevant subtypes and reachable via TS narrowing on kind or sport.
Sport / kind narrowing
client.on('sportEvent:added', ({ sportEvent }) => {
if (sportEvent.sport === 'basketball') {
sportEvent.homeTeam // typed
} else if (sportEvent.sport === 'tennis') {
sportEvent.competitor1 // typed
}
for (const market of sportEvent.markets.values()) {
if (market.kind === 'market:basketball_match.handicap') {
market.handicap // typed
}
}
})Multi-bookmaker behaviour
Every SportEvent carries a bookmaker field (derived from its id). The same underlying match (e.g. Lakers vs Celtics) reported by two sources shows up as two distinct entries with different id and bookmaker. Filter to a single bookmaker:
const ps3838Events = [...client.snapshot().sportEvents.values()]
.filter(ev => ev.bookmaker === 'ps3838')Reconnect tuning
Default policy: exponential backoff 1s → 30s, factor 2, ±30% jitter, unbounded attempts. Override per client:
const client = createClient({
url, apiKey,
reconnect: { initialDelayMs: 500, maxDelayMs: 10_000, maxAttempts: 20 }
})
client.on('error', ({ fatal, message }) => {
if (fatal) console.error('giving up:', message)
})Cleanup pattern
const onAdded = ({ sportEvent }) => { /* ... */ }
client.on('sportEvent:added', onAdded)
// later
client.off('sportEvent:added', onAdded)
await client.disconnect()For React/Vue effects:
useEffect(() => {
const client = createClient({ url, apiKey })
client.connect().catch(console.error)
return () => { void client.disconnect() }
}, [])Time semantics
receivedAt(on every event payload) — local clock when the SDK received the message. Authoritative for SDK-side latency analysis.quote.timestamp/orderBook.timestamp— observation time set by whichever party constructed the object (gateway or SDK at hydration). Approximates freshness; not the bookmaker's authoritative emit time.
Stability
This is 0.3.0. The shapes documented above are intended to remain stable through the 0.x line. Breaking changes will require a 0.x → 0.(x+1) minor bump and will be called out in the changelog. Pre-1.0 means we may still iterate on edge-case behaviour and undocumented internals.
See realtimeodds-spec for the wire-format JSON Schema (used by cross-language ports for protocol-level validation).
License
MIT — see LICENSE.
