Skip to main content
Verify Bitcoin transactions from any Stellar smart contract. bitcoin-spv is a Bitcoin light client that runs as a Soroban contract. It stores Bitcoin block headers that descend from a fixed checkpoint, checks each header’s proof-of-work and difficulty, and follows the most-work chain. Any Soroban contract can then ask it whether a Bitcoin transaction is in a block of that chain, with a given number of confirmations. Writz Lend uses it to verify BTC deposits; you can use it for anything that should react to a Bitcoin transaction. The SDK is the deployed contract, the relayer proof route, and two packages whose first release is pending: @writz/sdk for TypeScript and writz-spv-client for Soroban contracts (see Packages). Until they are published, the snippets below need only soroban-sdk or @stellar/stellar-sdk. For a step-by-step walkthrough with commands that were run against testnet, start with Build on Writz.

Deployment

Source: contracts/contracts/bitcoin-spv/src/lib.rs. Deployment record: contracts/deployments/testnet.md. This contract was redeployed on 2026-10-25, with a new checkpoint, because testnet rent rose sharply; the storage windows of headers and consumed deposits are now deployment parameters (header_ttl_ledgers, consumed_ttl_ledgers, see the Contract reference). Blocks at or below the new checkpoint can no longer be proven against it.

verify_transaction

It is a read: it changes no state, needs no authorisation, and can be simulated for free.

Return type

SpvVerificationResult, defined in the shared crate spv-types:
That is the complete result. It says nothing about the transaction’s outputs: if you need to know what the transaction paid and to whom, parse raw_tx yourself (see Checking outputs).

Errors

SPVError codes are stable. The ones a caller of verify_transaction can hit: The full list, including the codes of submit_headers and the admin functions, is in error.rs.

Other reads

consume_deposit and set_consumer are for the Writz lending contracts only; registering a consumer is admin-gated. Keep your own record of the txids you accept.

Packages

With writz-spv-client the contract below shrinks to the call itself:
Its tests check every type, error code and the parser against the bitcoin-spv and spv-types source.

Calling it from a Soroban contract

Without writz-spv-client, declare the struct locally as above; the call decodes the result by field name, so the names and types must match exactly. A complete, tested contract built this way is examples/spv-payment-check.

Checking outputs

spv-types includes the parser Writz Lend uses on chain. Add it as a git dependency pinned to a commit:
Pass the scriptPubKey as bytes (for example 0014... for P2WPKH; mempool.space shows it as scriptpubkey for each output).

Getting the proof: the relayer

txid is in display byte order (as explorers show it). confirmations is optional, 1 to 20, default 6. Response (200), shortened:
sorobanArgs maps one to one onto the parameters of verify_transaction, already in internal byte order. confirmations and min_confirmations echo what you asked for; the block’s real depth is what the contract returns. headers are the raw 80-byte headers from the block onwards, one per requested confirmation, for callers that want to inspect them; verify_transaction does not take them. Error responses: 400 invalid_txid, 400 invalid_confirmations, 404 unconfirmed, 404 tx_not_found (a txid that is not on signet), 409 insufficient_confirmations (with requested and available), 409 light_client_behind (the block is not yet in bitcoin-spv), 429 rate_limited, 502 upstream_error, 503 upstream_unavailable. The 409, 429 and 503 responses carry retryAfterSeconds and a Retry-After header.

Building the proof yourself

The relayer only reads public Bitcoin data, so you can do the same from any Esplora API (for example https://mempool.space/signet/api):
  1. GET /tx/:txid for status.block_height, then GET /block-height/:height for the block hash.
  2. GET /tx/:txid/merkle-proof for pos (the tx_index) and merkle (the siblings).
  3. GET /tx/:txid/raw, then strip the witness to get the legacy serialization.
  4. Reverse the block hash and each sibling to internal byte order.
The reference implementation is buildSPVProof in relayer/src/bitcoin/spv.ts.

Use cases

  • Bitcoin payment checks. Release a token, an NFT or access on Stellar once a Bitcoin payment confirms, without a bridge or a custodian.
  • Bitcoin-collateralised lending. What Writz Lend does: prove a deposit to a time-locked script before lending against it.
  • Escrow. Unlock funds on Stellar when the counterparty’s Bitcoin payment is buried under N blocks.
  • Bitcoin-gated actions. Votes, mints or roles for anyone who proves a specific Bitcoin payment.

Security notes for integrators

  • Check the outputs. Inclusion proves the transaction exists, not that it paid you. Parse raw_tx.
  • Replay protection. verify_transaction will verify the same transaction for anyone, any number of times. Store the txids you accept.
  • Choose confirmations deliberately. 6 is the usual choice and what Writz Lend uses.
  • Testnet only. The deployed light client tracks signet, with header submission restricted to Writz, and none of the contracts are audited.

Not yet available

These appeared in earlier versions of this page and do not exist today:
  • Output extraction in the result (an outputs field or address matching). The result has only the four fields above; use the parser in spv-types.
  • A headers argument to verify_transaction. Headers are submitted separately with submit_headers, and the call takes a stored block_hash.
  • A light client tracking Bitcoin mainnet on Stellar, and any per-call fee. Neither exists.

Next: Build on Writz for a runnable walkthrough, or Contract Reference for every function of every contract.