Index
Decoded rows you sweep into your own tables. You write the loop; the decoding, the backfill, and the reorg rewind are done.
1M rows free a month hosted, no meter on your own box.
, , and when a reorg lands. A crash resumes from your checkpoint.
7First run backfills from genesis.
11Commits with the rows.
15Delete from the fork height, inclusive.
Where to run it: Deploy.
Read envelopes carry a reorgs array: the reorganizations overlapping the page's height range, empty when none.
To recover from one by hand:
- Roll back rows whose cursor falls inside
orphaned_range. - Re-fetch from
new_canonical_tip. consume()does both for you.
In onReorg, the fork block and everything above it is no longer canonical, so delete rows at fork_point_height and above (inclusive); the consumer then rewinds the cursor and re-reads the canonical run for you.
- Both bounds are
<block_height>:<event_index>cursors tracking decoded-event positions. - On transaction-keyed feeds (
/transactions,/contract-calls), compare by the block-height component.
Running balances: invert them in a sink's onRollback (Sinks).
Every row carries its block_height. Prove that block, its transactions and its Bitcoin anchor on your machine. This is the block of the sBTC transfer on Streams:
| Option | Effect |
|---|---|
eventType | A literal narrows the rows: "ft_transfer" hands onBatch typed amount/sender, "print" hands it payload.topic. No discriminant check to write |
contractId | One id, or an array to follow a whole protocol on one cursor: ["…sbtc-token", "…sbtc-registry"], up to 20. Mutually exclusive with trait |
txContext: true | Joins the submitting transaction into each event (tx_sender, tx_type, …). Same on walk and list; every row carries its submitting transaction, so no per-event /v1/index/transactions call |
finalizedOnly: true | Holds delivery to rows at or below tip.finalized_height; finalized data never reorgs, so onReorg is unnecessary |
signal | Abort checked between batches, never mid-commit: the in-flight batch always commits; see Deploy |
index.contractCalls.consume() | Same loop for decoded calls; already carries sender/function_name, so it needs no txContext |
secondlayer codegen index emits the mirror schema so your tables can't drift:
ctx carries where the sweep has reached, so liveness needs no bookkeeping of its own.
| Field | Is |
|---|---|
cursor | The checkpoint to commit alongside your rows |
height | Highest canonical block a row was DELIVERED from; event recency, parks on a quiet filter |
scannedHeight | Highest block VERIFIED; an empty page proves nothing matches up to the tip; rolled back on a reorg |
tipHeight | Chain tip as of this page's read |
blocksBehind | tipHeight - scannedHeight, floored at 0; real backlog, ~0 for a caught-up tail even when height is old |
Verify your mirror: check your tables against the canonical chain any time.
To prove a block or transaction rather than compare hashes: Verify.
- Self-hosted: reads hit your Postgres, with no meter and no rate limit. Auth is in Self-host.
- Hosted
api.secondlayer.tools: keyed, and billsrows.delivered. 1M free per account per month; rates and free block headers in Keys and billing. - History:
from_height=0reaches whatever your instance holds. Restoring more from the signed archive is metered (credits).
One page with list(), a one-off export with walk(). Neither checkpoints, so neither is a live index.
list, walk, consume
Every feed offers the same three verbs. They are not interchangeable.
| Verb | Gives you | Follows cursors | Checkpoints | Reorgs | Ends |
|---|---|---|---|---|---|
list() | One page plus the envelope | No; you pass cursor back | No | Reports reorgs[]; you act | Immediately |
walk() | An async iterable of rows | Yes, automatically | No | Ignored | On catching the tip |
consume() | A loop calling your onBatch | Yes, automatically | Yes; you commit the cursor | Rewinds and fires onReorg | Never; tails until aborted |
walk() into a database is a trap. It's right for a one-shot export or an analysis script, and wrong as a live index:
- No checkpoint, so a crash restarts from zero.
- Reorgs are ignored, so a fork leaves orphaned rows behind forever.
- It returns at the tip instead of following the chain.
Writing rows you intend to keep is consume().
Plain HTTP
Query rows over HTTP from any client, no SDK needed.
Every endpoint, filter and response shape is in the API reference. GET /v1/index describes itself at runtime, so an agent can learn each event_type without docs.
Two params on the events and contract-call feeds:
| Param | Does |
|---|---|
contract_id | Takes a comma-separated set (max 20): one cursor over a protocol's contracts |
tx_context=true | Puts tx_sender, tx_type, tx_status, tx_contract_id and tx_function_name on every event in one pass. For print events it is the only place the sender lives |
Reads go oldest first. Add order=desc on events and ft/nft transfers to see what just happened: page one is the newest rows, and next_cursor pages older.
Every list response carries the collection, an opaque , the chain tip, and a reorgs array.
tip.block_height is the height committed for the event type you just read, not a global number. Each decoded type runs its own decoder, so two reads can report different tips at once.
/v1/index/blocksand/v1/index/transactionsstay on the ingest tip (source_block_height).- Re-read the tip per response.
Built on Streams: the decoder on your instance reads the same raw firehose you can.
Long-poll instead of tight-loop polling. An empty page holds the connection open until new data commits or wait elapses; a page with rows answers at once.
/v1/index/eventsand/v1/index/blockstakewaitin seconds, max 25.- An instance from before
waitshipped answers400; fall back to a plain poll loop.
From the CLI:
Tool names for agents are on MCP and skills; see the CLI for the rest. Mempool, state writes, PoX-5, sBTC and Runes have no MCP tool; use REST or the SDK. To be pushed instead of pulling: Webhooks.
Print schemas
Clarity ABIs don't describe print payloads, and the shape varies per topic within one contract.
The response shape is in the API reference. Sampled from indexed prints, raw Clarity hex deserialized for exact types, unified per topic. camel_name is what a handler sees on event.data.
- Optional fields report an
optional_some_rate. - Payloads with no
topicstring, or that aren't tuples, land under the"*"pseudo-topic withnon_tupleset. - Cached five minutes (
max-age=300+ weakETag).
Backs secondlayer subgraphs create <name> --from-contract <contract_id>; see Subgraphs.
Find contracts by standard
Find every contract that conforms to a token standard, without keeping a list of addresses. A contract counts as declared when its source has (impl-trait …), and inferred when its ABI matches the standard's shape.
| Param | Description |
|---|---|
trait | sip-009, sip-010, or sip-013. Required. |
conformance | declared, inferred, or any (default). |
limit | 1 to 500, default 100. |
cursor | The previous ; null on the last page. |
include | abi adds each contract's ABI. |
classified_after | ISO timestamp. Only contracts classified at or after it, to sync a list without re-reading it. |
GET /v1/contracts/{contract_id} returns one contract. trait also filters the event and contract-call feeds, and a subgraph source can target a trait and pick up contracts deployed later: Index by standard.
State writes
The exact contract-state writes each canonical block committed, in node order. Use it to see which key a transaction changed, which events alone don't show.
Each row is one write to contract storage.
| Field | Means |
|---|---|
cursor | <block_height>:<ordinal> |
block_height, ordinal | The block, and the write's position in node order |
tx_index | null for block-level writes |
key | The chain's storage key |
value_hex | The new value |
History starts at the block where your node first sent the state_writes observer key. There is no earlier history.
block_heightreads one block; it excludesfrom_heightandto_height.- On your own box this needs a node build that emits state writes. The stock node doesn't yet.
- Rows bill like event rows.
Check whether an output is spent
Watch a Bitcoin output, such as a peg or bond UTXO, and learn when it moves and which transaction moved it.
| Answer | Means |
|---|---|
spent: false | Still unspent, including in the mempool |
mempool: true | Spent by spending_txid, not yet in a block |
confirmed: true | Spent in block_height |
spending_txid: null | Spent more than 144 blocks below the tip; note says so |
- It reads your Bitcoin node live: Bitcoin Core 24 or later with
txindex=1. - One answer is one row. Poll once per block, or more often to catch a mempool spend.
- Fields and errors: Output spend.
Index on contracts everyone shares, with the same verbs and envelope as every other feed. Not products of their own: your contract stays in consume() or a subgraph.
PoX-5
Decoded prints from the pox-5 boot contract (SIP-045, live since Bitcoin block 960,230), plus per-cycle totals. The print log, not a position scoreboard: positions and payouts are a subgraph you deploy.
| Read | Route | SDK |
|---|---|---|
| All 19 print topics | /v1/index/pox5/events | pox5.events.list / walk / consume |
| Reward cycles of the current PoX | /v1/index/pox/cycles · /{reward_cycle} | pox.cycles.list / walk / get |
| PoX-4 stacking actions (historical) | /v1/index/stacking | stacking.list / walk |
Event filters:
| Filter | Narrows to |
|---|---|
topic | One print topic |
staker | One staker |
signer | One signer; alone it returns a pool's full activity, claims and key grants included |
signerManager | One signer manager |
bondIndex | One bond |
rewardCycle | One reward cycle |
fromHeight, toHeight | A block-height range |
confirmed | true holds a read to rows past the reorg margin |
register-for-bond rows add four fields:
bond_indexamount_satsis_l1_lockdata.btc-lockup
pox5.events.consume() is the checkpointed sweep, with the same options as Build your index on it. Cycles are exact to the node and follow the chain again at the next PoX fork. PoX-4 cycle history is final and not served; rebuild it from /v1/index/stacking on a self-hosted instance.
sBTC
The peg lifecycle, decoded, plus Bitcoin confirmation of sweeps.
- Withdrawals: a
withdrawal-acceptonly means the signers committed to a sweep. Your instance checkssweep_txidagainst bitcoind (the bundled full-node profile or an externalBITCOIN_RPC). - Deposits need no check:
completed-depositfires after the signers see the confirmations.
| Read | Route | SDK |
|---|---|---|
| Raw peg log, six topics | /v1/index/sbtc/events | sbtc.events.list / walk / consume |
| Completed deposits | /v1/index/sbtc/deposits · /{bitcoin_txid} | sbtc.deposits.list / walk / consume |
| Withdrawal lifecycles | /v1/index/sbtc/withdrawals · /{request_id} | sbtc.withdrawals.list / walk / get |
| Totals | /v1/index/sbtc/summary | none |
A withdrawal carries a settlement object:
settlement_confirmed has three values:
| Value | Means |
|---|---|
true | Past the confirmation threshold |
false | Shallow |
null | No sweep observed yet (not a denial) |
The list takes it as a filter:
?settlement_confirmed=trueis confirmed peg-outs.?settlement_confirmed=falseis the whole not-done bucket.
Withdrawals have no consume
A withdrawal row mutates as it moves REQUESTED, ACCEPTED, settled, and a checkpoint only moves forward. Mirror the append-only events and derive status from the withdrawal-* topics; read withdrawals.get(id) for the assembled lifecycle.
Settlement is verified against your instance's bitcoind, not a custody guarantee: a signer set still controls the pegged UTXO. A Bitcoin reorg that un-confirms a sweep self-corrects on read. To be pushed: Webhooks.
Runes
A catalog of etched runes, a per-rune activity log, and balances, on a Bitcoin tip instead of a Stacks one.
- A rune's id is
<block>:<transaction index>of its etch (840000:3). - Its name is a unique A-Z string. Spacers (
•) are display only, soDOG•GO•TO•THE•MOON,dog.go.to.the.moonanddoggotothemoonare one rune. - Every
runefield takes aRuneRef: an id or a name in any of those forms.
Hosted serves it now. To index Runes on your own box, see Self-host.
| Read | Route | SDK |
|---|---|---|
| Catalog | /v1/index/runes | runes.list({ search, sort }) |
| One rune | /v1/index/runes/{rune} | runes.get(ref), null on 404 |
| Current per-outpoint balances | /v1/index/runes/balances | runes.balances({ address }) |
| Etch, mint, transfer, burn log | /v1/index/runes/activity | runes.activity.list / walk |
From the terminal:
| Read | Filters |
|---|---|
runes.list | search (name prefix), sort (etch order, or mints for busiest) |
runes.balances | address or outpoint (exactly one, enforced by the types), plus rune |
runes.activity | rune, address, kind, txid; pages by <block_height>:<event_index> |
- Amounts (
premine,supply,burned,mints) are u128 decimal strings. Keep them asbigint, neverNumber(). supplyispremine + mints × terms.amount.- Txids come back without
0x.
A Bitcoin block needs 6 confirmations to count as final: tip.finalized_height is tip.block_height - 6.
tip.lag_seconds is the age of the newest indexed block, so it climbs between Bitcoin blocks and 600 seconds or more is normal. To check freshness, compare tip.block_height with your Bitcoin node.
- The catalog and balances read reorg-corrected snapshots, so their
reorgsis always empty. - Only
activityis an append-only log, so only itsreorgscan name replaced heights, at block level.
The raw per-transaction events are on Streams with chain=bitcoin. To keep your own copy, see Index Runes into your own database.