Core surfaces / Index

Index

Decoded Stacks data, kept indexed. We run the chain indexer and the decoder; you get one REST surface over decoded events, contract calls, the canonical block map, blocks, full transaction documents, PoX-4 stacking, and the pending mempool.

Read it keyless, or build your app's own index on it: the same rows are made to be swept, rolled back, and mirrored. No decoders to write, no node to run.

Keys and history

Reads need no key, and a minted free-tier key reads Index at the free-tier rate limit, never slower than anonymous. Free reads cover the recent 24-hour window. Older history needs pay-as-you-go (prepaid credits) or a paid plan.

Index itself is built on Streams: our decoder consumes the same raw firehose you can.

Every list response uses the same cursor envelope: the collection, a next_cursor, the chain tip, and reorgs where applicable. Cursors are opaque <block_height>:<index> resume tokens; pass them back unchanged to continue.

EndpointReturns
/v1/index/eventsDecoded events for a chosen event_type (+ ft-transfers / nft-transfers aliases)
/v1/index/contract-callsDecoded contract-call transactions with args and result
/v1/index/contracts/{contract_id}/print-schemaPer-topic print payload schemas, inferred empirically from indexed history
/v1/index/transactions · /{tx_id}Full transaction documents (fee, nonce, post-conditions, decoded payload)
/v1/index/blocks · /{height_or_hash}Decoded blocks, list or single
/v1/index/canonicalCanonical block-hash map, one row per height, for chain sync
/v1/index/stackingDecoded PoX-4 stacking actions (stack-stx, delegate-stx, …)
/v1/index/mempool · /{tx_id}Pending (unconfirmed) transaction documents
/v1/index/usageYour own consumption and tier limits
curl "https://api.secondlayer.tools/v1/index/events?event_type=stx_transfer&limit=20"

Without a cursor or from_height, list reads default to roughly the last day of blocks. Pass from_height=0 (or any range) to reach full history.

Free and anonymous reads are limited to that recent 24-hour window: a from_height/cursor below it returns 402 UPGRADE_REQUIRED. Top up pay-as-you-go credits (or start a plan) for older history.

Allowed filters vary by event_type: stx_* have no contract_id, nft_* accept asset_identifier, and print filters by contract_id and carries a payload with the Clarity value decoded to JSON. ft-transfers and nft-transfers are typed aliases for the two most common types.

Need the submitting transaction? Pass tx_context=true. Every event then carries its tx joined in — tx_sender, tx_type, tx_status, tx_contract_id, tx_function_name — so building your own index skips a /v1/index/transactions call per event, turning an N+1 transaction walk into one pass.

curl "https://api.secondlayer.tools/v1/index/events?event_type=print&tx_context=true&limit=20"

For print events tx_sender is the only place the real sender lives — the event itself has none.

Clarity ABIs don't describe print payloads, and the shape varies per topic within one contract: sbtc-registry's withdrawal-create has no bitcoin-txid, completed-deposit has no sender. /print-schema derives the real shape empirically: it samples the contract's indexed print events, deserializes the raw Clarity hex for exact types, and unifies a schema per topic.

curl "https://api.secondlayer.tools/v1/index/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry/print-schema"
{
  "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry",
  "topics": [
    {
      "topic": "completed-deposit",
      "count": 1342,
      "fields": [
        {
          "name": "bitcoin-txid",
          "camel_name": "bitcoinTxid",
          "clarity_type": "(buff 32)",
          "ts_type": "string",
          "column_type": "text",
          "always_present": true
        },
        {
          "name": "amount",
          "camel_name": "amount",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        }
      ]
    }
  ],
  "sampled": true,
  "sample": { "size": 2000, "newest_height": 8054704, "oldest_height": 591820 }
}

