Streams
Live subscriptions to heads, blocks, transactions, logs, transfers, AMM events, creations and launches, with filters you can change without reconnecting.
Streams are the push side of the same data the read routes serve. Where a read route answers what is true at one block, a stream tells you as new records land.
Topics
| Topic | Emits |
|---|---|
heads | Canonical head movement |
blocks | New canonical blocks |
transactions | New transactions |
logs | New logs |
transfers | New token transfers |
amm-events | New swaps and liquidity events |
creations | New contract and token creations |
launches | Launch lifecycle transitions: new, graduating, graduated |
candles | Live candle state per named token or pool series: a snapshot on connect, then updates and a final close per bucket |
Topics are isolated, so a slow consumer on one topic does not hold up another.
Every topic has its own single-topic route under /stream/. All of them except heads can also be
multiplexed through /stream/subscribe. The single-topic path is /stream/amm-events, but the
multiplexed select and resume parameters spell the same topic amm_events.
One connection, several topics
GET /stream/subscribe multiplexes several topics over a single connection rather than making you hold one per topic.
Filters on an open subscription can be changed in place:
POST /stream/subscribe/{topic}/{id}/selectorsThat mutates the filter set of a subscription that is already open, so narrowing or widening what you receive does not cost a reconnect and does not create a gap while you reconnect.
What a stream event is, and is not
Streams carry the same speculative-versus-canonical distinction as everything else, and it is the thing to get right when building on them.
A speculative event is an early observation. It has not been confirmed by execution, and the transaction behind it may revert. A confirmed event has been through receipt and root verification.
If you are driving something that must not act on an unconfirmed event, key on confirmed records. If you are driving a live view where being seconds early is worth being occasionally wrong, early observations are the reason streams exist. The overview explains how the two relate.
Recovering after a disconnect
Every durable event carries an id: of the form v1.<namespace>.<sequence>. Record it only after
your application has durably processed the event, then use it to resume:
| Route | Resume with |
|---|---|
| Single-topic streams | after_sequence=<sequence> (the number at the end of the ID), or the full ID in the Last-Event-ID header when after_sequence is absent |
/stream/subscribe | resume=<topic>:<full ID>, one entry per topic. Last-Event-ID alone is not honoured here |
A browser EventSource resends the last full ID as Last-Event-ID on its own, so it resumes without
extra code. An ID is bound to its topic and store, so send it back only to the route that emitted
it. A Last-Event-ID the route cannot use (a bare number, an ID from another topic, or any header on
/stream/subscribe) is not an error: the stream opens live and its first frame is
event: resume_unsupported, saying the header was not honoured and which form to use instead.
Two topics do not replay history. The head stream is not durable, and a reconnect across an API
restart reports a gap. The candle stream has no resume cursor at all: every connection opens with a
fresh snapshot of each selected series, and naming candles in resume is a 400.
Catch-up is bounded. A consumer that reads too slowly, or whose catch-up stopped short of the
present, receives an event: lag frame naming the last sequence delivered, instead of silently
losing events. A resume point older than what the store still retains is handled by how you sent
it:
- An explicit
after_sequenceorresumebelow the retention floor is refused with a400naming the lowest sequence still retained. - A
Last-Event-IDbelow the floor is served live, and the first frame is anevent: lagwithreason: resume_cursor_below_retention_floor, the requested sequence, and theretentionFloor. It is served rather than refused because a browserEventSourcenever retries after a4xx.
In every case, read the gap from the indexed routes, then reopen the stream. Do not skip the gap.
A sequence gap is recorded as unhealthy until the corresponding canonical blocks and receipts have been reconciled.