Verification
Trustless transaction-inclusion proofs let you verify — without trusting Secondlayer — that a transaction is included in a Stacks (Nakamoto) block, and that the reward cycle's signers attested to that block. Fetch a proof from the API, then recompute everything client-side: nothing Secondlayer returns is taken on faith.
A proof verifies at one of two levels.
- Anchored — recompute the txid from
raw_tx, fold it up thetx_merkle_pathto the header'stx_merkle_root, and recompute the header'sblock_hashandindex_block_hashfromraw_header. This proves the transaction is included in a block header that any Stacks node can independently corroborate. - Consensus — additionally recover the signer signatures from the header and confirm that ≥70% of the reward cycle's signer weight signed the block. This is fully trustless when you pass a reward set you resolved yourself (see Fully trustless); otherwise it falls back to the reward set embedded in the proof, which still trusts Secondlayer for that set alone.
curl "https://api.secondlayer.tools/v1/index/transactions/0x<tx_id>/proof"Like the other read endpoints, this is open during beta — no key required.
A 200 returns the raw transaction, the raw header, the merkle path linking them, and (when resolvable) the reward set for consensus verification:
{
"txid": "<hex>",
"index_block_hash": "<hex>",
"block_height": 8199502,
"tx_index": 0,
"raw_tx": "<hex>",
"raw_header": "<hex>",
"tx_merkle_path": [{ "position": "left", "hash": "<hex>" }],
"consensus": {
"reward_cycle": 136,
"reward_set": {
"signers": [{ "signing_key": "<hex>", "weight": 51 }],
"total_weight": 3862
}
}
}consensus is present only when the reward set could be resolved. Without it, the proof is anchored-only — still independently verifiable as included in a block header, but without the signer-weight attestation.
Errors
| Status / code | Meaning |
|---|---|
404 PROOF_UNAVAILABLE | The transaction, or the block that contains it, was not found. |
503 PROOF_TX_SET_INCOMPLETE | The server could not reproduce the block's tx_merkle_root from its stored transaction set, so it refuses to emit a proof that wouldn't verify. |
503 PROOF_NODE_UNAVAILABLE | The signed-header source (a stacks-node) could not be reached. The proof needs the raw Nakamoto header, so the server returns a clean 503 rather than an unverifiable proof. Retryable. |
Fail-safe by design
PROOF_TX_SET_INCOMPLETE is fail-safe: you never receive a proof that would fail client-side recomputation.
verifyTransactionProof recomputes the txid, the merkle root, the block hash, and the signer weight client-side — nothing in the proof is trusted until it checks out.
import { verifyTransactionProof } from "@secondlayer/sdk";
const proof = await fetch(
`https://api.secondlayer.tools/v1/index/transactions/${txid}/proof`,
).then((r) => r.json());
const result = verifyTransactionProof(proof);
// result.ok === true, result.level === "consensus"
// result.signerWeightBps ~ 7000+, result.rewardSetSource === "embedded"The result describes exactly what was checked:
type TransactionProofVerifyResult = {
level: "anchored" | "consensus";
txidMatches: boolean;
includedInHeader: boolean;
headerSelfConsistent: boolean;
signerWeightBps?: number;
thresholdMet?: boolean;
rewardSetSource?: "provided" | "embedded";
ok: boolean;
errors: string[];
};When you pass no reward set, consensus verification uses the one embedded in the proof (rewardSetSource: "embedded"), which inherits trust in Secondlayer for the signer set only. To remove that last dependency, resolve the set yourself.
fetchRewardSet resolves the reward set from your own stacks-node (/v3/stacker_set/{cycle}). Pass it to verifyTransactionProof and the proof is verified end-to-end against data you control.
import { verifyTransactionProof, fetchRewardSet } from "@secondlayer/sdk";
const rewardSet = await fetchRewardSet({
nodeUrl: "https://your-stacks-node:20443",
cycle: proof.consensus.reward_cycle,
});
const trustless = verifyTransactionProof(proof, { rewardSet });
// trustless.rewardSetSource === "provided"When rewardSetSource is "provided", the entire chain of checks — inclusion and signer attestation — rests only on the raw bytes of the proof and the reward set from your own node.
Verification uses Node's crypto (via
@secondlayer/shared), so it is intended for Node / server-side use — the same as the Streams signature verification.
See the SDK reference for verifyTransactionProof and fetchRewardSet, and the REST API reference for the proof endpoint.