Polling & performance
Odds data is time-series data: the difference between a naive client and an efficient one can be 100× in credit use. This guide covers the patterns that keep you fast and cheap.
Choose the correct odds profile
Ordinary /v1/events discovery is future pre-match by default;
without an explicit window or identifier, returned starts remain strictly after
the request boundary and in the future at final validation. A present or future
starts_from remains locally enforced as a future lower bound; only a
genuinely past value opts into historical discovery. Pair it with
starts_to to bound the intended window; the upper timestamp is
inclusive and a date-only value includes that complete UTC day. Both future
bounds are enforced locally after source validation. Exact event and parent
lookups are lifecycle-specific. Use
live=1 separately for the currently verified in-play set. Live
discovery is not response-cached: every request reads the complete bounded
candidate boundary for the selected filters, then performs bounded per-page
verification. A returned event must have live_status=1; its latest
odds must be bound to that event and contain an open line whose cutoff remains
strictly future when expiry is rechecked at final validation. There is no
arbitrary update-age or kickoff-age floor for a legitimately long-running open
event.
Because this verified scan performs work beyond returned rows, its variable
charge is the greater of returned-row credits and bounded discovery/offer-check
work through the same 20-unit blocks. Narrow filters and limit, and
treat X-Credits-Cost as authoritative. Broad discovery reads at
most 1,000 fixture candidates per page, then verifies at most
max(4, 2 × limit) candidates, capped at 512; an exact
event_id lookup verifies only that event. A page can therefore
return fewer offers than limit.
For the lowest-latency current view, call an odds endpoint without
full_history; for event odds, also omit since. Store the
latest snapshot and compare it locally. Event odds select the greatest positive
line_id for each event and period, retaining every primary and
alternate market row at that winning snapshot while excluding older off-board
line IDs. Prop odds select one greatest-line_id row per
special_id + contestant_line_id; handicap is a value on
that row, not a separate latest identity. Use an event-odds since
timestamp only when your application needs available matching timeline updates
after that cursor: it selects historical retrieval and can be materially larger
and slower than the latest-snapshot path. Use since by itself (or with
full_history=1); since=...&full_history=0 is rejected.
Always inspect X-Result-Truncated and never assume a historical
response is complete.
cursor = None
while True:
params = {}
if cursor:
params["since"] = cursor
response = GET_RESPONSE(f"/v1/events/{EVENT}/odds", params)
rows = response.json()
if response.headers.get("X-Result-Truncated") == "true":
raise RuntimeError("history was truncated; do not advance the cursor")
if rows:
process(rows)
cursor = max(r["timestamp"] for r in rows) # advance only after success
sleep(5)
Re-using a fixed since value re-fetches the same window on
every call and bills you for every row, every time. The cursor must move.
Caching & freshness
Reference, discovery, and selected lifecycle data can be served from
SharpRelay's disposable response cache,
which removes the source round trip and minimizes gateway overhead. End-to-end
latency still depends on client location and network conditions. Cached
responses use the same credit metering (they still bill normally). The
X-Cache header tells you which path a response took:
X-Cache | Meaning |
|---|---|
MISS | The disposable cache was available but had no usable entry, so the response was fetched from the source. A MISS does not guarantee the body was stored: lifecycle-empty or oversized responses and best-effort write failures can skip storage. |
HIT | Served from the disposable cache, avoiding the source round trip |
| (absent) | The route is not cache-eligible, caching is disabled, or the disposable cache was unavailable. The response has no cache-path freshness guarantee. |
| Endpoint class | Examples | Max cache age |
|---|---|---|
| Reference data | /v1/catalog/sports, /v1/catalog/leagues, /v1/catalog/periods | ~1 hour |
| Opening snapshot | /v1/events/{event_id}/lines/opening | Up to ~24 hours after a non-empty response |
| Closing and settlement | /v1/events/{event_id}/lines/closing, /v1/events/{event_id}/results | ~30 seconds; empty responses are not cached |
| Verified live discovery | /v1/events?live=1 | Not response-cached; every returned event has live_status=1, event-bound latest odds, and an open line whose cutoff remains strictly future when expiry is rechecked at final validation |
| Ordinary event and prop discovery | /v1/events, /v1/events/{event_id}/props | ~30 seconds |
| Outright discovery | /v1/outrights | ~5 minutes |
| Odds timelines | /v1/events/{event_id}/odds, /v1/props/{prop_id}/odds, /v1/analytics/clv | not response-cached |
Cache age starts when SharpRelay stores a response; it is not a promise about when the underlying data was first published. Empty opening, closing, and result responses are never cached because those lifecycle rows can appear later. Closing lines and results are also mutable: corrections and re-settlements can change a non-empty response, so refresh them whenever revisions matter.
Practical upshot: call reference endpoints at startup and refresh them on a
slow timer — never per event. Ordinary future-card discovery can be cached for
roughly 30 seconds. Verified live=1 discovery always goes to the
source verification path and therefore has no X-Cache header. For
other routes, inspect X-Cache instead of inferring the path from
latency.
Concurrency
- Use a bounded worker pool for independent event requests. Start with eight workers, reuse connections, measure complete-batch latency, and tune gradually while staying under your plan's req/s limit.
- On
429 rate_limited, honorRetry-Afterand add jitter so workers don't retry in lockstep (see Errors & retries).
Complete current-live discovery
A large in-play slate can require several verified event pages. Start with
live=1 and a bounded limit, then pass each opaque
X-Next-Cursor back as the cursor query parameter while
repeating the original sport, league, and page-size filters. Continue after an
empty page: it can mean the scanned candidates were stale or suspended, while a
later page can still contain an open offer. The scan is complete only when the
cursor header is absent and X-Result-Truncated is false.
This mode is intentionally different from ordinary future pre-match discovery: a candidate is returned only after SharpRelay verifies an event-bound latest open line whose cutoff remains strictly future when expiry is rechecked at final validation. There is no separate odds-update-age rule. A past kickoff by itself does not make a row historical while the event is still actively offered in play.
Each live page is freshly source-verified and is not served from the response cache. Repeating a page is therefore a new current observation, not a replay of a previous live response.
A cursor is filter-bound, preserves the first page's kickoff window, and expires after five minutes. Restart from page one for a new live snapshot. If a response is truncated but has no next cursor, narrow the kickoff window or league; never silently treat that response as complete.
Transport efficiency
- gzip — send
Accept-Encoding: gzip; responses are compressed automatically (most HTTP clients do this by default). - Keep-alive — reuse one HTTP client/connection pool; connection setup dominates small requests otherwise.
- History memory —
full_history=1requests available matching history and responses can be large. Process and persist them one at a time, inspectX-Result-Truncated, and avoid retaining several response bodies in memory.