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

# Changelog

Notable changes to Secondlayer and its SDKs. Entries dated before August 2026 describe the hosted service as it worked then (per-row read metering, API keys, `402` past the free window); it was withdrawn in August 2026 and has since returned, with hosted Index, Streams, subgraphs and webhooks at `api.secondlayer.tools` on an account key and prepaid [credits](/docs/authentication), while on a self-hosted instance reads stay yours and unmetered and archive bootstrap, backfill and reindex draw [credits](/docs/authentication#archive-credits).

## Index — October 2026

### Read the newest events first

Add `order=desc` to `/v1/index/events`, `/v1/index/ft-transfers` or `/v1/index/nft-transfers` and your first page is what just happened, not rows from a day ago. `next_cursor` then pages older, and a cursor resumes only under the order that issued it. The SDK takes `order: "desc"` on those lists and `walk()`, the CLI takes `--order desc`, and the MCP event tools take `order`. Reads still default to oldest first, and `consume()` always tails forward ([plain HTTP](/docs/index#plain-http)).

## CLI — October 2026

### The archive is ready for Clarity state writes

Bootstrap, verify and repair now handle a fourth archive dataset, `state_writes`: every Clarity state change each block made, named per key and checkable against the block's state root. The archive starts publishing it once the hosted stack holds it from genesis; until then nothing changes for you, and archives without it bootstrap and verify as before ([archive datasets](/docs/archive)).

### `secondlayer verify` names the exact blocks that differ

When a range of your local data differs from the signed archive, `secondlayer verify` now lists each diverging block and what differs in it (the block, its transactions or its events), instead of only the 50,000-block range. It reads the archive's per-block digests for free and checks them against the signed manifest. `--json` carries every height under `divergent_heights`, and you get a `secondlayer repair` command for every diverging range, not just the first ([verify against the archive](/docs/archive)).

## Index — October 2026

### Check whether a Bitcoin output is spent

`GET /v1/bitcoin/outspend/{txid}/{vout}` says whether an output is spent, by which transaction and input, and whether that spend is in a block or still in the mempool. It reads the Bitcoin node live, so a mempool spend shows on the next read. A confirmed spend more than 144 blocks below the tip answers `spent: true` without naming the spender ([how to use it](/docs/index#check-whether-an-output-is-spent)).

## SDK — October 2026

### sBTC peg address follows the v2 signer format

From Bitcoin height 972,295 the sBTC signers lock the peg to a `multi_a` script built from the registry signer set and threshold, not the aggregate key. `getSignersAddress` in `@secondlayer/stacks/sbtc` now returns the format active at the node's burn tip, or at the height or `version` you pass. `deriveSbtcV2PegOutput` gives the v2 script, scriptPubKey and address for any signer set. Once DKG is disabled the registry's aggregate key becomes a `0xFF` marker, and `getSignersPublicKey` throws `SbtcV2MarkerError` rather than returning it. Until the first v2 sweep confirms, the funds are still at the v1 address.

## MCP — October 2026

### Connect an agent with a URL

The MCP server now runs at `https://api.secondlayer.tools/mcp`, so an agent needs no install and no key in its config. Run `claude mcp add --transport http secondlayer https://api.secondlayer.tools/mcp`, and the first call opens a browser page where you sign in and allow the app. It carries the read tools (Index, Streams, Contracts, subgraph scaffolds), and its reads use your credits like a key's. Apps you allowed are listed under Apps on your Keys tab, where you can revoke one. Clients without OAuth can send an account key as the bearer. See [Connect](/docs/mcp#connect).

## Accounts — October 2026

### Your dashboard on your phone

On a phone, your account opens as an app with five tabs: Pulse, Insights, Webhooks, Subgraphs and Account. Pulse shows what needs you, the last 24 hours of deliveries and today's spend. You can pause, resume, test and replay a webhook, and revoke a key by holding its button. Add it to your Home Screen to install it. On a computer nothing changes.

### Push when ingestion breaks

The installed app can notify you when a webhook can't reach your server, a hosted subgraph stops ingesting or stops serving, or a replay you started finishes. Turn it on and pick what you get under Account, then Push. Spend and key alerts stay off unless you turn them on. Self-hosted instances send push once `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` and `VAPID_SUBJECT` are set; without them push stays off.

### Snooze an insight until tomorrow

`POST /api/insights/:id/snooze` with `{ "until": "<time>" }` hides an insight for up to 7 days, and `DELETE` on the same path shows it again. `DELETE /api/insights/:id/dismiss` undoes a dismiss.

### Keys get their own tab

Keys is a tab in your dashboard now. Each key shows the rows it read over 30 days, and a key you made that hasn't read anything in 30 days gets a note so you can revoke it. A new key is shown once, with how to use it from the CLI, your environment, the SDK or an agent. An account holds up to 50 active keys, and you can make 20 an hour. Your first key is one you name; sign-in no longer makes one called "Default".

### Insights tell you what's wrong, worst first

We now check your webhooks, subgraphs and reads every few minutes and record what needs attention: a receiver that's down, a certificate about to expire, a handler that keeps throwing, a polling loop, spend on pace to hit your cap. `GET /api/insights` returns them ranked, worst first, each with the evidence and a plain-text prompt you can hand to your own agent. `POST /api/insights/:id/dismiss` hides one, and `POST /api/insights/:id/expected` stops it coming back for that resource. `sl webhooks doctor` uses the same thresholds.

### Webhook-only stacks cost $5 a month

A hosted stack with no subgraphs no longer runs the process that executes subgraph handlers, so it uses less memory and bills less. Webhooks only: $5/mo minimum while your stack runs. With subgraphs: $10/mo minimum. Your first subgraph deploy moves the stack to the $10 minimum, and deleting your last one moves it back. See [What it costs](/docs/authentication#what-it-costs).

### Your hosted stack stops when it has nothing to run

Pause or delete every webhook and subgraph and your hosted stack stops within minutes, and its memory and storage charges stop with it. It never stops in its first 15 minutes, and your next request starts it again with your tables intact. Webhook-only accounts run the same stack. See [Keys and billing](/docs/authentication#what-it-costs).

### The spend cap covers every charge

Your monthly spend cap now stops your hosted stack too, not only reads past the free 1M rows. At the cap, webhooks stop delivering, subgraphs stop indexing and archive fetches answer `402 spend_cap_reached` until the next month or until you raise the cap. See [Spend cap](/docs/authentication#spend-cap).

## Webhooks — October 2026

### Watch a replay finish

A replay now has a record you can follow. `GET /api/webhooks/:id/replays/:replayId` returns how many events it queued, how many are sent and how many are still pending; when it finishes it adds how many were delivered and how many gave up. `GET /api/webhooks/replays` lists recent replays across your webhooks. Only one replay scans per webhook at a time, even across restarts.

### Requeue what gave up in one call

`POST /api/webhooks/:id/dead/requeue` sends every delivery that gave up back to the queue, optionally only those from one replay (`replayId`) or since a time (`since`). After an outage, use this instead of replaying the range: deliveries that were waiting go out on their own once you resume, and a replay would send them twice.

### One email when a webhook is down

If a hosted webhook fails 5 deliveries in a row and nothing gets through for 15 minutes, we email the account owner once. Several webhooks down at the same time arrive in one email. Deliveries keep retrying while it's down, and you get no further email until it recovers and goes down again.

### See the deliveries from one hour

`GET /api/webhooks/:id/deliveries?from=&to=` and `sl.webhooks.deliveries(id, { from, to })` return the attempts in a time window of up to 31 days. Activity now counts failed attempts per hour, includes a usual range for each hour of the day, and takes `?days=30` for daily totals.

### Know when your receiver's certificate expires

We check your https receiver's TLS certificate every few hours and return its expiry as `receiverCertExpiresAt` on the webhook.

### Sentinel webhooks keep their names

Sentinel finds its webhooks by name, so a webhook whose name starts with `sentinel:` can't be renamed. The API answers `409 WEBHOOK_NAME_MANAGED`.

### Match print fields with where

A `print_event` trigger or subgraph source takes `where`, so you get only the prints that carry your values: your signer on PoX-5, your pool on a DEX. See [Print fields](/docs/webhooks#print-fields).

### print_event topic now filters

Before, `topic` was ignored on webhooks and you received every print from the contract. It now delivers only prints whose tuple `topic` matches.

## SDK — October 2026

### Prove a Bitcoin transaction offline

`verifyBitcoinTx` proves a Bitcoin transaction is in a block of the checkpoint's Bitcoin chain, and that one of its outputs pays the address and amount you expect. It needs no contract call and no Stacks node. Pair it with a state proof of an sBTC withdrawal sweep to follow the withdrawal to its Bitcoin output. See [Bitcoin transactions](/docs/verification#bitcoin-transactions).

### Bitcoin transaction proofs route

`GET /v1/proofs/bitcoin-tx/{txid}` returns a transaction, its merkle branch and its block header, read from the instance's bitcoind. It is free like every proofs route. On your own box it reads the node at `BITCOIN_RPC_URL`, which needs `txindex=1`.

## Subgraphs — October 2026

### Health history and grouped handler errors

Your subgraph records a snapshot every minute: blocks behind the chain tip, rows written and storage, kept for 7 days. `GET /api/subgraphs/:name/health` returns it in hourly buckets. Handler errors are grouped by cause, so a bug that throws on every event shows up as one entry with a count. `GET /api/subgraphs/:name/errors` lists the groups, most frequent first.

### Contract reads now work in hosted subgraphs

`readContractAt` and `ctx.client` work in hosted handlers with no setup, pinned to the block being processed like on your own box.

- About 3,000 reads a minute per account and 5 s per read. Over the limit, reads wait and retry.
- Self-hosted processors re-read each cached value once after upgrading: the read cache key changed.
- See [Reading the chain from a handler](/docs/subgraphs#reading-the-chain-from-a-handler).

### Chain reads halt the block instead of dropping rows

Breaking for self-host. Before, a `ctx.client` read that couldn't complete (a missing node URL, a node timeout or outage) failed only that event, and the block committed without its rows. Now the block retries, then the subgraph halts there with status `error` and resumes from that block once the node answers.

- A contract that answers `err` still fails only that event.
- See [Reading the chain from a handler](/docs/subgraphs#reading-the-chain-from-a-handler).

### readContractAt deploys

A subgraph that imports `readContractAt` now deploys. It failed to bundle before with `No matching export`.

### Typed map and variable writes

Pass an `abi` on a map or variable write source and name the map or variable, and your handler gets `event.key` and `event.value` typed and decoded from the contract's types. Without one they stay unknown.

### Deploy checks map and variable names

`secondlayer subgraphs deploy` now fails when a write source names a map or variable its contract does not define, so a typo stops the deploy instead of matching nothing.

## CLI — October 2026

### One login for your account

`sl login` and `sl logout` always sign you in and out of your Secondlayer account, and `--credits` is gone. One account and one credit balance cover rows, your hosted stack and archive fetches, so there's nothing separate to sign in to. A browser login opens a session, like `sl login --email`, rather than saving an API key, so the Keys tab only lists keys you made.

### `sl login` opens your browser

`sl login` shows a short code and opens secondlayer.tools. Approve it there while you're signed in and the CLI is signed in on that machine until `sl logout`. Signing in never makes an API key, so the Keys tab only holds the keys you made for reading data. `sl login --email` keeps the emailed-code sign-in for a machine with no browser, and `--with-token` still reads a key from stdin.

### See your rows by day

`sl credits usage` shows the rows you were delivered each day for the last 30 days, split into free and paid, and where they came from. Pass `--days` for a shorter window or `--json` for scripts. The Rows tab in your account shows the same numbers.

### Self-host with docker compose

`secondlayer setup` and `secondlayer uninstall` are gone, along with the MCP `setup` tool. You run your own instance from `docker/oss`: `secondlayer init` writes the keys, `docker compose pull` and `up -d` start it, and `bootstrap` and `verify` fill and check it. Stop it with `docker compose down`. The compose file now pulls the published images, pinned with `SECONDLAYER_VERSION`. See [Self-host](/docs/self-host).

### Runes from the CLI

`secondlayer index runes` lists etched runes, and its `get`, `balances` and `activity` subcommands read one rune, a holder's balances and the etch, mint, transfer and burn log. `secondlayer streams events --chain bitcoin` (also `by-tx`, `consume` and `reorgs`) reads the raw Runes events. Self-hosted instances with the `bitcoin` profile have data now; hosted answers a "not configured" note until Runes data arrives with the hosted rollout. See [Runes](/docs/index#runes).

## Accounts — October 2026

### Talk to us from the site

Questions about hosting, volume or a deployment now have a home: [/contact](/contact) takes your message, keeps it, and a person replies to the address you give. Pricing's "Talk to us" links there.

## Webhooks — October 2026

### Subgraph webhooks removed

Webhooks fire on chain events only. Upgrading deletes any webhook on a subgraph table, and the API answers `400 SUBGRAPH_WEBHOOKS_REMOVED` for one. Your rows are yours: react to them from your Postgres, or poll the table hosted. See [React to your rows](/docs/subgraphs#react-to-your-rows). Run `secondlayer webhooks list --json` before you upgrade to keep a record.

### Deliveries carry the block's time

Every Stacks chain delivery's `data` now has `block_time`, when the block was produced (ISO 8601), or `null` when it wasn't recorded. You no longer need a second read to timestamp an event. See [Delivery envelope](/docs/webhooks#receive-and-verify).

## CLI — October 2026

### Recompute a subgraph and check its rows: `secondlayer verify subgraph --replay`

A `state` subgraph can now be checked end to end on your machine. `secondlayer verify subgraph <name> --replay` proves every block in the range from the checkpoint, checks each block's transactions against its header, names every write the block made, re-runs the deployed handler bundle in the deterministic realm, and compares the result with the rows the API serves. Rows compare on declared columns and `_block_height`; `_tx_id` is not compared, since which transaction made a write stays unproven. Reads are free. An instance without `state_writes` exits `2`. See [Check rows](/docs/subgraphs#check-rows).

### `verify block` checks transactions and names writes by default

`secondlayer verify block` adds a `txs` link: the block's transactions must hash to its header's tx merkle root. The state diff's writes are now named by default from `/v1/proofs/writes/{height}`, which is free and rate limited like the other proofs; where the instance has no writes for the block yet, the diff is still proven and listed as unnamed with a note. `--rows` now only adds the `vm_events` check, which bills as Index rows.

### Check a block yourself with `secondlayer verify block` (CLI 19.2)

`secondlayer verify block <height>` proves a mainnet Stacks block on your machine instead of asking you to trust us or any node. It checks Bitcoin headers and their proof of work, ties the block to the Bitcoin block that elected it, proves that cycle's signer set and that 70% of signer weight signed, then rebuilds the block's state root from a witness and lists every state change the block made. The only thing it trusts is a checkpoint built into the CLI. A busy block takes a few seconds. It covers state: contract maps and variables, balances and nonces. Print events aren't committed in Stacks block headers, so they aren't covered yet. See the [CLI reference](/docs/cli).

## SDK — October 2026

### Check a block in your browser

The Verify page and the verification docs now run the check live in your browser: type a height and watch each link turn green, computed on your machine against the free proofs API. `verifyBlock` takes `onStep` to report each link as it passes, and `describeLinks` turns a result into the lines and verdict, so you can build your own.

### Check a block without an account

`@secondlayer/verify` and `/v1/proofs` no longer need a key. Check a block from a script, a wallet or a web page with `new SecondlayerProofSource()`; add a key for higher rate limits.

### Check blocks older than the checkpoint (`@secondlayer/verify` 0.2)

`verifyBlock` now proves any height, not just blocks after the checkpoint. A block below it is tied to the checkpoint by a hash chain: parent links for nearby blocks, one proof from the chain's own height index for distant ones, epoch 2.x blocks included. `secondlayer verify block <height>` shows that link. Nothing new to configure.

### Run the same block check in your own code: `@secondlayer/verify` (0.1)

The verifier behind `secondlayer verify block` is now its own package. `verifyBlock(height, { source })` returns `ok`, the proven state diff, and the first broken link when one fails. Read proofs from `/v1/proofs` with `SecondlayerProofSource` and any account key, or take blocks and MARF proofs from your own node with `NodeRpcProofSource`. Proofs are free; `rows: true` also checks the block's indexed rows, which bill like any Index read. It trusts only the checkpoint it ships with, which you can replace. It's 0.x: expect the lower-level exports (MARF, witness and signer helpers) to change before 1.0. See the [package README](https://github.com/ryanwaits/secondlayer/tree/main/packages/verify#readme).

## Index — October 2026

### Every error carries a request id you can report (SDK 14.4)

Every JSON error from `api.secondlayer.tools` now includes a `request_id` (also sent as the `X-Request-Id` header), a `code`, and `feedback.url`. Send your own `X-Request-Id` and it comes back unchanged. When a call fails and the API looks wrong or incomplete, `POST /v1/feedback` with that `request_id` and one line on what you were trying to do. We attach our own record of the failed call, kept 24 hours, and `request_matched` in the reply tells you whether it was found. Hosted subgraph and webhook errors work the same way. In the SDK, `ApiError.requestId` holds the id, and MCP tool errors keep `code`, `request_id` and `feedback`. A self-hosted instance records nothing and points `feedback.url` at a GitHub issue. See the [REST API](/docs/rest-api) page.

### Proofs for any block at `/v1/proofs`

`/v1/proofs` serves what `verify block` checks: state witnesses, the bytes that tie a block to its Bitcoin block, Bitcoin headers, signed blocks and MARF proofs. Any account key works and proofs are never billed. Witness requests are limited to 2 per second per account.

### Every storage write a block committed: `/v1/index/state-writes`

`GET /v1/index/state-writes?block_height=H` returns the exact writes a block made to contract state, keyed the way the chain stores them, and the SDK has `index.stateWrites`. Rows bill like event rows. Hosted rows arrive once our nodes switch to the instance that records them; a self-hosted node records them with the `state_writes` observer key.

### Block headers are free on `/v1/index/blocks`

Hosted reads of `/v1/index/blocks` are never billed and never return a `402`, even past your free 1M rows or with no balance. Event and transaction rows still bill. See [Keys and billing](/docs/authentication#what-it-costs).

### Older releases
## Subgraphs — October 2026

### Subgraph reads return `next_cursor` on every page with rows

`GET /v1/subgraphs/<name>/<table>` used to set `next_cursor` only on a full page. It now comes back on every page that has rows and is `null` only on an empty page, so a poller tails with `cursor = next_cursor ?? cursor`. A loop that pages through a whole table ends on an empty or short page, one request later than before when the last page was full. `secondlayer verify subgraph --replay` already stops on a short page. See [React to your rows](/docs/subgraphs#react-to-your-rows).

### Subgraphs whose rows anyone can recompute (CLI 19.3)

Every deploy now reports a verification level and a `pin`. A subgraph whose sources are all state writes (`var_set`, `map_*`) is level `state`: its rows can be rebuilt from block headers, so a handler that reads `Date`, `Math.random`, `fetch`, timers or locale formatting is refused with `NONDETERMINISTIC_HANDLER`, naming each use by file, line and column. It then runs in a locked-down realm that rejects the same calls and any non-integer number in a row. Event sources are level `events` until event proofs ship; they deploy exactly as before and get the same findings as advice. `pin` hashes the schema, bundled handler, `startBlock`, network and runtime, so a handler-only redeploy changes it. Both show in `secondlayer subgraphs status` and the subgraph detail. Subgraphs deployed earlier show none until redeployed. See [Handlers](/docs/subgraphs#rows-anyone-can-recompute).

### `map_insert` sources are level `events`; deploys store their pin preimage

Storage holds an insert and a set alike, so a `map_insert` source now derives level `events` until event proofs ship; redeploy to update a stored level. A `state` subgraph reads its writes from `state_writes` once the instance holds them from its `startBlock` on, and keeps `vm_events` until then. Each deploy now stores the canonical JSON its `pin` hashes, served with the handler bundle so anyone can recompute the pin.

If you self-host a subgraph on `var_set` or `map_*` sources whose handler uses one of those calls, it keeps running, but its next redeploy is refused. Take the value from `event`, `ctx.block.timestamp` or `ctx.tx` instead.

### Handler redeploys apply without a restart

A redeploy that changes only the handler (`handler_updated`) now takes effect on the next block. Before, the processor kept running the previous handler until it restarted. Separately, `ctx.findOne` and `ctx.findMany` now return committed rows in insertion order. A `findOne` whose filter matches several rows used to get whichever row Postgres found first, so a handler that depended on that could index differently after a reindex; it now always gets the oldest.

### `_tx_id` always names the transaction that created the row

When one block created a key and a later transaction in the same block upserted it again, the stored row could carry the later transaction's `_tx_id`. It now keeps the creating write's `_tx_id` and `_block_height`, as it already did across blocks, and replay computes the same. Your own columns still take the last write. This applies to new writes; existing rows keep their `_tx_id` unless you reindex. See [System columns](/docs/subgraphs#system-columns).

### Hosted subgraphs, private to your account (CLI 19.1)

Deploy a subgraph to `api.secondlayer.tools` with an `sk-sl_*` key and it runs in your account's own stack, with no box to run. `secondlayer subgraphs` takes `SECONDLAYER_API_KEY` the way `webhooks` does. Your handlers run in a gVisor sandbox with 512 MB of memory and no network beyond Secondlayer's API and the public internet. Sources must be event filters or `contract_call` / `contract_deploy`; anything else is refused at deploy with `SOURCE_NOT_HOSTABLE`, naming the source. Indexing reads bill `rows.delivered` against your allowance, plus the stack's memory and storage. A subgraph whose handler keeps stalling or running the processor out of memory is restarted, then marked `error` after three restarts at the same block. Subgraphs are private to your account: there is no public catalog. Subgraph webhooks stay self-host only. A subgraph with no `startBlock` reindexes from genesis, which now works hosted and fetches only your subgraph's matching contract events; it still bills every matching event and transaction row, so set a `startBlock`. See [Deploy a hosted subgraph](/docs/subgraphs/hosted).

## CLI — September 2026

### `secondlayer console` is gone (CLI 19.0.0)

The instance web console and the `secondlayer console` command are removed. Operate an instance with the CLI, REST API, or MCP. Account settings live at `/account` on the web app.

### `repair` fixes transactions and events, not just blocks

`secondlayer repair` used to compare block digests only, so it reported "nothing to repair" whenever a transaction or event went missing underneath a block that still matched the archive. It now also compares your transactions/events row counts against the manifest and repairs those ranges on `--apply`, even when every block is already correct. The stale-height hint no longer suggests `bootstrap`, which refuses a database that already holds a completed import.

## Index — September 2026

### Credits: runway, projected spend, balance alerts and a monthly spend cap

Your credits page now shows what you spend per day, when your balance runs out, and what the month will cost at your current rate, plus your delivery service's memory against its 0.5 GB minimum. We email you at 7 days and 2 days of credit left, and if your delivery service stops. You can also set a monthly spend cap: once this month's spend reaches it, hosted Index and Streams reads past your free 1M rows pause with a `402 spend_cap_reached` until the 1st or until you raise it. Webhooks keep running.

### 1M rows a month free, then prepaid; a memory floor for hosted stacks

Hosted Index and Streams give you 1M free rows a month, down from 10M. Past that: $5 per 1M rows, $2 per 1M past $50 in a month. Your hosted stack (it runs your webhooks) now bills at least 0.5 GB of memory while it runs, about $10/mo, instead of raw sampled RAM. A stopped stack still bills nothing.

### `pox/cycles` now reports PoX-5, exact to the node (SDK major)

`/v1/index/pox/cycles` follows the chain: it now reports reward cycles of the current PoX (PoX-5) instead of the retired PoX-4 rollup, which stopped at the epoch 4.0 fork. Each cycle carries total stacked and reward-eligible STX, bond sats, sBTC custodied, rewards allocated and claimed, cumulative rewards-per-token, and `is_current`/`is_frozen`; the single-cycle route adds a per-signer breakdown. A new `pox_version` field marks the era, and this endpoint will follow the same move at the next PoX fork. PoX-4 cycle history is final and not served here: a cycle below 141 returns 404 with a note to rebuild it from `/v1/index/stacking` on a self-hosted instance. `sl.index.pox.cycles` picks up the new shape: an SDK major, since the old fields (`unique_stackers`, `unique_delegators`, `action_count`, `function_breakdown`) are gone.

### Runes: `sl.index.runes`, and `chain=bitcoin` on Streams

Runes is data on the existing Index plane, not a new product. `sl.index.runes.list()` is the etch catalog, `.get(ref)` a single rune by id or name, `.balances()` current per-outpoint balances, and `.activity.list()` / `.activity.walk()` the etch/mint/transfer/burn event log, with the same envelope shape, cursors, and consume conventions as every other Index feed, on a Bitcoin tip instead of a Stacks one. `sl.streams.events.list({ chain: "bitcoin" })` (and `stream`/`subscribe`/`consume`) reads the same events raw, on their own cursor space, and each event carries `ts`, its block's time. Four webhook triggers (`runeEtch`, `runeMint`, `runeTransfer`, `runeBurn`) fire off the same feed. Self-host with the `bitcoin` compose profile has data today; hosted follows. See [Runes](/docs/runes).

### `signer` now returns a pool's full pox-5 activity

`claim-rewards`, `claim-staker-rewards-for-signer`, `grant-signer-key`, and `revoke-signer-grant` only ever printed `signer-manager`, so a pool had to query `signer=` and `signerManager=` and merge the results. The contract's own assertions make the two the same principal, so the decoder now fills `signer` on those topics too: `signer=` alone returns everything. `signerManager` stays for compatibility. `pox5.events.consume()` also ships: the same checkpointed paging, cursor saving, reorg rollback, and retries as `events`, `contractCalls`, and `sbtc.events`.

### Tip is per event type, not one number for everyone

Each decoded event type runs its own decoder, and they don't all sit at the same height. `tip.block_height` on `/v1/index/events` (and `ft-transfers`/`nft-transfers`) now reports the committed height for the type you just read, instead of always reporting the `ft_transfer` decoder's checkpoint. A slower decoder no longer silently holds back reads of a type it isn't even feeding, and a type that's actually behind `ft_transfer` no longer serves rows past what it has decoded. Re-read `tip` per response if you cache it across event types. `/v1/index/blocks` and `/v1/index/transactions` are unchanged, on the ingest tip.

### Hosted chain webhooks: one less poll on the way to your endpoint

The chain-webhook evaluator no longer makes a separate `GET /public/status` request every tick to check decoder progress; it trusts the tip the Index API already returned. A tick that catches up a backlog of blocks re-runs immediately instead of waiting out the poll interval first. Part of an ongoing push to cut hosted webhook latency; more of that work is still in flight.

### `wait`: long-poll instead of a tight poll loop

`/v1/index/events` and `/v1/index/blocks` accept `wait` (seconds, max 25). With it, an empty page holds the connection open and returns the moment new data commits, instead of answering empty right away — same cursor logic, fewer wasted round trips. Under the hood, block ingest and every decoder checkpoint now NOTIFY Postgres on commit, which the L2 decoders and the hosted chain-webhook evaluator use to wake up early instead of sitting out their full poll interval. A self-hosted instance from before this shipped rejects `wait` with a 400; the SDK and evaluator detect that and fall back to plain polling automatically.

### 10M rows a month free, then prepaid

Hosted Index and Streams bill rows delivered, live or history, at one price. Every account gets 10M rows free each month, then $5 per 1M ($2 per 1M past $50 in a month). Past the allowance with no balance, reads answer `402 insufficient_credits` with a top-up link. Hosted reads need an `sk-sl_` key; a self-hosted instance stays keyless on loopback and unmetered.

### Full history on every account

The one-day free window and the Streams retention limit are gone. Any key reads from genesis; rows past the allowance draw credits.

## Webhooks — September 2026

### Receiver credentials are write-only

A webhook read (get, create, update, pause, resume, rotate-secret) no longer echoes back your bearer token, basic-auth string or header values. You get `auth: { type, headerNames, hasSecret }` instead: enough to see what's set, never the credential itself. Set it the same way as before, with `authConfig` on create or update. `secondlayer webhooks get` prints an `Auth` line ("bearer token (set, hidden), headers: x-team") in place of the old raw config dump.

### A config card that explains what you'd otherwise have to guess

The webhook detail page has a new Configuration card: a plain-English read of what each trigger matches, its own 7-day volume, and which fields aren't filtered. An unfiltered trigger gets a warning right on the page, not just inside the card, with the events-a-day estimate it's costing you. The deliveries table adds an Event column (a short transaction id plus its position in the block) so events from the same block stop looking like duplicates. Two bugs are fixed along the way: reopening a delivery no longer swaps to a different one when a poll reshuffles the table underneath it, and a tall response body no longer squashes the tab row to nothing.

### See what each webhook is doing at a glance

A webhook's page now shows its events per hour for the last 7 days, split into delivered, waiting and gave up, plus your last 100 attempts as one strip, so a failure burst or a growing backlog shows up before you read a single row. When a check fires, its insight carries a graph as evidence: events piling up while your receiver is down, the share of 429s, response times against your timeout, or block-to-delivery lag. Click any delivery to see the payload we sent and what your receiver answered, highlighted and copyable. The status pill reads Failing when your receiver is. The same data is in the SDK: `webhooks.activity(id)` and `webhooks.delivery(id, deliveryId)`.

### Doctor explains what's wrong, not just that something is

`secondlayer webhooks doctor` and the dashboard's detail page now run five deterministic checks against your last 100 delivery attempts: rate-limited, down, rejecting, slow, or running behind on delivery. Each fires on a fixed threshold and reports the evidence (attempt counts, response times, a status breakdown), never a guessed cause. One insight leads; the rest collapse underneath it. The webhooks list shows a one-line note per row from data it already has (a paused webhook, a tripped circuit breaker), and `/account/credits` now estimates days of credit left at this month's spend. [Troubleshooting](/docs/webhooks#troubleshooting) has one section per rule.

### Hosted chain webhooks

Chain webhooks run hosted now, at `api.secondlayer.tools` with an `sk-sl_` key and prepaid credits. Each account gets its own isolated webhook stack, not a shared queue. `secondlayer webhooks create` works against hosted the moment `SECONDLAYER_API_KEY` is set.

Delivered events run $10 per 1M; retries are free, never metered. Hosted caps at 7 retries and a 30s timeout per delivery.

### Watch and fix hosted webhooks from the dashboard

[`/account/webhooks`](/account/webhooks) lists your hosted webhooks and, per webhook, the last 100 delivery attempts, a diagnosis, and failed events waiting on you. Send a test event, pause or resume, rotate the signing secret, resend one failed event or all of them, and delete — from the browser. Create and edit still only happen on the CLI/SDK. Opening the page no longer starts (or bills for) a delivery service on an account that's never used webhooks; the credits page link on a `402` also goes somewhere real now.

### Rollbacks cover everything that may have landed

A reorg now rolls back every delivery your endpoint may have received, including one still in flight or retried, instead of dropping it. Delivery stays at-least-once and unordered.

### Limits you can set

`WEBHOOK_MAX_RETRIES_CEILING`, `WEBHOOK_TIMEOUT_MS_CEILING` and `WEBHOOK_REPLAY_MAX_BLOCKS` cap retries, timeouts and replay size. One replay runs per webhook at a time; a second answers `409`. `secondlayer webhooks doctor` warns when chain webhooks can't fire on your instance.

## Subgraphs — September 2026

### One way to write each row

Handlers write with `insert`, `update`, `upsert`, `delete` and `increment`, and read with `findOne` and `findMany`. Every source has a handler of the same name. `ctx.patch`, `ctx.patchOrInsert`, the handler aggregates, `ctx.formatUnits`, the `"*"` catch-all, `materialize`, table `relations` and the definition `version` field are gone. `--from-contract` now scaffolds a one-line `ctx.insert` handler per topic.

### Contract calls find their own ABI

A `contract_call` source with `functionName` no longer needs a pasted `abi`. `secondlayer subgraphs deploy` fetches the deployed contract's ABI and writes it into the definition. Pass `abi` yourself for wildcard, multi-contract or trait sources, and for a local deploy.

### Tip-first refuses handlers that read rows

Tip-first deploys and backfills already refused `update` and `increment`. They now also refuse `findOne` and `findMany`, because a read-then-write depends on the order blocks arrive in. Deploy those handlers in the default mode.

### Subgraph reads live on `/v1`

Rows, streams and aggregates are served from `/v1/subgraphs/*` only. The duplicate `/api` read routes and the subgraph `visibility` setting are removed.

## SDK — September 2026

### `findMany` returns a cursor page, `queryTable*` is gone

`queryTable`, `queryTableCount` and `queryTableAggregate` (the `/api` offset-read client) are removed. Typed `findMany` and untyped `rows`/`count`/`aggregate` read `/v1` instead: `findMany` returns `{ rows, nextCursor, tip }`, not a bare array — pass `nextCursor` back as `cursor` to resume. `orderBy` sorts one column; a second key throws.

### Unchanged pages revalidate for free

The SDK caches the last 64 pages it read and sends `If-None-Match` on the next request. A `304` on a finalized page costs nothing on the hosted API. Pass `etagCache` to keep the cache across runs.

## CLI — September 2026

### One name per setting

`SL_API_URL`, `SL_API_KEY` and `SL_ARCHIVE_API_KEY` are no longer read anywhere. The CLI, SDK and MCP read only the canonical names, with no fallback and no warning.

| Old | Canonical |
| --- | --- |
| `SL_API_URL` | `SECONDLAYER_API_URL` |
| `SL_API_KEY` / `SL_ARCHIVE_API_KEY` | `SECONDLAYER_API_KEY` |

`secondlayer init` now honors `SECONDLAYER_API_URL` (it used to read only the legacy name), and `secondlayer backup` now backs up `SECONDLAYER_API_KEY`. Re-run `secondlayer init` if your `.env` still has the old names. If your `.env` has the instance token only as `SL_API_KEY=<hex>` (no `INSTANCE_TOKEN=` line), the next `secondlayer init` / `setup` mints you a new one; copy that value into `INSTANCE_TOKEN=` first if you want to keep it.

### Setup asks plain questions instead of a terminal UI

`secondlayer setup` no longer installs a native terminal UI. Run it interactively and it asks for whatever `--network`, `--node-mode`, or `--against` you didn't already pass, with plain prompts that work under node, not just Bun. `--against` now defaults to the official archive, so you only pass it to point at a mirror or skip it with `--skip-bootstrap`.

### Observer stanzas work on a stock node

`secondlayer observer` prints `events_keys = ["*"]` in both modes, the only set a stock stacks-core starts with. `--recovery` is gone. A block your node fails to deliver stays a gap until `secondlayer repair` fills it from the archive.

### Instance web console is gone

The compose `--profile console` container and `secondlayer console` are removed. Operate with CLI, REST, and MCP. Old `/docs/console` redirects to [Self-host](/docs/self-host).

## MCP — September 2026

### Live Streams reads stay on REST

MCP keeps `streams_tip` and `streams_dumps`. List and follow tools (`streams_events` and siblings) are gone; call `GET /v1/streams/events`.

## Webhooks — September 2026

### Renamed from Subscriptions

The product name is Webhooks. Canonical surfaces are `secondlayer webhooks`, `sl.webhooks`, `webhooks_*` MCP tools, and `/api/webhooks`. The `subscriptions` aliases are gone. Tables were renamed in place on [upgrade](/docs/self-host#upgrade). Test-ping bodies send `webhook_id`.

## Subgraphs — September 2026

### Identity maps have no handler

You declare `materialize` on a pinned print source: snake columns `{ from: "tokenX" }`, `{ fromTx: "sender" }`. No `ctx.insert`. `--from-contract` and MCP scaffold emit that for named topics. A source is `materialize` or a handler, not both.

### Pinned prints are required, and empty mappings refuse deploy

A `print_event` with a contract id needs a `prints` map so `event.data` is typed. `contract_call` with `functionName` needs `abi`. Every other source needs a handler or `"*"`. Deploy and dryRun return `422 EMPTY_MAPPING` when dummy observed fields write 0 rows. Status `health.emptyMapping` is true only after processed events and empty tables.

### Preview IN/OUT, and skipped prints are queryable

`secondlayer subgraphs preview FILE --from H` prints per-event IN keys and OUT columns, and exits 1 on empty OUT. Print-validate skips persist to authed `GET /api/subgraphs/:name/violations` (not `/v1`). MCP `secondlayer://samples/{contractId}` is five camelCased prints plus the field list.

### Create from a contract

`subgraphs create` requires `--from-contract`, `--trait`, or `--blank` (native STX, not every FT). MCP `subgraphs_scaffold` prefers observed print-schema. `subgraphs add --from-contract` merges a second contract into the same file. `subgraphs_test` fail-closes the same way as the CLI; `dryRun` is DDL only.

## Index — September 2026

### `/extended` is gone

The optional Hiro-JSON listener on :3999 is removed. `/v1` is the contract. Old `/docs/extended` redirects to the REST guide.

### Hosted Index is keyed

Hosted `/v1` Index at `api.secondlayer.tools` takes an account key (`SECONDLAYER_API_KEY`, `sk-sl_*`). Same prepaid credits as archive bootstrap. Your instance on loopback stays keyless.

### `POST /v1/archive/verify`

Your instance compares local identity digests to a signed manifest URL. Nothing is uploaded but the URL. Signature failure is 200 `unanchored`, never `clean`. Without `from_block`/`to_block` the route caps at 40 ranges.

## Streams — September 2026

### Live list reads stay on REST

MCP no longer wraps `streams_events` and siblings. `GET /v1/streams/events` is the live page. `streams_tip` and dumps stay on MCP.

### Hosted Streams is keyed

Hosted `/v1` Streams uses the same account key and the same prepaid balance. Scoped `streams`/`index` keys are revoked; mint `account`.

## SDK — September 2026

### Two credentials on the client

`apiKey` / `INSTANCE_TOKEN` is your instance. `accountKey` / `SECONDLAYER_API_KEY` is the hosted account key. Archive quote and fetch send the account key, never the instance token. `SL_API_KEY` is a one-release alias of the hosted key. MCP archive tools read the same env.

### Signed archive from the SDK

`sl.archive` loads the signed canonical tree, quotes and fetches gated partitions against `api.secondlayer.tools` with an account key, and downloads sha256-checked bytes. Quote and fetch never talk to your instance `baseUrl`. Verify, repair, and bootstrap stay CLI; `download` does not decode parquet.

### Verify this instance, and name an empty index

`sl.archive.verify` POSTs to your instance. A 200 with `unanchored` is data, not an exception. `sl.instance.status` and `diagnose` read `/public/status`, so a box with no blocks names bootstrap instead of a bare DEGRADED. MCP can run the same loop: `instance_status`, `archive_bootstrap`, `archive_verify`, then codegen. Hosted credits need `SECONDLAYER_API_KEY`, not the instance token.

### Running balances survive a reorg

You keep event rows append-only and fold running balances in `onRollback` on the sink. Same transaction as the rewind: doomed rows are still there, a throw aborts the delete. Remaining facts are `height < fork`.

## Stacks SDK — September 2026

### `/tools` AI SDK entry is gone

`@secondlayer/stacks/tools` and `/tools/btc` no longer ship. Agent reads go through `@secondlayer/mcp`. `ai` and `zod` are not peers of the package.

### Same `getContract` client on Clarinet simnet

Point `createPublicClient` at a Clarinet session with `simnet()` from `@secondlayer/stacks/simnet` and keep using `getContract` the way you do on mainnet. The root SDK still does not load clarinet-sdk; that entry is the only one that talks to the VM.

## CLI — September 2026

### Codegen reads Clarinet from config, not a plugin API

`secondlayer.config.ts` takes `clarinet: true` (or `{ path, include, exclude }`). `@secondlayer/cli/plugins` and `plugins: [clarinet()]` are gone. `codegen contracts` still loads simnet ABIs. The `react()` hook generator is gone too.

### `secondlayer local` is gone

The frozen local stack group is removed. Use `secondlayer setup` for self-host and `secondlayer devnet` for Clarinet.

### `secondlayer archive`

Price a fetch and read the official tip without running bootstrap. `archive latest` and `archive quote` sit with the same verify / repair / bootstrap verbs. Top-level `bootstrap`, `verify`, and `repair` stay.

### Instance commands stay on your box

`setup`, `init`, `subgraphs`, and the other instance verbs refuse `api.secondlayer.tools`. `index` and `streams` send `SECONDLAYER_API_KEY` when pointed at us. `whoami` prints instance and merchant. `init` writes `INSTANCE_TOKEN` only.

### `testing()` is gone; simnet is a client

`testing()` no longer generates Clarinet helpers. Import `@secondlayer/stacks/simnet`, hand it your session, and call `getContract`. `clarinet()` now depends on `@stacks/clarinet-sdk`.

## CLI — August 2026

### Setup that lands in the right database, and a bootstrap that finishes

`secondlayer setup` writes `DATABASE_URL` into `.env` and hands it, with the archive key and instance token, to the `bootstrap` and `verify` it runs, so a fresh box restores into the Postgres it just started instead of whatever your shell pointed at. `bootstrap` resumes a torn import per dataset (blocks, transactions, events each keep their own mark), `--from-block` restores verify only what they loaded, and `repair --apply` rewrites a fixed block together with its transactions and events. `secondlayer start` is gone; `setup` brings the stack up.

### Verify offline with a key that ships in the release

`verify`, `repair`, and `bootstrap` resolve the archive signing key the same way: `--public-key`, then `ARCHIVE_SIGNING_PUBLIC_KEY`, then the key compiled into the CLI. A self-hosted instance never leaves the machine for a key, `http://` key endpoints are never consulted, and `latest.json` pointers must carry a digest and a valid signature. The tradeoff: rotating the built-in key takes a CLI release until the key ceremony lands.

### One confirmation gate, and credits that log in as you

Every destructive or metered prompt defaults to no, `-y` skips it, and without a TTY the command exits 1 naming `-y` instead of letting a pipe answer. `--json` never stands in for `-y`: `bootstrap` and `repair` print the quote as `CONFIRMATION_REQUIRED` and exit 2 so a script can read the price first. Archive credits send your `secondlayer login --credits` session, never the instance token, so a logged-in operator can quote and fetch. Dumps stay under `--to` and land via `.part`, the Postgres password rides in `PGPASSWORD`, scaffolded `.env` files are owner-only, and archive fetches retry transient failures with a re-run hint instead of a refusal.

## SDK — August 2026

### Consumers never skip rows on a reorg

`consume()` no longer rewinds forward: a fork above your checkpoint is noted, not rolled back, so the rows between the checkpoint and the fork are delivered instead of silently dropped. Fork points are validated before your sink runs, rewinds deeper than `maxRollbackDepth` (1000 blocks) throw, and the Streams loop polls `reorgs.list` on empty pages so a fork that lands while you idle at the tip is rolled back too. `Cursor.atHeight(0)` is now `null`, and `onReorg` can hand you a `null` cursor at genesis.

### Subgraph `subscribe()` works on a token-gated instance

Typed `subscribe()` is a fetch-based SSE reader that carries your bearer token, reads `/v1` keyless on loopback, and reconnects from the last delivered block height. Every path segment you supply is percent-encoded. `typed()` knows your declared columns, so a table with its own `id` column filters on your `id`, not the system row id. `new SecondLayer({ verify, verifyDumpsManifest, origin })` reaches `sl.streams`.

### `walk()` finishes, Streams errors keep their envelope, `VersionConflictError` is gone

`walk()` ends only when the server stops advancing `next_cursor`, retries page fetches like `consume()`, and rejects `batchSize` above 1000 up front. Index and REST requests run under `requestTimeoutMs` (30 s) and accept `signal`. Every failure status keeps the server's `{ error, code }`, Streams and Index alike; the signing key is cached only after a successful fetch, and a `key_id` rotation mid-stream refreshes it once. `streams.events.subscribe` redelivers an event whose handler threw, backs off with jitter, reopens a stale socket, and stops (through `onError` and a new `done` promise) on a 401 or bad signature. Breaking: `VersionConflictError` is removed (nothing threw it; the real 409 is `ApiError` with `code: "OPERATION_IN_PROGRESS"`), and `context()` fields are `{ value, error? }` so a `null` says why.

## Stacks SDK — August 2026

### `/tools` is deprecated; use `@secondlayer/mcp`

`@secondlayer/stacks/tools` and `/tools/btc` still import until the next major but get no new tools. `ai` and `zod` are optional peer dependencies now, so a project that only reads contracts installs neither. While they last, `readContract` and `getTransaction` return Clarity values through `cvToJSON` instead of crashing on a bigint, and every model-supplied principal, txid, and hash is validated and percent-encoded before a request is built.

### Transport timeouts cover the body, nonces come back on a failed send

One `timeout` covers headers and body, a stalled response rejects with `TimeoutError`, `RequestOptions.signal` cancels from your side, and broadcasts go out once. `NonceStore.release` hands a nonce back when a send fails before the node accepts it, so `FeeTooLow` no longer strands every later transaction. `multicall` caps reads in flight. Breaking: `indexSource`/`indexTxSource` go through the client transport, default `baseUrl` to the transport URL (no hosted default), and send `Authorization: Bearer`.

### Tuples hash like the chain, principals and units fail loudly

Tuple fields serialize in the node's byte order so SIP-018 hashes match `to-consensus-buff?`. The deserializer caps nesting and checks declared counts against the bytes left. `Cl.principal` and `parseContractId` reject undecodable addresses, `parseUnits` refuses exponent-form numbers and silent rounding, and a legacy Bitcoin address with a bad base58check checksum is refused before it becomes a PoX reward address. `apiKey` no longer appears on `transport.config`.

## Index — August 2026

### Optional `/extended` JSON on :3999

You can keep `/extended` clients pointed at your instance. Same Postgres, off by default (`EXTENDED_VIEW=1`), second bind on :3999. `/v1` is unchanged. `status`, `block`, `tx`, per-tx events, address txs, nft transfers, and decoded STX/FT/NFT holdings are live. BNS is empty until `BNS_DECODER_ENABLED=true`. No nonce; that stays on the node.

## Stacks SDK — August 2026

### PoX-5 lock, reclaim, and eligibility

You can fund an L1 lockup, prove it, spend it, and check a stake before you pay the fee. `buildRegisterMetadata` produces the P2WSH address and lock script. `buildPox5LockProof` maps a SIP-044 proof onto `registerForBond`. `reclaim()` spends the same script (locktime or early-exit). `eligibleStake` / `eligibleRegisterForBond` / `eligibleClaimRewards` return `{ ok: true }` or a set of `Pox5ErrorCode` reasons.

Signer calldata is a SIP-005 `pox-addr` tuple (`buildSignerCalldata`). `pauseRewards` is one-way. Import reclaim from `@secondlayer/stacks/pox5`; it is not on the Stacks wallet client.

### Staking post-conditions, SIP-018, and Clarity 4 deploys

You can write the Epoch 3.4 / 4.0 post-conditions the chain actually checks: `Pc.ustxToLock()`, `willPerformPox()`, NFT `maybe-sent`, and `postConditionMode: "originator"`. Hex PCs round-trip with `Pc.fromHex`. Contract deploys default to versioned Clarity 4, not an unversioned Clarity 1 payload.

SIP-018 structured-data hashing matches the spec (`hashStructuredData` / `signStructuredData`). `signMessage({ domain })` previously omitted the inner hashes, so those signatures would not verify on-chain. The pox-5 grant helper was already correct.

pox-5 adds `Pox5ErrorCode` (parse `(err u7)` to a name) and `getPoxInfo()`, which reads `sbtcContract` from `/v2/pox` so you do not hardcode the token principal.

`getDataVar({ contract, variableName })` reads a private data-var off the node (`GET /v2/data_var`) without a full `callReadOnly`.

## CLI — August 2026

### One command replaces the five-command setup

`secondlayer setup` replaces the old five-command, one-manual-copy-paste onboarding path. Secrets get generated, `docker-compose.yml` and `.env` get written straight into your target directory, the stack comes up, and verified history gets restored and checked, all from one command.

Interactive by default: a real terminal UI, built on `@opentui/react`. Without a TTY, or with `--yes`, it skips the prompts and runs the same steps from flags instead, `--network`, `--node-mode`, and `--against` (unless `--skip-bootstrap`) are required explicitly in that mode, so an agent drives it exactly as well as a human at a terminal.

`scripts/oss-bootstrap.ts` is gone. It minted an older account and API-key credential that no longer matches how self-hosted instances authenticate.

## Subgraphs — August 2026

### Experimental observer-http block source

A subgraph processor can page raw `/new_block` bodies from the indexer's internal observer journal instead of Postgres or Streams+Index. Set `SUBGRAPH_SOURCE=observer-http` and `OBSERVER_HTTP_URL` at the processor. Default is still Postgres. The route is opt-in on the indexer (`OBSERVER_HTTP_EXPORT=1`) and is not a public `/v1` API.

### Publish, unpublish, and the visibility flag are gone

Publishing claimed a name in a hosted global namespace. A self-hosted instance has no such namespace, so the verb had nothing left to mean. There is no shim, the routes 404 and the calls are gone.

- CLI: `subgraphs publish`, `subgraphs unpublish`, and `subgraphs deploy --visibility` are unregistered. Deploy's success footer always prints the `/api/subgraphs` read path.
- SDK: `subgraphs.publish()` / `subgraphs.unpublish()` and the `SubgraphPublishResult` / `SubgraphUnpublishResult` types are removed. `deploy()` no longer accepts or returns `visibility`.
- MCP: `subgraphs_publish` and `subgraphs_unpublish` are no longer registered, and `subgraphs_list` no longer reports `visibility` or a `publicUrl`.
- HTTP: `POST /api/subgraphs/:name/publish` and `.../unpublish` are deleted and 404 in every mode. The `PUBLIC_NAME_TAKEN` error code retires with them.

Reads are unchanged: rows still come from `/api/subgraphs/<name>/<table>` on your instance, and the open `/v1/subgraphs` directory still serves what it served. Generated subgraph specs now default their server URL to `http://127.0.0.1:3800` instead of the hosted API, so a spec produced on a self-hosted instance describes that instance.

## CLI — August 2026

### Deprecated CLI aliases are gone

No shim, old spellings error instead of quietly working. One door per capability now:

```diff
- secondlayer subgraphs cancel
+ secondlayer subgraphs stop
- secondlayer streams pull
+ secondlayer streams dumps
- secondlayer subgraphs codegen
+ secondlayer codegen subgraph
- secondlayer subgraphs client
+ secondlayer codegen client
- secondlayer index codegen
+ secondlayer codegen index
- secondlayer contracts generate
+ secondlayer codegen contracts
```

`subgraphs codegen --payloads` is now `codegen prints`. `contracts gen` was the same alias, and the `contracts` group held nothing else, so it's gone too.

`codegen subgraph` now defaults `--target` to `kysely`, matching every other codegen surface. The retired alias defaulted to `prisma`, pass `--target prisma` if you relied on that.

## SDK — August 2026

### Deprecated SDK method names are gone

Each call has one name now: `subscriptions.recentDeliveries()` is `subscriptions.deliveries()`, `subscriptions.requeueDead()` is `subscriptions.requeue()`, `subgraphs.get()` is `subgraphs.status()`. `subscriptions.get()`, `contracts.get()`, and the standalone `getSubgraph()` export are untouched.

### INSTANCE_TOKEN is read as your primary credential

Precedence is now an explicit `apiKey` option, then `INSTANCE_TOKEN`, then `SL_API_KEY`, matching what the docs have always said. Before this, exporting `INSTANCE_TOKEN` authenticated as nobody with no error, the quietest way a documented happy path can fail.

`SL_API_KEY` still works, nothing breaks, but following the docs now works too. Setting both to different values warns once on stderr and `INSTANCE_TOKEN` wins; `secondlayer init` writes identical values, so it never warns.

September 2026 inverted that alias: `SL_API_KEY` is a one-release hosted alias of `SECONDLAYER_API_KEY`, not of `INSTANCE_TOKEN`. `init` no longer writes the hex token there.

## Agents — August 2026

### MCP

Deprecated tool aliases are gone and `subgraphs_get` is renamed, every tool name is now `<product>_<verb>` with one spelling: `scaffold_from_contract` → `subgraphs_scaffold`, `get_contract_abi` → `contracts_get_abi` (last cycle's alias is retired for real now), `subgraphs_get` → `subgraphs_status` (matching `sl.subgraphs.status()` and `secondlayer subgraphs status`). Update any pinned tool name, the server no longer registers the old one.

`INSTANCE_TOKEN` is also read as your primary credential here, see the SDK entry above, the precedence change applies wherever the MCP server resolves a token.

Account tools (`account_whoami`, `account_create_key`) register only when the server is pointed at `https://api.secondlayer.tools`. A self-hosted instance does not show them. Docs and client examples lead with `INSTANCE_TOKEN`. September 2026: `SL_API_KEY` is a hosted alias of `SECONDLAYER_API_KEY`, not of the instance token.

`POST /api/keys` from an API key always mints an `account` key. Requesting `streams` or `index` on that path is a 400. Dashboard sessions can still mint scoped keys. Existing scoped rows still authenticate.

## Subgraphs — August 2026

### Hosted subgraphs and webhooks are retired

`/api/subgraphs` and `/api/webhooks` now 404 wherever the archive used to run them. Deploying handler code and delivering webhooks was never something a shared box should have been doing on your behalf. Self-host and nothing changes: it's still your subgraph, your subscription, your infrastructure end to end.

Since reversed: hosted subgraphs returned in October 2026, private to your account. See [Deploy a hosted subgraph](/docs/subgraphs/hosted).

## SDK — August 2026

### Pay-per-call is gone

The x402 rail is removed. Reads on your instance were never metered by us, and the rail shipped a hardcoded price list you couldn't override, the opposite of "you are the merchant." Removed from the SDK: `withX402`, `createX402Client`, `payAndRetry`, `buildSignedX402Payment`, `readX402Challenge`, `readX402Receipt`, `selectOffer`, `resolveAccountNonce`, `X402SpendGuardError`, and the `@secondlayer/sdk/x402` subpath. Also gone: `@secondlayer/stacks/x402`, `@secondlayer/shared/x402`, the `/v1/x402/*` routes, and wallet-owned deploys with their 7-day expiry. Archive credits are unchanged. Hosted Index/Streams reads at `api.secondlayer.tools` later draw the same prepaid balance.

## CLI — August 2026

### Archive fetches now quote before they charge

Point `secondlayer bootstrap` or `secondlayer repair` at the official archive and you see the price before anything is charged: partitions, dollars, and your balance, in the same confirm prompt you already use. Fetches pull signed partitions over short-lived presigned URLs and every partition is digest-checked before a row is written. Your first six repair bundles each month are free. Your own mirrors and local archives never touch the gate, and `secondlayer verify` stays free with no account, always. The tradeoff is plain: restoring from our archive costs credits; everything you run yourself costs nothing.

### Uninstall without losing your index

`secondlayer uninstall` stops the stack and leaves your data where it is. Containers, networks, and the handler cache come down; the index, chainstate, secrets, and backups stay. It prints the plan and changes nothing until you pass `--apply`.

```bash
secondlayer uninstall            # dry run: what would go, what would stay
secondlayer uninstall --apply
```

Wiping the volumes is a separate, guarded decision: `--purge` refuses to run unless `--backup <dir>` points at a bundle proving your keys exist somewhere else.

### Backup and restore exit when they finish

Both commands close the database pool, so they return to your shell instead of hanging after the work is done. Scripts and CI jobs that waited on them no longer need a timeout.

### Back up and restore your instance

`secondlayer backup` writes an encrypted bundle; `secondlayer restore` reads it back. Both carry a secrets-key canary, so restoring with the wrong key fails immediately instead of leaving you with subscription secrets nobody can decrypt.

### Forward-only installs stop looking broken

An instance now records where its history starts. If you never bootstrapped genesis, the missing prefix reads as deliberate rather than as a gap in every check. Set `SECONDLAYER_SYNC_START_HEIGHT` to declare it.

### One starter, generated from your contract

`secondlayer subgraphs create` no longer ships five opinionated templates behind `--template`. It emits a single starter, and the path worth using is `--from-contract <id>`, which infers sources, schema, and handlers from the contract's observed print events.

## SDK — August 2026

### Default client hits your local API

`new SecondLayer()` and `createStreamsClient()` now talk to `http://127.0.0.1:3800`. Override with `baseUrl` or `SL_API_URL`. Archive dumps stay on the public signed bucket.

## SDK — August 2026

### One account model: yours

Plans, tiers, projects, and API-key tiers are gone from every surface. Deploys are open on any instance (no trial, no quota, no visibility flag), and archive bootstrap/backfill draws prepaid [archive credits](/docs/authentication#archive-credits). Hosted Index/Streams reads at `api.secondlayer.tools` later use that same balance. Removed with them: `client.apiKeys`, `client.projects`, `index.usage()`, `subgraphs.publish()`, and BYO `databaseUrl` deploys (typed ORM codegen for drizzle/prisma/kysely stays). Your schema, your Postgres, your instance: the SDK now assumes nothing else.

## CLI — August 2026

### Buy archive credits from the CLI

`secondlayer credits buy --email you@example.com --pack 25` opens a one-time card checkout. `secondlayer credits balance` shows the prepaid amount. `secondlayer credits refill --below 5 --pack 25` turns on auto-refill; default is off.

### One container next to Postgres

Self-host is `postgres` + `secondlayer`. `secondlayer start --print` writes the compose line. `NETWORK`, `DATABASE_URL`, `NODE_MODE`, `DATA_DIR`, `API_PORT`, and `INDEXER_PORT` are the required non-secrets. Optional `--profile stacks-node` or `full-node` bundles chain daemons.

```bash
secondlayer init --network mainnet
docker compose -f docker/oss/docker-compose.yml up -d
secondlayer verify all --against <manifest>
```

### Point verify at raw, a decoder, or a subgraph

`secondlayer verify` takes `all`, `raw`, `decode:<name>`, or `subgraph:<name>`. Default is raw. `--quick` hashes identity columns. `--deep` also recomputes semantic digests. `--anchor` requires a verified archive signature.

```bash
secondlayer verify decode:ft_transfer --against ./snapshot.json --deep
```

A target with no matching archive dataset exits `2`: not being able to check is not a pass.

### Run the runtime without an account

`secondlayer init` writes the keys for a machine you operate. `secondlayer bootstrap` restores
verified history. `secondlayer observer` prints the stanza your Stacks node posts to.

```bash
secondlayer init --network mainnet
secondlayer bootstrap --against <manifest>
secondlayer observer
```

No login. No project. `secondlayer instance` is gone; those three verbs are top-level.

### Mint mock sBTC on your devnet

`secondlayer devnet faucet` gives you sBTC to test against, locally:

```bash
secondlayer devnet faucet --to ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
```

No Bitcoin, no regtest, no waiting on someone else's faucet. The sBTC contracts
you deployed make you the signer set on your own chain, so minting is just a
contract call. Refuses any network but devnet, since elsewhere the signer set isn't
yours.

### Check your chain data against a signed archive

`secondlayer verify` tells you whether your history is intact. It compares your database
against a signed canonical archive and names the exact height ranges that
diverge: missing blocks, broken parent links, duplicate heights, forks left on
the losing branch.

```bash
secondlayer verify --against https://archive.secondlayer.tools/.../latest.json
```

It is read-only and nothing leaves your machine: only digests are compared. The
full mainnet chain, 8.7M blocks, checks in well under a minute.

When something is wrong, `secondlayer repair` fixes it. It shows you a plan first, names
every height it would change, and refuses any archive object that does not match
its signed digest.

```bash
secondlayer repair --against <archive>            # plan
secondlayer repair --against <archive> --apply    # fix
```

And `secondlayer bootstrap` stands up a fresh instance from the archive instead of
replaying the chain from genesis, and it shows you both timings before it starts,
then restores and verifies itself.

All three exit with stable codes for CI: `0` clean, `1` divergence found, `2`
unable to verify. That last one never reports as success: not being able to
check is different from being fine.

`secondlayer verify --semantic` recomputes each partition's `sha256:semantic-v1` digest
locally and compares it to the manifest. The default pass hashes identity
columns; `--semantic` also catches divergence in `raw_tx`, event `data`, and
`function_args`.

A published `attestations/<digest>/node.json` is an independent stacks-node
audit of that snapshot. `coverage` is the attested window; `stats` is exact;
`mismatches[]` is a capped sample. Transactions and events stay
`db-reconstructive`: the node does not expose them.

### Mirror the PoX-5 feed in your own database

`secondlayer index codegen` refused `pox5_events` with `Unknown Index table`: the table
was missing from the Index read contract, so the one stacking feed that is
still growing after the epoch 4.0 activation was the one feed a BYO mirror
couldn't hold. (The closed pox-4 era was covered the whole time.)

All three targets now emit it:

```bash
secondlayer index codegen --target kysely --tables pox5_events
```

You get `Pox5Events` for Kysely, `pox5Events` for Drizzle, and a
`model Pox5Events` keyed on `cursor` for Prisma, 21 columns including the
decoded `data` payload.

## Index — August 2026

### Filter FT transfers by asset from the MCP tool

`index_ft_transfers` takes `assetIdentifier`, so an agent can ask for one
token's transfers directly:

```
index_ft_transfers({ assetIdentifier: "SP…sbtc-token::sbtc-token" })
```

The filter was already on the API, the SDK, and the `index_nft_transfers`
twin; only the FT tool's schema was missing it, which left agents scanning
the full feed and paying per row to find one asset.

## SDK — August 2026

### `sbtc.deposits.consume` stops pretending to accept `fields`

`index.sbtc.deposits.consume({ fields: [...] })` type-checked and then ignored
you: the consume loop built its page request field by field and never
forwarded the projection, so you got full rows back either way. The option is
gone, so passing it is a compile error instead of a silent no-op. For a
projected read, use `sbtc.deposits.list` or `.walk`, which have always applied
it.

The same gap was fixed for `index.events.consume` earlier; every Index consume
now forwards its filters from one place, so a filter can't be dropped between
what you asked for and what the loop requests.

### Contract names with underscores work everywhere

`my_contract` is a legal Clarity contract name, but the principal validation
rejected it, so passing `SP….my_contract` as a principal argument threw, both
in `@secondlayer/stacks` and in the contract interfaces the CLI generates.
Those contracts are now usable.

The same change tightens validation in the other direction. A principal
carrying more than one dot segment (`SP….token.extra`) used to slip through:
generated code truncated it to `SP….token` and called a different contract than
you asked for, and the ABI guards passed it along. It throws now. Filter
builders also check the contract name against the Clarity grammar, so a typo'd
id fails where you write it instead of quietly matching zero rows.

`parsePrincipal` and `CONTRACT_NAME_REGEX` are exported from
`@secondlayer/stacks/utils` if you want the same check.

Generated files keep the old behavior until you re-run `secondlayer generate`.

## SDK — August 2026

### `subgraphs.reindex()` takes just a name

`reindex(name)` no longer accepts `fromBlock` or `toBlock`. The API stopped
supporting a ranged reindex: it answers `400 REINDEX_RANGE_NOT_SUPPORTED`, so
the old signature type-checked and then failed at runtime.

Reindex is always the whole subgraph: dropped and rebuilt from your `startBlock`
to chain tip. For a specific range, use `subgraphs.backfill()`, which never drops
anything.

The `subgraphs_reindex` MCP tool drops the same two parameters.

## Subgraphs — August 2026

### Reindex no longer takes a block range

`secondlayer subgraphs reindex` drops `--from-block` and `--to-block`, and the API
rejects either with `400 REINDEX_RANGE_NOT_SUPPORTED`.

A reindex drops your subgraph's tables and rebuilds them. The range only ever
controlled the rebuild, never the drop, so a narrow range didn't reindex those
blocks, it kept them and threw away the rest. The subgraph then looked fine:
status back to `active`, cursor at chain tip, no gaps recorded, nothing logged.
Only counting rows against another source showed the history was gone.

Reindex is now always the whole subgraph: every row dropped, rebuilt from your
`startBlock` to chain tip. The confirmation prompt says so plainly. For a
specific block range, use `secondlayer subgraphs backfill --from-block <n> --to-block <n>`. It never drops anything, and it already refuses handlers that would
double-count on a replay.

## Index — August 2026

### Watch the mempool for one function, not a whole contract

`/v1/index/mempool` takes `function_name`. Watching a privileged function on a
busy contract used to mean pulling every pending call to it and filtering
client-side; now you ask for the one you care about:

```bash
curl http://127.0.0.1:3800/v1/index/mempool \
  -G -d "contract_id=SP….my-dao" -d "function_name=execute-proposal"
```

It composes with `sender`, `type`, and the cursor like every other filter.
`secondlayer index mempool --function-name <name>` and `functionName` on the SDK's
`mempool.list`/`walk` pass it through. A `(contract_id, function_name)` index
backs it, which also puts an index behind plain `contract_id` filtering for the
first time.

## Subgraphs — August 2026

### Sorted reads stay fast on deep pages

Indexed columns now get a `(column, _id)` index alongside the single-column one,
matching how `/v1` orders sorted reads. On a low-cardinality column, a deep page
went from ~17.5 ms and 8,557 buffer reads to ~0.09 ms and 14: Postgres walks
the index in final order instead of re-sorting each tie group.

New tables get it on deploy. Existing tables pick it up on their next reindex or
schema change, not on a plain redeploy.

## CLI — August 2026

### Deploy refuses a subgraph source that isn't committed

`secondlayer subgraphs deploy` now checks whether your definition file is committed to
git before it deploys. If it's untracked or has uncommitted changes, you get a
prompt in a terminal and a hard failure in scripts and CI, because a deployed
definition whose source isn't in version control exists only as a database row,
with nothing to review, diff, or restore from.

Pass `--allow-uncommitted` to deploy anyway; it prints a line saying the check
was skipped, so the choice is visible in CI logs. Deploys from outside a git
repo, and `--dry-run`, are unaffected.

## Subgraphs — August 2026

### Block processing is now replay-proof across restarts

Your subgraph's accumulator tables are guarded against a class of drift that
processor restarts could cause: if two catch-up walkers ever overlap on the
same block (a restart mid-walk, a leadership handoff), only one commit
stands. The block's writes and its checkpoint advance travel in one
transaction with a conditional cursor, so a duplicate application aborts
cleanly and a stale walker can no longer rewind progress and trigger
re-walks. Reorg rewinds still work exactly as before; they take a dedicated
path that serializes across processes.

None of this changes your handler code. `ctx.increment` and friends behave
identically; the guarantee is in the runtime underneath them.

## Index — August 2026

### Staking post-conditions come through on PoX-5 transactions

`/v1/index/transactions` and `/v1/index/mempool` now return the SIP-045
staking and pox post-conditions that arrived with epoch 4.0. Before this, a
`stake` call's post-conditions came back as an empty array, the same answer
you'd get for a transaction that genuinely carried none.

Both types sit on `post_conditions` alongside `stx`/`ft`/`nft`: `staking`
carries a principal, an `amount` (a string, like every other amount), and the
fungible condition code; `pox` carries a principal and one of
`will_not_perform`, `may_perform`, or `will_perform`. `IndexPostCondition` in
the SDK gains both members. Post-conditions are decoded from `raw_tx` at read
time, so every staking transaction already indexed reads correctly now, with no
reindex and no migration.

## Web — August 2026

### The writing section gets a front page and a proper post frame

[/writing](/blog) now leads with the newest post and its signature figure
rendered live as the feature art (the figures are the thumbnails); older
posts drop into a two-column ledger beneath. Posts gain a right-hand meta
rail on wide screens: number, date, reading time, and a figure index that
jumps straight to each figure, and every post now ends with a
previous/next pair and the [feed](/feed.xml), so the writing hands you the
next piece instead of just stopping. Margin sidenotes moved to the left
margin to make room.

### Writings: long-form posts with an interactive figure library

The site grows a [/writing](/blog) section for long-form, mechanism-first
guides that don't fit the docs register. The first post,
[Why your indexer resumes from the same block](/blog/resuming-from-the-same-block),
walks how consume loops, sinks, and checkpoints work: what the three
heights on the health endpoint mean, what a poll costs, and why the cursor
parks on a quiet feed.

Posts are built from a 34-component figure library: annotated payloads,
position tracks, sequence diagrams, hand-built SVG charts with
collision-managed labels, and one interactive explorer per post, all on the
site's tokens in both themes, with reduced-motion and keyboard support
throughout. There's an [RSS feed](/feed.xml) too.

## SDK — July 2026

### blocks_behind now means backlog, not event age

Your consumer on a quiet contract used to report tens of thousands of
blocks "behind" while fully caught up: the number measured how old the
last matching event was, and it only grew. Now `ctx.blocksBehind` (and
`consumerHealth`'s `blocks_behind`) is `tip - scannedHeight`: the highest
block the sweep has *verified*, where an empty page counts as the server
confirming nothing matches up to the tip. A caught-up tail reads ~0. The
new `ctx.scannedHeight` carries the verified position; `ctx.height` still
marks the last delivered row. Capped server scans only claim the cursor
they returned, `finalizedOnly` stops at the finalized boundary, and reorgs
roll the verified position back below the fork.

### Bring any database: sinks are now an open surface

Three sinks ship: `kyselySink`, `drizzleSink` (Postgres and SQLite, with
your schema typing `ctx.tx`), and `bunSqliteSink` (one file, zero
dependencies, no docker), and the machinery behind them is yours to build
on. `createSink(driver)` from `@secondlayer/sdk/sinks/core` owns the
transaction sequences that make a sink correct; your driver is seven methods
of dialect. The contract's 13 invariants are documented on the interface and
mechanically probed by a conformance kit at `@secondlayer/sdk/sinks/testing`,
covering torn batches, cursor replay, inclusive `>=` rollback, and lock
contention. It is the same kit every shipped sink passes in CI.
[Write your own sink](/docs/sinks/custom).

Append-only stores (ClickHouse, parquet) declare
`capabilities: { finalizedOnly: true }`: the consume loop refuses to follow
the unfinalized tip at startup, loudly, instead of corrupting at the first
fork. The Postgres advisory lock also moves to an int8 hash, so two
unrelated sink ids can no longer collide into a false "another consumer"
409.

### The type spelling you meant now works on both feeds

`/v1/streams/events?event_type=print` and `/v1/index/events?types=print` both
work now: each surface accepts the other's spelling for a single type. Ask
the Index for a type *set* and it refuses with the reason: Index pagination
is keyed per event type, by design (a design we wrote down this week). The
CLI takes both flags on both commands.

Streams consumers also close two gaps: the top-level `consume()` iterator
takes the labelled `filters` map, `replay()` can narrow its live tail, and a
literal `types: ["ft_transfer"]` now narrows row types at compile time the
way labels always did. And subgraph factory sources (a router plus pools
created after you deploy) can now be authored through the `on.*` filter
vocabulary; every non-subgraph projection refuses the field loudly.

### Filter renames stop pretending to be capability gaps

`on.stxLock({ lockedAddress }).toIndexParams()` works now: the Index stores
the locked address as the row's `sender`, so the field is a rename, not
something the surface can't do. Same for `caller` on contract-call reads:
the endpoint filters by tx sender, which is the caller. And `trait` with
`contractId` fails at projection time with the fix in the message, instead of
as a server 400 after the request already left.

Two silent-wrong-results bugs are also gone: `streams.events.stream()` now
forwards the labelled `filters` map (it used to silently deliver the full
firehose, billed per row), and a wildcard inside a `contractId` array is
refused at projection like scalar wildcards always were, instead of reaching
the wire as a literal match that returns zero rows.

## Index — July 2026

### One day of dogfooding, four fixes

We ran the whole surface from a fresh repo with one API key, the way you
would. What broke got fixed the same day:

- `on.ftTransfer({ assetIdentifier })` projected a filter the Index refused:
  the server accepted `asset_identifier` for nft event reads but not ft. It
  now works on all six token event types, and `ftTransfers.list` takes
  `assetIdentifier` directly.
- `pox5/events` and `sbtc/deposits` 500'd, and `transactions` returned a
  corrupt cursor, when `fields` omitted the index column, because pagination
  machinery was reading projected rows. Cursors and reorg spans now always
  come from the raw rows.
- `/v1/subgraphs/<name>/<table>?_fields=…` without `_id` returned a full page
  with `next_cursor: null`, silently dropping the rest of your rows. `_id`
  now drives the cursor server-side whether or not you asked for it.

### Field projection reaches the transfer feeds

```ts
const { events } = await sl.index.ftTransfers.list({
  contractId,
  fields: ["amount", "sender", "recipient"],
});
events[0].tx_id; // ✗ not requested, not fetched, not in the type
```

`ft-transfers` and `nft-transfers`, almost certainly the highest-traffic
Index reads, now take `fields`, with the same contract as everywhere else:
requested columns come back, a misspelled one is refused, and the row type
narrows to match. `cursor`/`block_height` always survive, plus `event_type`
because it is the row's discriminant. Omitting `block_time` also lets the
server skip the `blocks` join entirely.

We previously wrote here that these rows were "5-8 columns" and not worth
projecting. That was a measurement error: we counted only the type-specific
columns and missed the 12-column base they extend. A transfer row is 17
columns wide, wider than the withdrawals we did project. Corrected.

`blocks` alone stays unprojected, now for a checked reason: it is 8 columns
read off a single table: `block_time` is a column of the row itself, so
unlike the event feeds there is no join for a projection to skip.

### Field projection across the Index surface

```ts
const { withdrawals } = await sl.sbtc.withdrawals.list({
  fields: ["amount", "status"],
});
withdrawals[0].sender; // ✗ not requested, not fetched, not in the type
```

Deposits, withdrawals, transactions, and pox-5 events all take `fields` now,
each narrowing its row type to what you asked for. **Which columns always
survive differs per resource, on purpose**: a withdrawal is keyed by
`request_id` and has no `block_height`, a transaction keeps `tx_id`, and a
pox-5 event keeps `topic` because it is the row's discriminant. Dropping those
would leave a row you cannot paginate or identify.

`blocks`, `ft_transfers`, and `nft_transfers` did not take `fields` in this
release. The "5-8 columns" rationale originally published here undercounted
the transfer rows; see the correction above. Both transfer feeds have since
gained `fields`.

### Field projection reaches sBTC deposits

```ts
const { deposits } = await sl.sbtc.deposits.list({
  confirmed: true,
  fields: ["bitcoin_txid", "output_index", "amount", "tx_id", "block_height"],
});
deposits[0].sender; // ✗ not requested, not fetched, not in the type
```

Projection shipped on `/v1/index/events` and stayed there, so every sibling
resource returned full rows whether or not you wanted them. The parse-and-strip
logic is now shared, which keeps the two rules that matter identical wherever
it is applied: a misspelled field is **refused** rather than quietly ignored,
and `cursor`/`block_height` always survive so pagination and reorg reporting
cannot be projected away.

## Reliability — July 2026

### A reorg is confirmed before it overwrites anything

A block arriving for a height we already hold no longer replaces it on sight.
The node's event observer emits competing blocks at the same height routinely,
so a hash mismatch says "someone built a different block here", not "the chain
moved", and only the second justifies orphaning indexed data. Guessing picked
the losing fork twice in one week.

The contender is now staged and the **next block decides**: a block names
exactly one parent, so whichever side it names is the one the chain kept. Cost
is one block of latency on a genuine reorg; the alternative was sitting on the
wrong chain until someone noticed.

### Integrity asks whether the chain joins up

`findBrokenLinks` reports canonical heights whose block does not descend from
the one below it, surfaced as a `chain_unlinked` status. The previous check only
asked whether every height was *present*, which a chain on a losing fork
answers yes to.

Orphaned blocks the canonical chain still links to are now reclaimed
automatically, using local data only: if the canonical block at `h + 1` names
an orphaned block at `h` as its parent, that block is on-chain by definition.

### Status reports progress, not just liveness

`/public/status` reported `subgraph_processor: ok` through a seventeen-hour
stall: the loop was ticking and heartbeating while making no progress. It now
also checks how far the furthest-behind subgraph trails the tip.

### Decoded rows and their checkpoint commit together

Every decoder wrote its rows and then its checkpoint as two separate
statements; a crash in between re-delivered the batch on resume, survivable only
because the writes happened to be idempotent upserts. They now share one
transaction. For BNS this also pulls its `bns_names` projections into the same
commit, which previously could lag the events they derive from.

## Stacks SDK — July 2026

### `on` is the filter union (breaking)

```ts

on.ftTransfer({ assetIdentifier: `${SBTC}::sbtc-token` }).toIndexParams({ limit: 50 });
```

The name used to hold a second, older set of factories keyed on snake_case
**database column** names and typed `Record<string, …>`, so a typo compiled and
silently matched nothing. They had no consumers, and they are gone, along with
the `Filter` / `FilterClause` / `FilterOperator` / `FilterPrimitive` /
`SubscriptionFilterSpec` / `BnsAction` / `FactoryTarget` / `PoxFunction` types.

`on` at the package root is now the same typed union as
`@secondlayer/stacks/filters`: per-event-type fields, and explicit projections
onto every surface. Using `on.transferTo(...)`, `on.bnsName(...)`, or
`on.poxStack(...)`? Build the filter with `on.ftTransfer({ … })` and project it
onto the surface you're calling.

### Field projection narrows everywhere it applies

`events.walk({ fields })` now narrows its row type the way `events.list` already
did; it was forwarding `fields` to the wire, so it yielded stripped rows while
the type promised every column. The callable `index.events({ fields })`
shorthand narrows too. `events.consume` no longer accepts `fields` at all: the
consume loop never forwarded it, so it type-checked and was then dropped.

## Agents — July 2026

### The docs, as text an agent can read

- **`/llms-full.txt`**: every docs page in one markdown file, sidebar-ordered,
  each passage tagged with the page it came from.
- **Any page as markdown**: append `.md` to a docs URL
  (`/docs/streams.md`). Generated from the MDX itself, so it can't drift.
- **`?mode=agent` now works.** It was advertised in `llms.txt` but only ever
  set from localStorage, so the URL did nothing. Agent mode also stopped
  replacing the page, so you get the prompt deck *and* the reference content.
- **`AGENTS.md` ships inside `@secondlayer/sdk`** with the five facts you
  can't infer from the types: reads need no key, cursors are opaque, reorg
  rollback is `>=` not `>`, rows and cursor commit in one transaction, and
  `walk()` is not reorg-safe.

### MCP

`codegen_index_schema` emits an ORM schema for the Index tables in-conversation,
from the same generator as `secondlayer codegen index`. `get_contract_abi` becomes
`contracts_get_abi` (the unprefixed name filed itself under a phantom "get"
product in the capability listing); the old name keeps working until the next
major.

## Subgraphs — July 2026

### Read contract state from a handler

```ts

const token = readContractAt(ctx, contractId, SIP010, {
  cache: "contract-constant",
});
const decimals = await token.read.getDecimals({}); // bigint, typed from the ABI
```

Some facts aren't in any event (a token's `decimals`, a pool's reserves), so
`amount` stayed a bare integer nobody could render. Now a handler can ask the
contract, with the ABI's camelCased read methods and no `call` (a handler
indexes the chain, it never writes to it).

- **Pinned to the block being processed.** The call carries that block's
  `index_block_hash`, so a handler is a pure function of its block and a
  reindex produces byte-identical rows. A block whose id was never persisted
  throws rather than silently reading at the node's tip.
- **Cached in Postgres**, keyed on the block id, so a reorg-replaced block
  can't inherit the orphaned fork's answer. Pass
  `cache: "contract-constant"` for values that genuinely can't change and it
  is fetched once, ever; that is what makes a full backfill affordable.
- Unit tests stay offline: `createTestContext(schema, { reads })` stubs reads
  by `"<contract>.<function-name>"`.
- Self-hosting needs `STACKS_NODE_RPC_URL` on the subgraph processor: the
  node you already run, not a new service.

The `sip-010-balances` starter now labels tokens with their real symbol and
decimals.

## Streams — July 2026

### Track two things in one loop

```ts
await sl.streams.events.consume({
  filters: {
    peg: { types: ["ft_transfer"], assetIdentifier: `${SBTC}::sbtc-token` },
    treasury: { types: ["stx_transfer"], sender: TREASURY },
  },
  on: {
    peg: (events) => credit(events),      // narrowed to ft_transfer
    treasury: (events) => debit(events),  // narrowed to stx_transfer
  },
});
```

A flat filter is one AND-ed set, so two unrelated concerns meant two consume
loops, two cursors, and two checkpoints, or fetching the union and discarding
half of it. Name each concern and the server ORs them into a single scan.

- Each label's declared `types` narrows its handler, so `event.payload` is
  typed without an `event_type` guard, and adding a label to `filters` is a
  compile error until `on` handles it.
- One cursor covers every label. The position is computed over the block's
  full event set, so you can add or drop labels between restarts and keep the
  checkpoint.
- Over REST it is one JSON query param, and each event carries the labels it
  matched. SSE takes the same param, so `subscribe` narrows the same way.

## Index — July 2026

### Ask for the columns you want

```ts
const { events } = await sl.index.events.list({
  eventType: "ft_transfer",
  fields: ["recipient", "amount"],
});
events[0].asset_identifier; // ✗ not requested, not fetched, not in the type
```

The server projects the SELECT, so an unrequested column is physically
absent, and now the row type says so, making a read of one a compile error
instead of `undefined` at runtime.

- Omitting `block_time` lets the server skip the `blocks` join entirely. It
  is not a stored column but a `to_timestamp()` off a join taken on every
  read, so this is a planner-level saving on wide sweeps.
- `cursor`, `block_height`, and `event_type` always come back, because pagination
  and the union discriminant don't depend on what you asked for. Reorg
  reporting is unaffected too.
- It does not change your bill: Index meters per row, not per field.

## Subgraphs — July 2026

### Run a handler before you deploy

```bash
secondlayer subgraphs test subgraphs/bns-names.ts --from 167484 --to 167600
secondlayer subgraphs test subgraphs/bns-names.ts --offline
```

Real chain events, your local handler code, no deploy. The first run records
a cassette so later runs are offline and free; changing a source filter
discards it rather than passing against data your subgraph would no longer
request. If events arrive and your handlers write nothing, it fails, and that is
the shape of the field-mapping bug that ships a 0-row subgraph.

For unit tests, `@secondlayer/subgraphs/testing` gives you the same context
the runtime uses, backed by memory, so read-your-writes, `increment` deltas,
and upsert merging behave exactly as they do in production.

### Index a contract set, including one that grows

`contractId` takes an array, so a router plus its pools is one source and one
handler. For pools created *after* you deploy, a `factory` discovers
addresses from another source's events:

```ts
swaps: {
  type: "print_event",
  topic: "swap",
  factory: { from: "registry", field: "data.pool" },
}
```

A pool discovered in block N receives its own block-N events, and the
discovered set rolls back on a reorg like any other chain-derived state.

### Hoistable schemas, and balances that don't lose updates

`defineSchema()` lets you pull the schema out of the call and keep helpers
fully typed. The `sip-010-balances` starter no longer reaches for `ctx: any`;
it moves balances with `ctx.increment`, which applies a delta atomically
instead of a read-modify-write that silently loses same-block updates.

Source filters are now validated as a real discriminated union, so a field
that a source type doesn't support fails at deploy instead of matching
nothing forever.

## Subgraphs — July 2026

### Print payloads are typed all the way down

`prints` could only describe flat fields, so a nested payload had to be
declared `"jsonb"`. That is how a handler reading flat `data.name`
type-checked, deployed, and decoded to null on every event while BNS-V2
emitted `name` as a nested tuple: zero rows chain-wide, while the subgraph
tailed happily at the tip.

```ts
prints: {
  "name-register": {
    name: { tuple: { name: "text", namespace: "text" } },
    owner: "principal",
    memo: { type: "text", optional: true },
  },
}
// event.data.name.namespace → string
// event.data.memo           → string | undefined
```

- Declaring `prints` also turns on runtime validation: an event that does not
  match is skipped and logged instead of written as nulls. It never throws, since
  a block that can't advance would wedge your checkpoint.
- Deploy now refuses (rather than warns) when a handler on a `prints`-declaring
  source reads a field no observed event carries.
- The `abi` on a source is validated at deploy against the real Clarity ABI
  shape. A raw Hiro or Clarinet ABI used to pass and then mis-decode
  `event.input` forever; the error now names the fix.

## CLI — July 2026

### One verb for generating code

`secondlayer codegen contracts | subgraph | index | client | prints` replaces six
entry points under three verbs. `--target` means the same thing with the same
default everywhere; previously `secondlayer subgraphs codegen -o db.ts` wrote Prisma
while the identical-looking `secondlayer index codegen -o db.ts` wrote Kysely. Old
paths still work, print a deprecation notice, and keep their original
defaults so your output doesn't change.

- Scaffolds now emit the contract's `as const` ABI and reference it, so
  handlers read `event.input.tokenId` instead of `event.args[0] as bigint`.
- Generated row types are derived from the schema instead of a second
  hand-written map that answered `uint` with `number`, which didn't compile
  against the client it shipped beside, and truncated `uint128`.

## SDK — July 2026

### Sinks: your indexer is one file now

`kyselySink` owns the three hard parts of a durable indexer (checkpoint
persistence, rows+cursor atomicity, and reorg rollback), so your handler is
just the writes:

```ts
const sink = kyselySink(db, { id: "sales", tables: ["sales"], height: "block_height" });

await new Index().contractCalls.consume({
  contractId: MARKETPLACE, functionName: "purchase-asset", fromHeight: 0,
  sink,
  signal: shutdownSignal(),
  onBatch: (calls, _env, ctx) =>
    ctx.tx.insertInto("sales").values(calls.map(toSale)).execute(),
});
// no onReorg, no checkpoint table, no SIGTERM block
```

- Rows and cursor commit in one transaction; a crash mid-batch aborts both
  and the batch is simply re-read. Reorg rollback is automatic and
  unconditional, so forgetting `onReorg` can no longer skip reorgs silently.
- `ctx.tx` is a fully typed `Transaction`; a table missing the height
  column fails loudly at startup, not silently at reorg time.
- `consumerHealth()` answers your platform's liveness probe (page recency,
  never lag, since a genesis backfill is millions behind and healthy) and
  `shutdownSignal()` finishes the in-flight batch on SIGTERM.
- On Streams, `decoded: true` delivers events as the same flat rows Index
  serves; the eleven guard+decode pairs are deprecated.
- The flagship sales-index example dropped from 167 lines across three files
  to one 61-line file, and CI keeps it that way.

### Write the filter once: `on.*`

One chain event used to be spelled differently on every surface: `eventType`
on Index, `types` arrays on Streams, a `sources` entry on subgraphs, a
trigger object on webhooks, with three incompatible `minAmount` types
among them. The new `@secondlayer/stacks/filters` module is the single
spelling, projected explicitly to each surface:

```ts

const usdc = on.ftTransfer({ assetIdentifier: USDC, minAmount: 1_000_000n });

sl.index.events.list(usdc.toIndexParams({ limit: 100 }));
sl.streams.events.consume({ ...usdc.toStreamsParams(), onBatch });
sl.webhooks.create({ name, url, triggers: [usdc.toChainTrigger()] });
defineSubgraph({ sources: { usdc: usdc.toSubgraphSource() } });
```

- A surface a filter can't reach is a missing method: a compile error, not
  a runtime surprise. A field a surface can't express throws at projection
  time with a message naming the surface that supports it.
- Amounts are `bigint` end to end and stringify at exactly one boundary, so
  a spread bigint can never crash `JSON.stringify` again.
- Typos fail at construction: a contract id where an asset identifier
  belongs, or a malformed principal, is an immediate error instead of a
  silent zero-row query.

## Subgraphs — July 2026

### Add an index without losing your data

Flipping `indexed: true` (or `search`) on a column of a populated subgraph now
builds the index in place with `CREATE INDEX CONCURRENTLY`, so it no longer
counts as a breaking change, so it no longer costs a `DROP SCHEMA` and a full
reindex. The most routine operation in an indexer's life is now a few seconds
of DDL instead of hours of backfill.

- `uniqueKeys` changes are no longer invisible to deploys: additions run
  `ALTER TABLE … ADD CONSTRAINT` in place, and removals are correctly refused
  as breaking. Previously a `uniqueKeys` addition reported success, created
  nothing, and every later `upsert` failed against a constraint that didn't
  exist.
- Handler-only redeploys now warn that new logic applies from the current tip:
  rows already indexed keep the previous handler's output unless you
  reindex.
- The CLI confirms destructive deploys **before** sending the request. The
  old prompt fired after the server had already dropped your data.

### `findMany({ fields })` returns what it says

Requesting specific fields now narrows the row type to exactly those columns.
The server always projected the SELECT, so unrequested fields were physically
absent while TypeScript promised they existed, so reading one is now a compile
error instead of a runtime `undefined`.

## SDK — July 2026

### A 429 no longer kills your backfill

Both consume loops now retry the page fetch on rate limits, server errors,
and network failures, honoring the server's `Retry-After`, so one transient
error hours into a genesis sweep no longer costs you the process. Retry wraps
only the fetch: your `onBatch`/`onReorg` code and real 4xx mistakes still
fail fast.

- Tune with `retryCount` / `retryDelay` (the same knobs as
  `@secondlayer/stacks` transports); watch with the `onError` observer.
- One error family: `RateLimitError`, `AuthError`, and `ValidationError` are
  now `ApiError` subclasses thrown by every surface, so a single
  `instanceof` check works around Index, platform, and Streams calls alike.
- Errors carry `retryable`, `retryAfterSeconds`, a `docsUrl` pointing at the
  fix, and `walk()` for cause-chain inspection.

### finalizedOnly consumers can no longer skip events

Returning `envelope.next_cursor` from a `finalizedOnly` consumer's `onBatch`
now throws instead of silently committing past events that were never
delivered. The filtered unfinalized tail is re-read once it settles; a cursor
above it would have dropped those events forever, with reorg handling skipped
in that mode. The loop now catches the one wrong return it can detect.

- `fetchImpl` is honored by every platform client, so you can inject a fetch for tests,
  proxies, or an x402-wrapped payer without monkey-patching the global.
- x402 payments and proof verification no longer touch `Buffer`, so both work
  on edge runtimes like Cloudflare Workers.

## Index — July 2026

### Read PoX-5 events at `/v1/index/pox5/events`

The decoded pox-5 log is now queryable. One row per print event across all 19
topics, with the full decoded tuple in `data`, so nested shapes like
`btc-lockup`, `bond-rewards`, and `bond-periods` come through intact rather
than flattened away.

- Filter by `topic`, `staker`, `signer`, `signer_manager`, `bond_index`, or
  `reward_cycle`, and page with the same `<block_height>:<event_index>` cursor
  the rest of Index uses.
- `?confirmed=true` clamps the window to the finality boundary, so settlement
  consumers only see rows past the reorg margin.
- Every response carries `tip` and a `reorgs[]` array for the page's height
  range, so you can reconcile anything you already committed from a fork.

### PoX-5 events, decoded from activation block one

The indexer now ships a PoX-5 decoder alongside the existing PoX-4 dataset.
Every print event the new `pox-5` boot contract emits, all 19 topics, from
`stake` and `register-for-bond` through `claim-rewards` and signer grants,
lands in a queryable `pox5_events` dataset with typed columns for stakers,
signers, bonds, amounts, and cycle windows, plus the full event tuple as JSON.

- Nothing to configure: the decoder is on by default and idles until the
  Epoch 4.0 fork activates at Bitcoin block 960,230. Your first pox-5 event
  is decoded the moment it exists.
- Self-hosting? Raw events are always stored, so upgrading after the fork
  backfills the dataset with zero data loss, and no re-sync from your node.
- `@secondlayer/stacks` now exports `POX5_EVENT_TOPICS`, `Pox5EventTopic`,
  and `POX5_CONTRACT_ID_MAINNET` for building your own event tooling against
  the same verified topic set.

## SDK — July 2026

### Mirror the sBTC peg with a checkpointed consumer

`index.sbtc.events.consume()` and `index.sbtc.deposits.consume()` bring the
decoded peg feed the same cursor commit, reorg rewind, and progress context
every other Index loop has. Server-side `topic` filtering survives pagination,
so following one topic no longer means pulling all six.

- Building a durable sBTC index used to mean consuming raw `print` events off
  the registry contract and matching topics yourself, losing the typed columns.
- `withdrawals` has no `consume` on purpose: the row is a lifecycle aggregate
  that mutates as a peg-out settles, and a forward-only cursor would commit it
  once and never see it change. Consume the append-only events instead; see
  [sBTC settlement](/docs/index#sbtc).

### Follow a whole protocol on one cursor

`contractId` now takes an array on `index.events` and `index.contractCalls`.
sBTC is four contracts; a `v3` → `v4` migration is two. One consumer, one
checkpoint, one answer to "how far is my index complete".

- Up to 20 ids, still mutually exclusive with `trait`; reach for `trait` when
  you mean "every contract of a standard", since it picks up contracts
  deployed after you ship.

### Rows match the event type you asked for

`index.events.list`, `.walk`, and `.consume` are generic over the `eventType`
literal, so `consume({ eventType: "ft_transfer" })` hands `onBatch` an
`IndexFtTransfer[]` with `amount` and `sender` reachable directly.

- The `if (e.event_type !== "ft_transfer") continue` line every handler opened
  with, written only to satisfy the compiler, is gone.
- A non-literal `eventType` still yields the union, so dynamic callers are
  untouched.

### Every batch tells you how far along it is

`onBatch` now hands you `height`, `tipHeight`, and `blocksBehind` alongside
`cursor`, on both the Index and Streams consumers. Answering "is my indexer
still moving" no longer means tracking it yourself.

- `height` survives empty pages, so a consumer caught up at the tip keeps
  reporting its position instead of dropping to nothing.
- After a reorg it rolls back with the chain, to the last block still
  canonical, the case a hand-rolled `at(-1)` gets wrong.
- Wire it straight into a health check: see [Deploy](/docs/deploy#bind-a-port)
  for why your process needs one and what to gate it on.

### Prove a Bitcoin payment without naming a contract

`verifyBitcoinPayment` resolves the reference `spv-adapter` on mainnet, so
`contract` is now optional. Build a proof, pass it, get `verified`, with no adapter
principal to look up, and no verifier contract of your own to deploy first.

- The adapter is a read-only wrapper over the SIP-044 built-ins, so it goes
  live with the Epoch 4.0 fork at Bitcoin block 960,230. Reads before the fork
  still need Clarinet simnet.
- Stacks testnet has no Epoch 4.0, so the built-ins don't exist there. Pass an
  explicit `contract` on testnet, and the thrown error now says exactly that
  instead of telling you to wait for an activation that isn't coming.
- `SPV_ADAPTER_CONTRACTS` and `getSpvAdapter(network)` expose the principal if
  you'd rather pin it yourself.

### One constant for the Epoch 4.0 activation height

`EPOCH_4_ACTIVATION_BURN_HEIGHT_MAINNET` is Bitcoin block 960,230. Epoch 4.0
carries both SIP-044 (the Bitcoin SPV built-ins) and SIP-045 (`pox-5` Bitcoin
Staking) share one fork and one height, so the bitcoin and pox5 modules read it from
the same place and can't drift apart.

- Only mainnet has a fixed height. Elsewhere, read it from the node with
  `getPox5Activation` or pass it explicitly.

### Clients pick up `SL_API_KEY` from your environment

Export `SL_API_KEY` and `new Index()` uses it, the same variable the CLI and
MCP server already read. No more constructing a client keyless by accident,
then hitting `402 PAYMENT_REQUIRED` the first time you reach past the free
24-hour window.

- An explicit `apiKey` still wins. Pass `apiKey: ""` to force keyless reads on
  a machine that has a key exported.
- Exported as `resolveApiKey` if you want the same precedence in your own code,
  and guarded for browsers and edge runtimes where `process` is undefined.

### Name any decoded event, not just the union

`IndexEvent` is a union of eleven shapes, and until now you could name the
union but none of its members. Every variant is exported from the package root:
`IndexFtTransfer`, `IndexNftTransfer`, `IndexStxTransfer`, `IndexStxMint`,
`IndexStxBurn`, `IndexStxLock`, `IndexFtMint`, `IndexFtBurn`, `IndexNftMint`,
`IndexNftBurn`, and `IndexPrint`.

- Write the handler signature you actually want,
  `function onTransfer(e: IndexFtTransfer)`, instead of taking `IndexEvent`
  and re-narrowing inside every function.
- The `consumeIndexFeed` types come with it: `IndexFeedItem`,
  `IndexFeedEnvelope`, and `IndexFeedFetcher` are all importable from
  `@secondlayer/sdk`, so you can type a custom fetcher or hold a feed item in a
  variable without reaching into a subpath.
- Purely additive: nothing was renamed or removed.

### PoX-5 events from `sl.index.pox5`

The decoded pox-5 feed has a typed client. `sl.index.pox5.events.list()` for a
page, `.walk()` to stream every event across pages without writing cursor
logic yourself.

- Filters map to the endpoint one-to-one: `topic`, `staker`, `signer`,
  `signerManager`, `bondIndex`, `rewardCycle`, plus `confirmed` to stay behind
  the finality boundary.
- `amount_ustx` and `amount_sats` stay strings, so stacking amounts past
  `Number.MAX_SAFE_INTEGER` survive the round trip intact.
- Types are importable from the package root: `IndexPox5Event`,
  `IndexPox5EventTopic`, `Pox5EventsEnvelope`, and the list/walk param shapes.

### Chain webhooks decode and narrow from one import

`decodeChainWebhook` is now reachable from the SDK root. Verify a delivery's
signature, hand the same raw body to the decoder, and switch on
`data.trigger` with your event fully narrowed, with no hand-rolled parser and no
guessing which fields a `print_event` versus a `contract_call` delivery
carries.

- The result type comes with it: `import type { ChainWebhookDelivery }` from
  `@secondlayer/sdk` alone. Every piece the union composes is exported too:
  the per-trigger `Chain*Data` shapes, `ChainEventEnvelope`,
  `ChainTxLevelEvent`, `ChainReorgRollbackDelivery`, and the sBTC deposit and
  withdrawal payloads.
- The decoder validates as it narrows. A body that isn't a `chain.*`
  delivery, or whose `type` disagrees with `data.trigger`, throws instead of
  quietly handing you a mistyped object.
- `WebhookHeaderInput` and `StandardWebhooksHeaders` are exported as well, so
  you can type your own framework adapter around `verifyWebhookSignature`.

### PoX-5 calls, fully typed

Every `client.pox5.*` action and read now runs through the module's committed
pox-5 ABI: arguments are checked at compile time, so a wrong name or type is a
`tsc` error rather than a failed transaction, and reads return plain JS values
instead of raw Clarity envelopes.

- Reads decode for you: `uint` becomes `bigint`, tuples become camelCase
  objects, and an optional read returns `null` when the record doesn't exist.
  `getStakerInfo(...)` hands you `{ amountUstx, firstRewardCycle, numCycles,
  signer } | null` directly.
- One breaking change: the 12 individual reads no longer return
  `ClarityValue`. `getStakerState` is unchanged.
- The ABI is machine-extracted from the `pox-5` boot contract and guarded by
  a simnet drift test, so a stacks-core contract change fails our build, not
  your integration.

### Typed Clarity narrowing, cheaper signing

`cvToJSON` returns `any`, which switches the compiler off at the exact boundary
where Clarity meets your code. Five new helpers (`cvToBigInt`, `cvToString`,
`cvToBuffer`, `cvToBoolean`, `cvToPrincipal`) narrow a `ClarityValue` to a
typed primitive or throw with a message naming the mismatch, so a wrong
assumption fails at the call site instead of three functions later.

- Signing is cheaper: transaction serialization is memoized per object, so
  multi-sig and sponsored flows no longer re-serialize the same transaction on
  every signer step. Wire bytes are unchanged.
- Multi-sig metadata survives the wallet round-trip: `_multisig` signer keys
  set at build time carry through provider signing, so auto-detection keeps
  working after `stx_signTransaction` returns.
- `cvToJSON` and `cvToValue` are untouched, so existing code keeps working.

### PoX-5 Bitcoin Staking, two weeks before the fork

Build against SIP-045 today: the new `@secondlayer/stacks/pox5` module ships
every staking flow (bonds, STX staking, L1 lockup scripts, signer-key
grants), pinned against the final `pox-5` contract in stacks-core 4.0.0.
Your integration gates itself: `client.pox5.isActive()` asks the chain, so
code you ship now simply switches on when Bitcoin block 960,230 lands
(~July 29).

- 13 wallet actions that inherit fee tiers, nonce management, and typed
  errors; `getStakerState()` reads a staker's whole position in one batched
  call.
- Off-chain tooling works pre-activation: CLTV lockup scripts and P2WSH
  addresses, SIP-018 signer grants, cycle and bond-phase math.
- Not a hand-transcribed ABI: scripts and grant hashes are byte-compared
  against the actual boot contract in Clarinet simnet, and every action is
  pinned to the contract interface through the real signing path.

See the [PoX-5 staking docs](https://stacks.secondlayer.tools/guide/pox5).

### More node data, fewer silent failures

New actions round out the read surface, and a fixed-up transport catches
errors it used to swallow: `getContractSource`, `getRawBlock`,
`getAccountHistory`, `getMempoolStats`, and `getNftHoldings` join the SDK's
action set, and every read now throws instead of quietly treating a failed
request as real data.

- `getContractSource` and `getRawBlock` read straight from a stacks-node
  (Clarity source, raw block headers) for cases the indexed extended API
  can't serve.
- `getAccountHistory`, `getMempoolStats`, and `getNftHoldings`, promoted out
  of the AI-tools layer into first-class actions, also available on
  `client.*`.
- The HTTP transport throws a typed error on any failed request and now
  retries rate limits (429), not just server errors, so a 404 body no longer
  gets parsed as if it were a successful response.

### Bitcoin addresses without the Bitcoin baggage

Derive your paired BTC account, the `bc1q…`/`bc1p…` addresses Leather and
Xverse show next to your Stacks address, from the same mnemonic, with zero
new dependencies. And ask the chain, not a config file, where sBTC deposits
go: `client.sbtc.getSignersAddress()` derives the signers' taproot address
from the on-chain registry, encoded for whatever network your client points
at.

- `mnemonicToBitcoinKeys` covers BIP84 (native segwit) and BIP86 (taproot)
  paths, validated against the official spec vectors.
- Pubkey→address helpers (`publicKeyToP2trAddress`, `taprootTweakPubkey`)
  ship in `@secondlayer/stacks/bitcoin`: pure derivation, no tx signing.
- Network-aware end to end: mainnet, testnet, and regtest encodings, so a
  devnet integration never shows you a mainnet address by mistake.

### Send, price, and confirm transactions in one flow

Pass `fee: 'min' | 'low' | 'mid' | 'high'` to any send action and await the
result with `client.waitForTransactionReceipt({ txid, confirmations })`, with no
more hand-rolled fee estimates or polling loops. `'min'` is the node's relay
floor (1 uSTX per byte), computed offline, and it's also the automatic
fallback when the node can't produce an estimate, so your broadcast goes
through instead of erroring.

- `waitForTransactionReceipt` returns a normalized receipt with the decoded
  Clarity result, rejects with typed errors on abort/drop/timeout, and
  recounts confirmations if a reorg moves your transaction.
- Status sources are pluggable: the default reads any extended-API host;
  `indexTxSource()` reads Secondlayer's index and gets the chain tip in the
  same response, so N-confirmation waits cost one request per poll.
- `sendTransaction({ wait: 2 })` broadcasts and confirms in one call; x402
  settlement can now await the payment landing before reporting it.
- Broadcast rejections are typed: `BroadcastError.reason` narrows across all
  26 stacks-node rejection strings, with the node's `reason_data` attached.

## CLI — July 2026

### Watch mode and dependency types for contract codegen

`secondlayer contracts generate --watch` regenerates your typed clients the moment a
`.clar` file, your config, or `Clarinet.toml` changes, so leave it running next
to your editor. And the `clarinet()` plugin now types the contracts you depend
on, not just the ones you wrote: everything under `[project.requirements]`
gets a typed client by default (`includeRequirements: false` to opt out).

- Generated output now ships named type aliases per function
  (`TokenTransferArgs`, `TokenTransferResult`) and a `TypedAbi`-branded ABI
  const, so hovers and type errors name your types instead of dumping expanded
  conditionals.
- Generated imports target real `@secondlayer/stacks` subpaths; previously
  emitted root imports didn't resolve.
- Breaking: the `actions()` codegen plugin is gone. It generated code against
  an API surface that no longer exists; the generated contract objects and
  `getContract` cover the same ground, typed.

## Index — July 2026

### Reorg-safe contract lookups

`GET /v1/contracts/:contractId` no longer serves contracts that were reorged
out of the canonical chain. A reorg flips the registry row non-canonical, and
the moment the deploy reappears on the new fork, discovery restores it
automatically, so your fetched ABIs survive the round trip.

- Trait discovery (`/v1/contracts?trait=`) and as-of-block trait resolution
  already excluded reorged-out contracts; the by-id read now agrees with them.
- The burnchain reward tables (`burn_block_rewards`,
  `burn_block_reward_slots`) drop their never-enforced `canonical` column;
  replace-per-height is the documented reorg contract for burnchain data. If
  your `secondlayer index codegen` mirror includes these tables, re-run codegen and
  drop the column; it was always `true`, so nothing is lost.

## SDK — July 2026

### Named types on hover, unsigned transactions on demand

Your generated ABIs now carry their own type names. `getContract` picks up the
`TypedAbi` brand emitted by `secondlayer contracts generate`, so hovering
`contract.call.transfer` shows `TokenTransferArgs`, cmd-clickable and readable
in errors, instead of a wall of inferred conditionals. Hand-written
`as const` ABIs keep working exactly as before.

- New `contract.buildCall.*` namespace builds unsigned transactions for
  wallet-signs-later flows; it never broadcasts, and fee and nonce auto-resolve when
  you omit them. Plus `publicKeyToAddress` in `@secondlayer/stacks/utils`.
- `jsToClarityValue` takes pre-built ClarityValues anywhere, and buffer args
  accept hex strings, `Uint8Array`, or tagged `{ type, value }` objects.
- Read methods on `(response ok err)` outputs now type as the `ok` value,
  so no more `unknown` leaks out of typed reads.

### SIP-045 staking post-conditions, ready before the fork

`@secondlayer/stacks@2.10.0` speaks the Epoch 4.0 wire format ahead of the
Bitcoin Staking hard fork (SIP-045, targeted ~July 29). The transaction codec
decodes and encodes the two new post-condition types (`0x03` Staking and
`0x04` PoX), byte-identical to the reference implementation, so your decoders
don't break the day pox-5 transactions hit the chain.

- Protect staking calls with `staking-postcondition` (principal + amount bound,
  evaluated on `stake`, `register-for-bond`, `stake-update`) and
  `pox-postcondition` (`will-not-perform` / `may-perform` / `will-perform`,
  evaluated on `unstake`, `announce-l1-early-exit`, and other non-locking PoX
  calls).
- The deserializer now fails loud on unknown post-condition types instead of
  silently misreading everything after them, so malformed or future-format
  transactions surface as errors, not corrupt data.
- `ClarityVersion.Clarity6` ships for Epoch 4.0 contract deploys (SIP-044).

Type names mirror stacks.js, so post-conditions written for either SDK are
portable. See the [Stacks SDK docs](https://stacks.secondlayer.tools/guide/transactions).

## Index — June 2026

### Build your app index with `consume()`

Index now ships the loop. `index.events.consume()` and `index.contractCalls.consume()` are checkpointed consumers over decoded rows, with no node, decoders, or sync loop to hand-roll.

- Write your rows inside `onBatch`, return the cursor you committed, and crash-restart resumes exactly there.
- Reorgs rewind the cursor to the fork point automatically.
- `finalizedOnly` holds delivery to rows at or below `tip.finalized_height` (which the Index tip now reports).
- `fromHeight: 0` backfills from genesis.
- Runnable example: [`sales-index`](https://github.com/ryanwaits/secondlayer/tree/main/examples/sales-index) indexes every Gamma marketplace sale into your own Postgres in ~50 lines, then graduates to the same table as a one-file Subgraph.

### Empirical print-event schemas

Clarity ABIs don't describe `print` payloads, and the shape varies per topic within one contract. New `GET /v1/index/contracts/:id/print-schema` infers each topic's real payload schema from indexed history: sampled print events deserialized from raw Clarity, with exact Clarity types, TS types, and subgraph column types per field.

- `sl subgraphs create <name> --from-contract <id>` scaffolds a fully typed subgraph (per-topic `prints` maps to typed `event.data`; `--table-per-topic` to normalize).
- `codegen --payloads` emits the payload types as a `.d.ts`.
- Deploys now warn when a handler reads a field never observed on-chain.
- SDK: `sl.index.printSchema()`. MCP: `index_print_schema`.

### PoX-4 stacking decoded

Index now decodes PoX-4 stacking. `stack-stx`, `delegate-stx`, and reward actions are normalized into typed rows at `/v1/index/stacking`; filter by stacker, caller, or PoX function with no node and no decoders of your own.

### Keyless reads, plus pay-as-you-go credits

Index reads need no API key. Anonymous requests work with wildcard CORS, and a free-tier key reads Index too (never slower than anonymous).

- Free and anonymous reads cover the recent 24-hour window; a read into older history returns `402 UPGRADE_REQUIRED`.
- Top up prepaid credits with a card (`POST /api/billing/topup`, packs $10–$100) to read older history without a plan.
- A positive balance reads beyond the window, unthrottled, debited $5 per 1,000,000 rows across both Index and Streams from one shared balance, with the prepaid balance as a hard cap.
- Public chain data stays public.

### Trait-filtered event queries

Restrict any decoded event type or contract call to a SIP standard with
`?trait=`. `sip-010`, `sip-009`, and `sip-013` scope results to conforming
contracts without enumerating every contract id.

## Subgraphs — June 2026

### Aggregates

Roll up a subgraph table server-side instead of paging every row. The new
`/aggregate` endpoint runs `count`, `sum`, `min`, `max`, and distinct counts
over the same filters as the list endpoint.

```ts
const stats = await sl.subgraphs.sbtcFlows.transfers.aggregate({
  where: { sender: "SP3PE7Q9..." },
  sum: ["amount"],
  countDistinct: ["recipient"],
});
```

`sum`/`min`/`max` are numeric-only (enforced at compile time) and return
lossless strings; the result shape is inferred from the spec. Also available
over REST and MCP. See [aggregates](/docs/rest-api#aggregates).

### Safer schema changes on your own database

For [bring-your-own-database](/docs/subgraphs#typed-orm-schemas-codegen) subgraphs, a breaking schema change (removed table/column, changed type, or a forced reindex) is now **refused**, keeping your data intact.

- The deploy returns the exact migration plan: the `DROP SCHEMA … CASCADE` plus the rebuild DDL, so you can run it yourself and re-deploy.
- `sl subgraphs deploy` prints the plan.
- The SDK throws a typed `ByoBreakingChangeError` exposing `reasons` and `plan`.

## Subscriptions — June 2026

Subscriptions went polymorphic. A subscription is now one of two kinds: `subgraph` (fires on subgraph table rows, as before) or `chain`, a webhook on **raw chain events with no subgraph at all**. The lambda for Stacks.

### Direct chain subscriptions

Pass a `triggers` array instead of a subgraph table. A chain subscription is forward-looking: it starts at the chain tip and fires the moment a matching event lands, with no deploy and no backfill.

```ts

await sl.subscriptions.create({
  name: "amm-swaps",
  url: "https://my-app.com/webhook",
  triggers: [
    trigger.contractCall({ contractId: "SP....amm", functionName: "swap-*" }),
    trigger.ftTransfer({ trait: "sip-010", minAmount: "1000000" }),
  ],
});
```

`trigger.*` builders cover every event type (`contractCall`, `contractDeploy`, `printEvent`, and the `stx*`/`ft*`/`nft*` transfer/mint/burn variants), with `*` wildcards and a `trait` filter that scopes to a whole SIP. Over REST, `POST /api/subscriptions` accepts the same `triggers` array (1–50).

### Apply / rollback delivery

Chain deliveries carry a typed envelope: a match sends `chain.{type}.apply`
(with `block_hash`, `block_height`, `tx_id`, `canonical`, the matched `trigger`,
and the `event`), and a reorg sends `chain.reorg.rollback` (with
`fork_point_height` and the orphaned events) so you can cleanly undo. Delivery
stays at-least-once and HMAC-signed; key your state on `(tx_id, block_hash)`.

## Streams — May 2026

The raw-event availability layer matured across the board.
`@secondlayer/sdk@5.9.0`, `@secondlayer/cli@8.2.0`, `@secondlayer/shared@6.11.0`.

### Query

New payload filters on `/v1/streams/events`: `sender`, `recipient`, and
`asset_identifier` (exact-match), alongside the existing `contract_id` and
`types`. Event types that lack a field simply don't match, so the firehose
narrows naturally.

```bash
curl -H "Authorization: Bearer $SL_API_KEY" \
  "https://api.secondlayer.tools/v1/streams/events?types=ft_transfer&sender=SP...&limit=50"
```

### Finality

`/v1/streams/tip` now returns `finalized_height`, and every event carries a
`finalized` flag. Finality is anchored to Bitcoin (burn-block) confirmations
rather than a fixed count of Stacks blocks, so a `finalized` event will not reorg.

### Caching

Fully-finalized pages (a closed range with `to_height ≤ finalized_height`) are
served `Cache-Control: public, max-age=31536000, immutable` with a weak `ETag`
and `304` on `If-None-Match`. Tip-spanning requests stay `private, max-age=2`,
so historical reads are cheap while the live edge stays fresh.

### Proofs

Streams responses are signed with ed25519. Each read carries an `X-Signature`
(over the exact response body) and `X-Signature-KeyId`. Fetch the public key at
`GET /public/streams/signing-key` and verify, or set `verify` on the SDK
client; you can trust the data without trusting the server.

```ts
const streams = createStreamsClient({ apiKey, verify: true });
```

### Bulk & backfill

Download all of it. Finalized history is published as bulk dumps with an ed25519-signed manifest; the SDK verifies that signature by default (`verifyDumpsManifest`) before trusting any per-file sha256 it lists.

- SDK: `client.dumps.list()` / `download(file)` and a one-call
  `events.replay({ from: "genesis", onDumpFile, onBatch })` that backfills cold
  history from dumps, then tails live from the manifest's finalized cursor with
  no gap or duplicate at the seam.
- CLI: `sl streams pull --to ./dump` downloads finalized dumps locally
  (dumps are public, no API key).

### Reliability

Reorgs now archive orphaned rows instead of deleting them, keeping the raw log
intact and auditable. The indexer gained leader election, making it safe to run
as multiple instances.

## Notes for agents

- Ask the user which @secondlayer/sdk or cli version they run, or the date they last integrated, then report what shipped since, grouped by surface (Index, Subgraphs, Webhooks, Streams), flagging anything that affects their code.
- To adopt a capability (Index consume() consumers, chain webhooks via triggers[], subgraph /aggregate, Streams events.replay({ from: 'genesis' })), wire it in with the exact SDK or CLI calls.

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