What you can build on today
Ideas that fit in a weekend: a Soroban contract that releases something (an NFT, access, an escrowed token) once a Bitcoin payment confirms; a Bitcoin payment checker for a store or invoice app; a dashboard of the Writz pool; a bot that watches a Bitcoin address and proves its payments on Stellar.
Not available yet: Bitcoin mainnet or testnet3/testnet4 verification on Stellar (the testnet light client tracks signet only), and any Writz contract on Stellar mainnet. See Bitcoin SPV SDK.
Packages
Both are published:
npm install @writz/sdk and cargo add writz-spv-client. The snippets on this page also work without them, with only @stellar/stellar-sdk or soroban-sdk. The package code and its tests are in the repository either way.
Network and contract IDs
The source of truth for every ID, with deploy transactions and wasm hashes, is
contracts/deployments/testnet.md. The same values, machine-readable, are in contracts/deployments/testnet.json and in the @writz/sdk network preset, both generated from that file. Writz contracts are never upgraded in place: a new version gets a new ID, recorded there.
The bitcoin-spv checkpoint is signet block 327,925. Only blocks after it can be proven.
Testnet bitcoin-spv and writz-lend were redeployed on 2026-10-25 because testnet rent rose sharply: testnet’s live state is above its target size, and the previous contracts gave every entry they created the long mainnet storage windows, so a deposit was charged about 428 XLM in fees. The new contracts take those windows as deployment parameters and run testnet-length ones (a deposit was charged about 89 XLM, measured 2026-10-25). Positions on the previous contracts were closed and their BTC released; their IDs are recorded as superseded in contracts/deployments/testnet.md. A transaction in a block at or below the new checkpoint can no longer be proven; pick one above it. How the storage windows work: Contract reference and Storage and TTL management.
1. Check the light client (1 minute)
Install the Stellar CLI (brew install stellar-cli). Read calls are simulated, never submitted, so the source account only has to be a valid address; it is not charged and does not sign.
height should be at or near the signet tip on mempool.space/signet. The Writz relayer submits new signet headers to the contract as they are mined.
2. Get a proof from the relayer
Pick any confirmed signet transaction (here one in block 327,928) and ask the relayer for its proof:txid, rawTxNoWitness, blockHash, txIndex, merkleProof, headers, blockHeight, confirmations and sorobanArgs. Use sorobanArgs: its five fields are exactly the five arguments of verify_transaction, as hex strings and integers, already in the byte order the contract expects. confirmations (query parameter, 1 to 20, default 6) is the minimum you ask for, not the block’s actual depth.
The relayer builds proofs from public signet data (mempool.space). It holds no keys for you and you do not have to trust it: a wrong proof simply fails on chain.
3. Verify it on Stellar
confirmations is the block’s real depth below the light client’s tip. Note that txid comes back reversed compared to the TXID you passed in: see Byte order.
4. From TypeScript
Runnable example:examples/verify-bitcoin-tx (Bun and @stellar/stellar-sdk, read-only, no keys).
@writz/sdk (npm install @writz/sdk) the whole flow is three lines, and relayer errors arrive typed, with the Retry-After wait already honoured:
writz-lend (--contract-id CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG). Generate bindings from the deployed contract ID, not from the source on main: the source can be ahead of what is deployed.
5. From your Soroban contract
Your contract callsbitcoin-spv with a cross-contract client. The crate writz-spv-client contains the client, the types and the output parser: add writz-spv-client = "0.1" to Cargo.toml and use writz_spv_client::{find_output_value, BitcoinSpvClient};. Without the crate, declare the result type yourself with these exact field names (they are decoded by name):
verify_transaction proves inclusion and depth only. It does not tell you what the transaction paid or to whom. To check that, parse raw_tx in your contract. The shared Writz crate spv-types has an on-chain parser you can use as a git dependency:
verify_transaction is a pure check and will verify the same transaction again for anyone, so replay protection is yours.
The complete contract, with tests, is examples/spv-payment-check: it accepts a Bitcoin payment of at least N sats to a given scriptPubKey, once per txid. It was deployed to testnet and run against the real bitcoin-spv with the proof above; its README has the commands.
6. Read the Writz lending pool
10000000000 is 1,000 USDC). Rates are annual basis points (800 is 8%). Indexes are scaled by 10^18. get_position and get_health_ratio_bp take a deposit txid in internal byte order; get_health_ratio_bp returns basis points (15000 is 150%, liquidation at 12000). Full interface: Contract Reference.
supply_usdc and withdraw_supply are permissionless writes signed by the supplier; section 7 covers them. Borrowing needs a real signet deposit to the Writz script, which is what the testnet app does for you.
7. Earn yield on USDC as a lender
Yield on Writz is the lender side ofwritz-lend. Lenders supply USDC to the pool that Bitcoin borrowers draw from, and the interest those borrowers pay is the lenders’ yield. You need no contract of your own: the user’s wallet signs supply_usdc and withdraw_supply, and your app reads the balance and the rate by simulation. The asset is the same test USDC as the rest of this page (USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5), and the user’s account needs a trustline to it.
100000000 is 10 USDC. get_supply_balance includes the interest earned so far. withdraw_supply fails with #13 (InsufficientSupply) above the lender’s balance and with #11 (InsufficientLiquidity) above the USDC that is not lent out at that moment.
The supply rate is variable and is zero while nothing is borrowed; with very little borrowed it still reads 0, because it is counted in whole basis points (500 is 5% a year). Read it live and label it as variable. Never show a fixed APY.
With @writz/sdk these are lend.getSupplyRateBp, lend.getSupplyBalance, lend.buildSupplyUsdc and lend.buildWithdrawSupply; the builders return unsigned transactions for the wallet and never sign. A browser flow with @stellar/stellar-sdk and product ideas built on it are in What to build on Writz.
Byte order
Bitcoin explorers show txids and block hashes reversed (display order).bitcoin-spv works in internal order, the raw SHA256d output.
- The relayer route takes the txid in display order (as on mempool.space) and returns
sorobanArgsalready in internal order. Pass them through unchanged. verify_transactionreturnstxidandblock_hashin internal order. To compare with an explorer, reverse the bytes:69e20795...5cf9c0eaiseac0f95c...9507e269.- If you build proofs yourself from an Esplora API, reverse the block hash and every Merkle sibling before calling the contract.
Common errors
From the relayer (GET /spv-proof/:txid):
Every 409, 429 and 503 carries
retryAfterSeconds in the body and a Retry-After header. Honour it instead of retrying in a loop. Proofs for deeply confirmed transactions are cached, so asking again for the same txid is cheap.
From bitcoin-spv (simulation fails with Error(Contract, #N)):
Other frequent mistakes:
- Witness data in
raw_tx. The contract hashesraw_txto get the txid, so it must be the legacy (non-witness) serialization. The relayer’sraw_txalready is. - Sending instead of simulating. Reads need
--send=noon the CLI (orsimulateTransactionin the SDK); there is nothing to sign. - Generating bindings from source. Use the deployed contract ID so the client matches what is on chain.
Faucets and wallets
- Stellar testnet XLM:
stellar keys generate me --network testnet --fund, or Friendbot in Stellar Lab or in the Freighter wallet on testnet. - Test USDC: add a trustline to
USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5, then swap testnet XLM for it in Freighter, or use faucet.circle.com (Stellar Testnet). This is the USDC the Writz pool uses. - Bitcoin signet coins: a signet wallet (Xverse and Sparrow support signet) and its
tb1q...ortb1p...address, then a public Signet faucet such as signetfaucet.com, bitcoinsignetfaucet.com or signet.dcorral.com. Their amounts, captchas and limits are set by their operators. The relayer’s/faucet/signetroute is not a builder faucet: it only funds accounts created through the Writz testnet app’s social login, once each, and refuses any other request. Coins from a faucet are unconfirmed at first; wait for a block (about 10 minutes) before building a proof. - Explorers: mempool.space/signet for Bitcoin, stellar.expert/explorer/testnet for Stellar.
Limits to know
- Testnet and signet only. Nothing here is on mainnet, and the contracts are not audited. Do not use real funds.
- Signet trust. Signet blocks are authorised by a signature the contract does not check, so on signet only one designated Writz account may submit headers. Proof-of-work and difficulty are still checked per header.
- Shared relayer.
api.testnet.writz.xyzis one instance with no uptime guarantee. If it is down, build the proof yourself from any signet Esplora API (seerelayer/src/bitcoin/spv.tsfor the exact steps) and call the contract directly.
Next: Bitcoin SPV SDK for the full
verify_transaction reference, or For AI Agents to hand this to your coding agent.