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:
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:
bitcoin-spv and spv-types source.
Calling it from a Soroban contract
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:
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 examplehttps://mempool.space/signet/api):
GET /tx/:txidforstatus.block_height, thenGET /block-height/:heightfor the block hash.GET /tx/:txid/merkle-proofforpos(thetx_index) andmerkle(the siblings).GET /tx/:txid/raw, then strip the witness to get the legacy serialization.- Reverse the block hash and each sibling to internal byte order.
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_transactionwill 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
outputsfield or address matching). The result has only the four fields above; use the parser inspv-types. - A
headersargument toverify_transaction. Headers are submitted separately withsubmit_headers, and the call takes a storedblock_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.