.md

Handle Stacks reorgs in your indexer

A reorg orphans blocks your tables already hold. Delete your rows from the fork height, inclusive, then read forward again. One key, one call to see them:

$curl "$SECONDLAYER_API_URL/v1/streams/reorgs?since=2026-08-01T00:00:00Z&limit=1" \
-H "Authorization: Bearer sk-sl_…"
200response.jsonjson
{
"reorgs": [
{
"id": "9dba566b-8325-45df-bc17-aed6be93ff70",
"detected_at": "2026-08-01T18:30:11.356245Z",
"fork_point_height": 8683934,
"old_index_block_hash": "0x500e56fb65237ebec60a6da210135d9e96c9f62e0ff5a07cc2bbb72e34e5f849",
"new_index_block_hash": "0x2569032e50f5cc1f783701d992691ab5ab97d56ff4e9a5d58839d5643cc2205a",
"orphaned_range": { "from": "8683934:0", "to": "8683935:14" },
"new_canonical_tip": "8683934:0"
}
],
"next_since": "2026-08-01T18:30:11.356245Z~9dba566b-8325-45df-bc17-aed6be93ff70"
}

Keep rows from the orphaned range and your tables describe a chain that no longer exists.

How you read decides how much rollback code you write.

You read withWhat happens on a reorgYou do
A consumerIt rewinds and calls onReorgDelete from the fork height
A sinkIt deletes the rows and rereadsNothing
A subgraphRows are rewritten under new idsWait a few blocks before moving money
A webhookchain.reorg.rollback lists what may have arrivedUndo each entry
  • Delivery after a rewind is at least once, so keep writes idempotent.
  • The fork block itself is re-delivered, which is why the delete is inclusive.

Rows at or below tip.finalized_height never reorg, so a consumer that reads only those has nothing to undo. The tradeoff is freshness: finalizedOnly holds delivery until each row is final, which can put you hundreds of blocks behind the tip.

Use it for accounting and reports. Keep the rollback path for anything a user watches live. The option is on the Index consumer.

Edit on GitHub