Streams
The raw firehose, for when you want to decode it yourself. Most people want Index; this is what Index is built on.
Hosted reads are keyed (Keys and billing); on your own box see Self-host. Cold history beyond what your instance holds restores from the signed archive (credits).
events.consume is the checkpointed consumer with automatic reorg rewind; the loop is the one on Index.
3Null on first run.
7Commits with the rows.
11Delete from the fork height, inclusive.
A consumer , , and . Each page ends with ; hand it back as and the next page starts right after it.
Where to run it: Deploy. Dated changes land in the changelog.
The loop is this call on repeat. A live page from mainnet, trimmed:
ft_transferPass the response's next_cursor back as cursor. Filters are exact-match, and event types without a given field don't match.
| Filter | Narrows to |
|---|---|
types, not_types | Event types, included or excluded |
contract_id | One contract |
sender, recipient | The two sides of a transfer |
asset_identifier | One asset |
filters | Several named concerns on one cursor (Filters) |
from_height, to_height | A block-height range |
Full grammar: API reference.
The SDK checks every frame's ed25519 signature before onBatch sees it; pin the key and you trust only the key (Per-frame signatures). To check what you stored, compare it with the signed archive. A divergence reads as it does on Archive.
Reorgs rewind to the fork point and re-deliver, inclusive of the fork block. Delivery is at-least-once, so keep writes idempotent.
- Only forks at or below your checkpoint roll back; a fork above it has nothing to undo.
- A rewind deeper than
maxRollbackDepth(default 1000 blocks) is refused before anything is deleted. - On empty pages the loop polls
reorgs.list, so a fork that lands while idle at the tip is caught too. The cost is one extra request per idle poll. - A fork at genesis hands
onReorganullcursor.
events.replay backfills from the signed bulk dumps, then tails live from the manifest's latest_finalized_cursor, no gap or dupe at the seam:
from: "genesis" covers every window the dumps hold; the manifest's coverage reports that range, which starts where the dump program began, not chain block 1. Pass a cursor for a partial backfill.
Plain parquet. Pull a verified range and query it locally:
Manifest signature is the root of trust
The bulk manifest is itself ed25519-signed and verified before any per-file hash is trusted. Listing dumps and events.replay() both enforce it; opt out with verifyDumpsManifest: false.
Every read has a secondlayer streams subcommand that prints JSON.
| Command | Does |
|---|---|
tip | Current canonical tip. |
events | One page of events; takes the REST filters as flags. |
by-tx <txId> | Every event a transaction emitted. |
block-events <heightOrHash> | Every event in one block. |
canonical <height> | Canonical block metadata at a height. |
consume | Long-running pull from a cursor, one event per line, until SIGINT or --max-pages. |
reorgs | Recent reorgs, cursor-paginated. |
dumps | Download finalized parquet dumps to a local dir, sha256-verified. |
chain=bitcoin serves Runes events on their own cursor space.
| Event types |
|---|
rune_etch |
rune_mint |
rune_transfer |
rune_burn |
- A bitcoin cursor only resumes with
chain=bitcoinset again. - Stacks-only filters (
contract_id,sender,recipient,asset_identifier,filters,clock) answer 400 here. runeandaddressare bitcoin-only.- The tip and canonical block drop the burn-chain fields Stacks carries, since Bitcoin has no burn chain of its own.
- Events carry
ts, the block's time.
The rune catalog, per-rune activity feed, and balances live on Index; this is the raw event log underneath them.
Index Runes into your own database
events.consume takes chain: "bitcoin" and runs the same checkpointed loop as Build an indexer on it. Rows and cursor commit together, and a Bitcoin reorg deletes from the fork height.
rune(an id or name) oraddressnarrows the feed.finalizedOnly: truewaits out the 6 confirmations instead of handling reorgs.
One filters object can follow several concerns on one cursor.
Two concerns, one loop
A flat filter is one AND-ed set, so tracking sBTC mints and treasury spends
meant two consume loops, two cursors, two checkpoints, or fetching the union
and throwing half of it away. filters names each concern and the server ORs
them into a single scan:
Each label's declared types narrow its handler, so event.payload is typed
without an event_type guard.
A label added to filters is a compile error until on handles it.
- A label's own events arrive in cursor order.
- Events of different labels don't interleave within a page.
- Add
onBatchfor the whole page when you need strict global order.
One cursor covers all of it. The position is computed over the block's full event set, not the filtered one, so you can add or drop labels between restarts and keep the checkpoint.
Over REST it's one JSON query param, and each event echoes the labels it hit:
SSE takes the same param, so subscribe narrows the same way.
Deliveries are idempotent, so retries never double-count.
- Fully-finalized pages are served
Cache-Control: public, immutablewith anETag(304onIf-None-Match). - The moving tip stays short-lived.
GET /v1/streams/events/stream takes the same filters as /events.
| Param | Does |
|---|---|
cursor (or from_cursor), from_height | Start position. With none it live-tails from the reorg-clamped tip |
ping | Keepalive, every 20s |
Wire contract: REST API.
Near-real-time, not instant
The server polls roughly every 1.5s from a tip held a couple of blocks back for reorg safety, so events land seconds after they appear on chain.
client.events.subscribe(...) auto-reconnects from the last delivered cursor and verifies per-frame signatures; verification modes are under per-frame signatures. Fan out to your backend instead with Webhooks.
Each frame body is { event, sig, key_id }. The signature is ed25519 over JSON.stringify(event).
The SDK verifies every frame by default, against the key at /public/streams/signing-key. Lenient, strict, skip, and pinned-key modes: SDK.