Skip to content

Filtering & performance

The SDK pushes everything the gateway has to your client. For a single sport with one bookmaker that’s modest; for many bookmakers across many leagues, it can be hundreds of odds:changed per second. A few habits keep things responsive.

Reject events you don’t care about at the top of your handler — before any heavy work:

const watchedEvents = new Set<SportEventId>(['vmid:ps3838:1610547234'])
client.on('odds:changed', payload => {
if (!watchedEvents.has(payload.sportEventId)) return
// … now do the work
})

Same idea by bookmaker, by sport, by competition:

client.on('sportEvent:updated', ({ sportEvent }) => {
if (sportEvent.bookmaker !== 'ps3838') return
if (sportEvent.sport !== 'basketball') return
if (sportEvent.competition !== 'comp:basketball.nba') return
render(sportEvent)
})

A Set.has or === comparison costs nothing — it’s the heavy work after that you want to skip.

If you rebuild your view on every event, you’ll do redundant work — sportEvent:updated fires alongside each odds:changed. Coalesce instead:

let pending = false
const dirty = new Set<SportEventId>()
function schedule() {
if (pending) return
pending = true
queueMicrotask(() => {
pending = false
flush(dirty)
dirty.clear()
})
}
client.on('sportEvent:updated', ({ sportEvent }) => {
dirty.add(sportEvent.id)
schedule()
})
function flush(ids: Set<SportEventId>) {
for (const id of ids) {
const ev = client.getSportEvent(id)
if (ev) renderRow(ev)
}
}

queueMicrotask (or requestAnimationFrame for UI) collapses bursts: ten ticks in 1ms produce one redraw, not ten.

Use odds:changed instead of sportEvent:updated for price-only watches

Section titled “Use odds:changed instead of sportEvent:updated for price-only watches”

If all you care about is a specific selection’s price, odds:changed carries exactly that — no need to walk the parent event:

client.on('odds:changed', ({ selectionId, quote }) => {
if (selectionId !== watchedSelectionId) return
setPrice(quote.price)
})

Compare with the alternative — subscribing to sportEvent:updated and walking markets → selections to find the one you care about — which costs a map lookup chain on every tick.

Don’t build a parallel state store — the SDK already maintains one. client.snapshot() is O(1) and reflects everything received so far. Replace local “events I’ve seen” maps with snapshot reads:

const ev = client.getSportEvent(id) // O(1)
const market = ev?.getMarket(marketId) // O(1)
const selection = market?.getSelection(selectionId) // O(1)
const price = selection?.quote?.price

Today, every connected client receives every update from every source the gateway is fetching. Server-side subscriptions (e.g. “only NBA events from ps3838”) are on the roadmap but not implemented in alpha. If your bandwidth or CPU budget is tight, filter aggressively at the SDK boundary using the patterns above.

Every SportEvent and its Markets / Selections carry a few KB of objects. With thousands of events times multiple bookmakers, you’re looking at tens of MB resident — comfortable in Node, fine in modern browsers, potentially noticeable on low-RAM devices. If you only watch a subset, listen for sportEvent:added and release events you don’t care about by simply not retaining references to them. The gateway’s snapshot still holds them, but your local view stays small.

If you do heavy computation on every tick (writing to disk, calling an external API, complex DOM diffing), do it on a coalesced batch — never inline in the listener. The handler should be cheap and synchronous; the work should happen elsewhere.

import { setTimeout as defer } from 'timers/promises'
const queue: OddsChange[] = []
client.on('odds:changed', payload => { queue.push(payload) })
;(async () => {
while (true) {
if (queue.length > 0) {
const batch = queue.splice(0)
await persistBatch(batch)
}
await defer(100)
}
})()

That keeps the on('odds:changed', ...) handler returning in microseconds while a separate loop drains.