Streams is the raw event firehose for Stacks. One service captures every event a Stacks node emits — STX, FT and NFT transfers, contract prints, locks — and saves them as an immutable, ordered log. You read that log over a cursor-paginated REST API.

Because the data is append-only and never changes, it's heavily cacheable and trivially replayable: sync, stop, and pick up exactly where you left off. The point is what you don't do — you never run a Stacks node. We shoulder data availability so you can build higher-level APIs and indexers on top. It's the same firehose every Foundation Dataset decoder reads internally. For push delivery, see Subscriptions.


How it works

Indexerfaces the nodeRaw eventscanonical · orderedStreams API/v1/streamsYour consumertail · replay

so you never run a node

An indexer faces the Stacks node and writes raw, canonical events; the Streams API serves them to your consumer over a cursor. Stop and resume anytime — the cursor is just height:event_index.


Auth

Streams is read-only but keyed — every request needs an , including during open beta.

curl -H "Authorization: Bearer sk-sl_..." \
  https://api.secondlayer.tools/v1/streams/tip

Reading the log

Every event carries a cursor — height:event_index. Page forward from a cursor and the stream is fully idempotent: persist the last cursor you saw and a restarted job resumes from exactly that point, no duplicates. Because the log is append-only it's also reorg-aware — when a fork resolves, /v1/streams/reorgs tells you which cursors to roll back, so your derived state stays consistent.

The SDK wraps this into a consume loop with checkpointing:

import { createStreamsClient } from "@secondlayer/sdk";

const streams = createStreamsClient({ apiKey: process.env.SL_API_KEY! });

await streams.events.consume({
  fromCursor: lastCheckpoint,
  types: ["print"],
  contractId: "SP2QEZ06AGJ3RKJPBV14SY1V5BBFNAW33D96YPGZF.BNS-V2",
  batchSize: 500,
  onBatch: async (events, envelope) => {
    for (const e of events) await handle(e);
    await saveCheckpoint(envelope.next_cursor);
    return envelope.next_cursor;
  },
});

Try it

Try /v1/streams/events
GET/v1/streams/events?limit=5
API key · stored in your browser only
not run yet
"events":
"event_type": "ft_transfer",
"block_height": 7869999,
"block_hash": "0xdef…",
"tx_id": "0xabc…",
"event_index": 12,
"contract_id": "SP2…BNS-V2",
"sender": "SP3…",
"recipient": "SP1…",
"amount": "1000000"
}
]
,
"next_cursor": "7870001:7"
}
PressSendto run this against live data.
Code · curl, fetch
curl "https://api.secondlayer.tools/v1/streams/events?limit=5" \
  -H "Authorization: Bearer $SL_API_KEY"
const res = await fetch(
  "https://api.secondlayer.tools/v1/streams/events?limit=5",
  { headers: { Authorization: `Bearer ${process.env.SL_API_KEY}` } },
);
const data = await res.json();