<!-- source: https://secondlayer.tools/docs/index -->

# 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](/docs/self-host#instance-token).

## Build your index on it

Name the event, commit rows and the checkpoint together, and delete from the fork height when a reorg lands. A crash resumes from your checkpoint.

```ts index.ts figure

const index = new Index();
await index.events.consume({
  eventType: "ft_transfer",
  fromCursor: await db.loadCheckpoint(), // null on first run
  // First run backfills from genesis.
  fromHeight: 0,
  onBatch: async (events, envelope, ctx) => {
    await db.transaction(async (tx) => {
      for (const row of events) await tx.upsert(row);
      // Commits with the rows.
      await tx.saveCheckpoint(ctx.cursor);
    });
    return ctx.cursor;
  },
  // Delete from the fork height, inclusive.
  onReorg: (reorg) => db.deleteFromHeight(reorg.fork_point_height),
});
```

Where to run it: [Deploy](/docs/deploy).

## Reorgs

Read envelopes carry a `reorgs` array: the reorganizations overlapping the page's height range, empty when none.

```json response.json
{
  "reorgs": [
    {
      "id": "...",
      "detected_at": "2026-06-05T12:00:00Z",
      "fork_point_height": 892100,
      "old_index_block_hash": "0x…",
      "new_index_block_hash": "0x…",
      "orphaned_range": { "from": "892101:0", "to": "892140:7" },
      "new_canonical_tip": "892140:3"
    }
  ]
}
```

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](/docs/sinks#rollback-is-height-stamped)).

## Check it

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](/docs/streams#read-it-directly):

```text ~/secondlayer exit=0
$ secondlayer verify block 9210757
✓ block       9,210,757  c61897739dde0b73ac9c99d146c288ad0d5b728f07df7d728680bc57683c5604
✓ bitcoin     headers 967,680..971,479, proof-of-work and retargets checked
✓ burn        height 971,479  000000000000000000006445b9307441f663ae84a9fa66f5a50e4a8dc95b2ff5
✓ cycle       145, signer set proven forward from checkpoint cycle 144
✓ signatures  2,947 / 4,000 weight signed, threshold 2,800
✓ txs         4 transactions match the tx merkle root
✓ state root  a1a4bfd3784a6650af0e2afa707d256c3272c9ddad66314a212e6951c783d538 matches the witness
✓ Block 9,210,757 is proven. Trusted: only the checkpoint (Stacks 9,055,251, Bitcoin 967,680).
```

## Options

| 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](/docs/deploy#handle-sigterm) |
| `index.contractCalls.consume()` | Same loop for decoded calls; already carries `sender`/`function_name`, so it needs no `txContext` |

## Mirror schema

`secondlayer codegen index` emits the mirror schema so your tables can't drift:

```bash
secondlayer codegen index --target kysely   # also: prisma, drizzle, json-schema
```

## Progress and health

`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.

```ts
const { canonical } = await sl.index.canonical({ fromHeight: myLowestHeight });
for (const block of canonical) {
  if (myHashAt(block.block_height) !== block.block_hash) {
    await db.deleteFromHeight(block.block_height); // diverged — re-sweep from here
    break;
  }
}
```

To prove a block or transaction rather than compare hashes: [Verify](/docs/verification).

## Cost and history

- Self-hosted: reads hit your Postgres, with no meter and no rate limit. Auth is in [Self-host](/docs/self-host#instance-token).
- Hosted `api.secondlayer.tools`: keyed, and bills `rows.delivered`. 1M free per account per month; rates and free block headers in [Keys and billing](/docs/authentication).
- History: `from_height=0` reaches whatever your instance holds. Restoring more from the signed archive is metered ([credits](/docs/authentication#archive-fetches)).

## Other ways to read

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.

```ts
const { rows, next_cursor } = await sl.index.blocks.list({ limit: 10 }); // one page
for await (const t of sl.index.ftTransfers.walk({ contractId })) {} // every row, once
await sl.index.events.consume({ eventType: "ft_transfer", onBatch }); // forever, resumable
```

| 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.

```bash
curl "$SECONDLAYER_API_URL/v1/index/events?event_type=stx_transfer&limit=20" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

Every endpoint, filter and response shape is in the [API reference](/docs/api/index#list-events). `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 `next_cursor`, 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/blocks` and `/v1/index/transactions` stay on the ingest tip (`source_block_height`).
- Re-read the tip per response.

Built on [Streams](/docs/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.

```
GET /v1/index/events?event_type=ft_transfer&cursor=<last>&wait=25
```

- `/v1/index/events` and `/v1/index/blocks` take `wait` in seconds, max 25.
- An instance from before `wait` shipped answers `400`; fall back to a plain poll loop.

From the CLI:

```bash
secondlayer index transactions get 0x<tx_id>
secondlayer index mempool --contract-id SP….token
```

Tool names for agents are on [MCP and skills](/docs/mcp); see the [CLI](/docs/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](/docs/webhooks).

### Print schemas

Clarity ABIs don't describe `print` payloads, and the shape varies **per topic** within one contract.

```bash
curl "$SECONDLAYER_API_URL/v1/index/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry/print-schema" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

The response shape is in the [API reference](/docs/api/index#get-print-schema). 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 `topic` string, or that aren't tuples, land under the `"*"` pseudo-topic with `non_tuple` set.
- Cached five minutes (`max-age=300` + weak `ETag`).

Backs `secondlayer subgraphs create <name> --from-contract <contract_id>`; see [Subgraphs](/docs/subgraphs#typed-print-payloads).

### 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.

```bash
curl -G "$SECONDLAYER_API_URL/v1/contracts" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d "trait=sip-010" -d "conformance=any"
```

```json response.json status=200
{
  "contracts": [
    {
      "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.usda-token",
      "deployer": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9",
      "block_height": 21000,
      "declared_traits": ["sip-010"],
      "inferred_standards": ["sip-010"],
      "abi_status": "fetched",
      "classified_at": "2026-10-15T17:15:45.764Z"
    }
  ],
  "next_cursor": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.usda-token"
}
```

| 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 `next_cursor`; `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](/docs/subgraphs#index-by-standard-not-by-address).

### 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.

```bash
curl "$SECONDLAYER_API_URL/v1/index/state-writes?block_height=8054704" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```ts
const { state_writes, next_cursor } = await sl.index.stateWrites.list({
  blockHeight: 8054704,
  contractId: "SP1.vault", // only keys under this contract
  txContext: true, // join the writing transaction
});

for await (const write of sl.index.stateWrites.walk({ fromHeight: 8_000_000 })) {
  console.log(write.cursor, write.key, write.value_hex);
}
```

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_height` reads one block; it excludes `from_height` and `to_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.

```bash
curl "$SECONDLAYER_API_URL/v1/bitcoin/outspend/$TXID/0" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```ts
const out = await sl.index.bitcoin.outspend(txid, 0); // null on 404
if (out?.spent) console.log(out.spending_txid, out.mempool ? "mempool" : out.block_height);
```

| 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](/docs/api/index#get-bitcoin-outspend).

## Decoded protocols

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](/docs/subgraphs).

### 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` |

```ts
for await (const event of sl.index.pox5.events.walk({ topic: "stake", signer: "SP…" })) {
  console.log(event.block_height, event.staker, event.amount_ustx);
}

const { events } = await sl.index.pox5.events.list({
  topic: "register-for-bond",
  fields: ["bond_index", "amount_sats"], // narrow the row to the columns you ask for
});
```

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_index`
- `amount_sats`
- `is_l1_lock`
- `data.btc-lockup`

`pox5.events.consume()` is the checkpointed sweep, with the same options as [Build your index on it](#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-accept` only means the signers committed to a sweep. Your instance checks `sweep_txid` against bitcoind (the bundled full-node profile or an external `BITCOIN_RPC`).
- Deposits need no check: `completed-deposit` fires 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:

```json
{
  "request_id": 42,
  "status": "ACCEPTED",
  "settlement": { "sweep_txid": "abcd…", "btc_confirmations": 7, "settlement_confirmed": true, "btc_block_height": 880321, "confirmed_at": "2026-06-01T00:00:00.000Z" }
}
```

`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=true` is confirmed peg-outs.
- `?settlement_confirmed=false` is 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](/docs/webhooks#triggers).

### 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, so `DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon` and `doggotothemoon` are one rune.
- Every `rune` field takes a `RuneRef`: an id or a name in any of those forms.

Hosted serves it now. To index Runes on your own box, see [Self-host](/docs/self-host#runes).

| 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:

```bash
secondlayer index runes [--search <prefix>] [--sort number|mints]
secondlayer index runes get <rune>
secondlayer index runes balances --address <address>   # or --outpoint <txid:vout>
secondlayer index runes activity [--rune] [--address] [--kind etch,mint,transfer,burn] [--txid]
```

```ts
await sl.index.runes.list({ search: "dog", sort: "mints" });
await sl.index.runes.balances({ address: "bc1phnp234rd5hel0h8ulqvnq3zrww2td0a8q48hjcg7mv67j97jnxfq9ayczm" });

for await (const activity of sl.index.runes.activity.walk({ rune: "840000:3" })) {
  console.log(activity.kind, activity.rune.spaced_name, activity.amount);
}
```

| 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 as `bigint`, never `Number()`.
- `supply` is `premine + 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 `reorgs` is always empty.
- Only `activity` is an append-only log, so only its `reorgs` can name replaced heights, at block level.

The raw per-transaction events are on [Streams](/docs/streams#bitcoin-events-runes) with `chain=bitcoin`. To keep your own copy, see [Index Runes into your own database](/docs/streams#index-runes-into-your-own-database).

## Notes for agents

- Events: 'GET /v1/index/events' (event_type such as ft_transfer, stx_transfer, print) and 'GET /v1/index/contract-calls'; filter by contract, principal or block range; cursor-paginate.
- Mirror index: 'secondlayer codegen index --target kysely' for the schema, then index.events.consume(). Write rows in onBatch, return the committed cursor, handle onReorg by deleting from the fork height (inclusive). Set fromHeight: 0 to backfill from genesis.
- Print payloads: 'GET /v1/index/contracts/<contract_id>/print-schema' gives per-topic fields: Clarity type, camel_name on event.data, and which fields are always present versus optional. Read it before designing tables.
- Contracts by standard: 'GET /v1/contracts?trait=sip-010&conformance=any'; classification is declared_traits versus inferred_standards; page with next_cursor until null. A subgraph source can target the trait instead of an address, e.g. { type: 'ft_transfer', trait: 'sip-010' }, so later deployments are indexed too.
- State writes: sl.index.stateWrites.list({ blockHeight, contractId, txContext: true }) or 'GET /v1/index/state-writes?block_height=<h>'. Rows have key, value_hex, ordinal, and a null tx_index for block-level writes. Cursor is '<block_height>:<ordinal>'. Rows exist only from the height the node first sent the state_writes observer key.
- PoX-5: 'GET /v1/index/pox5/events?topic=register-for-bond' or sl.index.pox5.events.walk({ topic, signer }).
- sBTC: 'GET /v1/index/sbtc/withdrawals/:request_id' has a settlement object. settlement_confirmed null means no sweep observed yet, not a denial. List filter: '?settlement_confirmed=true|false'.
- Bitcoin outputs: sl.index.bitcoin.outspend(txid, vout) or 'GET /v1/bitcoin/outspend/<txid>/<vout>' answers spent, spending_txid, spending_vin, confirmed, mempool, block_height. spending_txid null with a note means spent deeper than 144 blocks below the tip; still treat it as spent. 404 means no such output (or OP_RETURN). Re-read each block; answers change with the mempool and reorgs.
- Runes: sl.index.runes.list({ search }), runes.get(ref) (null on 404), runes.activity.walk({ address }). Amounts are u128 strings; the Bitcoin tip has 6-confirmation finality.

## Setup

Ensure setup once, skipping any step already done:
- Skill: `bunx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `bun add -g @secondlayer/cli`
- Instance: `secondlayer init` writes `INSTANCE_TOKEN`. Loopback `/v1` reads need no key.
- Hosted/archive: export `SECONDLAYER_API_KEY` (`sk-sl_*`) or buy credits with `secondlayer credits buy` (`balance`, `refill`), then `secondlayer whoami`
