sBTC settlement
Accepted isn't received. A withdrawal-accept event only means the signers committed to a Bitcoin sweep; your instance verifies that sweep_txid against your bitcoind (the bundled full-node profile or an external BITCOIN_RPC) and surfaces the result on the decoded sBTC feed.
Deposits need no such check: completed-deposit fires only after the signers see the BTC confirmations.
Every peg-out lifecycle carries a settlement object. GET /v1/index/sbtc/withdrawals/:request_id:
{
"request_id": 42,
"status": "ACCEPTED",
"settlement": {
"sweep_txid": "abcd…",
"btc_confirmations": 7,
"settlement_confirmed": true,
"btc_block_height": 880321,
"confirmed_at": "2026-06-01T00:00:00.000Z"
}
}settlement_confirmed is true past the confirmation threshold, false while the sweep is still shallow, null when there's no settlement record yet (no sweep observed, not a denial).
GET /v1/index/sbtc/withdrawals carries settlement_confirmed per row and as a filter:
# only peg-outs whose BTC sweep is confirmed
curl "http://127.0.0.1:3800/v1/index/sbtc/withdrawals?settlement_confirmed=true"
# the complete "not done" bucket — not yet confirmed, or no sweep yet
curl "http://127.0.0.1:3800/v1/index/sbtc/withdrawals?settlement_confirmed=false"sbtc.events.consume() is the checkpointed sweep over the raw peg log: all six topics, or one:
await sl.index.sbtc.events.consume({
topic: "withdrawal-accept", // server-side; omit for every topic
fromCursor: await db.loadCheckpoint(),
fromHeight: 0,
onBatch: async (events, _envelope, ctx) => {
for (const e of events) await db.upsertPegEvent(e); // typed columns, no payload parsing
await db.saveCheckpoint(ctx.cursor);
return ctx.cursor;
},
onReorg: async (r) => db.deleteFromHeight(r.fork_point_height),
});sbtc.deposits.consume() is the same loop over completed deposits alone.
Withdrawals have no consume
A withdrawal row is a lifecycle aggregate keyed by request_id, and it mutates as the peg-out moves REQUESTED → ACCEPTED → settled. A checkpoint only moves forward, so a consumer would commit a row as REQUESTED and never see it change. Mirror the append-only events above and derive status from the withdrawal-* topics; read withdrawals.get(id) when you want the assembled lifecycle.
Subscribe to sbtc_withdrawal_swept_confirmed and skip the polling.
import { trigger } from "@secondlayer/sdk";
await client.subscriptions.create({
url: "https://your-app.com/webhooks/sbtc-settlement",
triggers: [trigger.sbtcWithdrawalSweptConfirmed()],
});- Scope with
requestId/sweepTxid. - Fires once per sweep, when
btc_confirmationsfirst crosses the threshold (default 6). - Forward-only: settlements confirmed after you subscribe, never a backfill.
- A Bitcoin reorg that un-confirms a sweep never re-fires it; the read self-corrects.
Delivery is the standard chain.{type}.apply envelope; event carries the settlement specifics:
{
"action": "apply",
"trigger": "sbtc_withdrawal_swept_confirmed",
"event": {
"topic": "withdrawal-swept-confirmed",
"request_id": 42,
"sweep_txid": "abcd…",
"btc_confirmations": 6,
"btc_block_height": 880321,
"confirmed_at": "2026-06-01T00:00:00.000Z",
"amount": "100000",
"sender": "SP…"
}
}See Subscriptions for delivery formats, signature verification, and the full trigger list.
What this proves, and what it doesn't
Settlement is verified against your instance's bitcoind, not a custody guarantee: a signer set still controls the pegged UTXO.