camel_name is what a handler sees on event.data. Optional fields report an optional_some_rate, and payloads with no topic string (or that aren't tuples) land under the "*" pseudo-topic with non_tuple set.

Anonymous and unmetered like the other reads, cached five minutes (max-age=300 + weak ETag). The endpoint takes no query params (400 otherwise).

This is the schema behind sl subgraphs create <name> --from-contract <contract_id>; see Subgraphs.

curl "https://api.secondlayer.tools/v1/index/contract-calls?function_name=transfer&limit=20"

Each row decodes the call's args and result from the raw Clarity. Contract-call cursors are a separate keyspace from event cursors.

curl "https://api.secondlayer.tools/v1/index/transactions/0x<tx_id>"

Full transaction documents, decoded from the raw transaction: fee, nonce, sponsored, post_conditions, and a typed payload sub-object. List with /v1/index/transactions filtered by type, sender, or contract_id, or fetch one by tx_id.

Proofs

curl "https://api.secondlayer.tools/v1/index/transactions/0x<tx_id>/proof"

/{tx_id}/proof returns a trustless inclusion proof: the raw transaction, raw header, and merkle path needed to verify, client-side, that the transaction is in a Stacks block and that the reward cycle's signers attested to it. Nothing Secondlayer returns is trusted. See Verification.

curl "https://api.secondlayer.tools/v1/index/blocks/892100"

/v1/index/blocks lists decoded blocks (or fetch one by height or hash). /v1/index/canonical returns the canonical block-hash map (exactly one row per height, orphaned blocks excluded) so a client can sync only the canonical chain.

curl "https://api.secondlayer.tools/v1/index/stacking?function_name=stack-stx&limit=20"

Decoded PoX-4 stacking actions, filterable by function_name, stacker, or caller.

curl "https://api.secondlayer.tools/v1/index/mempool?contract_id=SP000000000000000000002Q6VF78.pox-4"

Pending transactions with the same decoded enrichment as /transactions, but pre-chain: no block_height or result, plus a received_at. Filter by sender, type, or contract_id.

Rows leave the mempool on confirmation or drop, so the single form 404s once a transaction mines. Never cached.

GET /v1/index describes itself. Alongside the route list it returns event_type_filters, a per-type map of columns, allowed_filters, equality_filters, and required_non_null generated from the event registry, so an agent can learn what each event_type accepts at runtime rather than hard-coding it.

Read envelopes (e.g. /v1/index/stacking, /transactions, /contract-calls) carry a reorgs array alongside the data: the chain reorganizations overlapping the page's height range, empty when none. Use it to reconcile any rows you've already committed from a fork.

{
  "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 reconcile: roll back any rows whose cursor falls inside orphaned_range, then re-fetch from new_canonical_tip. The ingest loop below shows the reconcile in code.

Both bounds are <block_height>:<event_index> cursors. Reorg cursors track decoded-event positions; on transaction-keyed feeds like /transactions and /contract-calls, compare by the block-height component.

Everything above is also the substrate for your own app index: every list envelope carries a resume cursor, the tip, and overlapping reorgs[]; from_height=0 reaches genesis; pages below finality are served with immutable caching, so re-sweeps are cheap. You bring the database and run the loop; we keep the decoded data flowing.

sl index codegen emits the mirror schema so your tables can't drift from the API:

sl index codegen --target kysely   # also: prisma, drizzle, json-schema

The loop is built in: consume() is a checkpointed consumer with automatic reorg rewind (the same contract as the Streams consumer). Write your rows in onBatch, return the cursor you committed; crash anywhere and the next run resumes from your checkpoint:

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

const index = new Index();
await index.events.consume({
	eventType: "ft_transfer",
	fromCursor: await db.loadCheckpoint(), // null on first run
	fromHeight: 0, // first run: backfill from genesis
	txContext: true, // join the submitting tx into each event (tx_sender, tx_type, …)
	onBatch: async (events, envelope, ctx) => {
		await db.transaction(async (tx) => {
			for (const t of events) await tx.upsertTransfer(t);
			await tx.saveCheckpoint(ctx.cursor); // commits with the rows
		});
		return ctx.cursor;
	},
	onReorg: async (reorg) => {
		// the fork block and everything above it is no longer canonical (delete
		// is inclusive of the fork height); the consumer then rewinds the cursor
		// and re-reads the canonical run for you
		await db.deleteFromHeight(reorg.fork_point_height);
	},
});

txContext: true works the same on walk and list — set it once and every row carries its submitting tx, so you write tx fields straight into your tables with no per-event /v1/index/transactions call. index.contractCalls.consume() does the same for decoded calls (and already carries sender/function_name, so it needs no txContext), and finalizedOnly: true holds delivery to rows at or below tip.finalized_height; finalized data never reorgs, so onReorg becomes unnecessary. The complete runnable version (every Gamma marketplace sale in your own Postgres, ~50 lines) is the sales-index example.

On startup (or any time), verify your mirror followed the canonical chain with /v1/index/canonical (one row per height, orphans excluded):

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;
	}
}

For contract-specific indexes, /print-schema tells you the real shape of a contract's print payloads before you design tables; then walk event_type=print&contract_id=… into them — add tx_context=true to capture the submitting sender in one pass (prints carry none) instead of fetching each transaction. Transaction inclusion proofs let you verify anything you mirrored without trusting us.

This loop is what Subgraphs runs for you: the same decoded rows, your schema, our backfill and reorg handling, one TypeScript file, zero ops.

The Index is the same data through every surface. Pick the one that fits your runtime:

// SDK — typed client, cursor walking built in
const block = await sl.index.blocks.get(892100);
let locked = 0n;
for await (const action of sl.index.stacking.walk({ functionName: "stack-stx" })) {
	locked += BigInt(action.amount_ustx ?? 0); // walk() follows next_cursor for you
}
# CLI
sl index transactions get 0x<tx_id>
sl index mempool --contract-id SP….token

In MCP, the same families are tools: index_events, index_transactions, index_stacking, index_mempool, index_blocks, and the rest.

Read your own consumption and tier limits anytime.

curl "https://api.secondlayer.tools/v1/index/usage" -H "Authorization: Bearer $SL_API_KEY"
# → { product, tier, limits: { rate_limit_per_second }, usage: { decoded_events_today, decoded_events_this_month } }

Also sl.index.usage() (SDK) and index_usage (MCP).