Skip to main content
This page is written for AI coding agents (and the people who prompt them). Everything here is exact and was checked against Stellar testnet on 2026-10-23. If something you need is not on this page or in Build on Writz, it probably does not exist: do not invent it. Machine-readable entry points: docs.writz.xyz/llms.txt (index), docs.writz.xyz/llms-full.txt (all pages), and any page as Markdown by adding .md to its URL, for example docs.writz.xyz/developers/for-ai-agents.md. Prompt to give your agent:

Facts

Source of truth for IDs: contracts/deployments/testnet.md, machine-readable in contracts/deployments/testnet.json. The contracts are immutable; a new version gets a new ID there. 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. Any other bitcoin-spv or writz-lend ID is superseded; do not use it. How the storage windows work: Contract reference and Storage and TTL management.

Packages

npm install @writz/sdk and cargo add writz-spv-client both work. The surface:
  • @writz/sdk: createWritzClient() (testnet preset), writz.verifyBitcoinTx(txid), writz.relayer.getSpvProof(txid) (throws RelayerError with code, status, retryAfterSeconds; retries 409 light_client_behind, 429 and 503 after Retry-After), writz.spv.verifyTransaction(sorobanArgs) (throws ContractError with code, errorName), writz.lend.getProtocolState(), writz.lend.getSupplyRateBp(), writz.lend.getSupplyBalance(address), writz.lend.buildSupplyUsdc(address, stroops) and writz.lend.buildWithdrawSupply(address, stroops) (unsigned XDR, the SDK never signs).
  • writz-spv-client: BitcoinSpvClient, SpvVerificationResult, SPVError, find_output_value(&raw_tx, &script_pubkey) -> Option<u64>, txid(&env, &raw_tx).

Does not exist (do not generate code for it)

  • Writz publishes exactly two packages: @writz/sdk on npm and writz-spv-client on crates.io. There is no writz-sdk and no other @writz/... package.
  • verify_transaction has no headers argument and its result has no outputs field.
  • No Bitcoin mainnet, testnet3 or testnet4 verification. Signet only.
  • No Writz contract on Stellar mainnet.
  • No relayer integration route other than those listed below (the rest serve the Writz app); no API keys; no webhooks.
  • External contracts cannot call bitcoin-spv.consume_deposit (admin-registered consumers only).

bitcoin-spv interface

Rules: block_hash and every merkle_proof item in internal byte order (reverse of explorers); raw_tx without witness data, never exactly 64 bytes; min_confirmations >= 1. Inclusion only: to check amount and recipient, parse raw_tx. Store accepted txids yourself (replay protection). Errors (Error(Contract, #N)): 2 InsufficientConfirmations, 3 ZeroMinConfirmations, 5 MerkleProofInvalid, 7 EmptyTransaction, 15 AmbiguousTransactionLength, 22 HeaderNotFound, 23 NotOnBestChain, 24 BlockNotAfterCheckpoint.

writz-lend reads and the lender side

Indexes are scaled by 10^18. Lender yield is the interest Bitcoin borrowers pay: get_supply_rate_bp is variable and reads 0 while nothing, or very little, is borrowed, so read it live and never present a fixed APY. get_supply_balance includes accrued interest. withdraw_supply fails with #13 InsufficientSupply above the lender’s balance and #11 InsufficientLiquidity above what is not lent out; supply_usdc fails with #17 Paused while the pool is paused, and the supplier needs a trustline to the test USDC (without one, simulation fails inside the token contract). Generate a typed client from the deployed ID for anything else; the full list is in Contract Reference.

Relayer

/spv-proof 200 body: { txid, rawTxNoWitness, blockHash, txIndex, merkleProof[], headers[], blockHeight, confirmations, sorobanArgs: { block_hash, merkle_proof[], tx_index, raw_tx, min_confirmations } }. Hex strings. sorobanArgs is already in internal byte order and maps one to one onto verify_transaction. Errors: 400 invalid_txid or invalid_confirmations, 404 unconfirmed, 404 tx_not_found (txid not on signet), 409 insufficient_confirmations (requested, available), 409 light_client_behind (block not yet in bitcoin-spv; calling the contract now fails with #22 or #2), 429 rate_limited (60 per minute per IP), 502 upstream_error, 503 upstream_unavailable. On 409, 429 and 503, wait retryAfterSeconds (body) or Retry-After (header) before retrying; never retry in a tight loop. CORS is open. Signet coins for an integration come from a public Signet faucet (for example signetfaucet.com or bitcoinsignetfaucet.com); claim them by hand and never automate around a faucet’s captcha. The relayer’s /faucet/signet route is not for agents or builders: it only funds accounts created through the Writz testnet app’s social login, needs that login’s session token, and refuses every other request. /timeline lists one event per transaction of a writz-lend position, in chain order: btc_lock and released on bitcoin, verified, borrowed, received and repaid on stellar, and a received mint on base-sepolia. at is Unix ms or null; url is the mempool.space, stellar.expert or Basescan page. A received event (a swap to a stablecoin or XLM, or a CCTP burn) is linked to the loan by a rule, not by the chain: it belongs to the owner’s most recent borrow before it, with no borrow or repay by the owner in between. complete: false with gaps means a source could not be read in full; the events listed are still real. Errors: 400 invalid_txid, 404 position_not_found (use /deposit-status before the deposit is verified), 429 rate_limited (60 per minute per IP), 503 stellar_unavailable or lending_not_configured. /cctp/relay completes a Stellar to Base Sepolia CCTP V2 transfer so the recipient needs no wallet or gas on Base Sepolia: call POST once with the Stellar hash of the deposit_for_burn, then poll GET /cctp/relay/:burnHash every few seconds until kind is arrived (mintHash is the Base Sepolia transaction, null only while it is still being looked up) or failed (reason). Transient failures (an RPC outage, an empty gas account, the daily cap, Circle slow to confirm) are retried by the relay itself: the status then stays confirming or confirmed with retrying: true, reason and nextRetryAt; failed is final (a refused or invalid message, or 24 hours of retrying). Asking again never mints twice. Only burns from Stellar (domain 27) to Base Sepolia (domain 6) through Circle’s TokenMessengerV2, with no destination caller and no hook, are relayed. Errors: 400 invalid_burn_hash, 404 not_found (on GET, before any POST), 429 rate_limited (30 per IP per 10 minutes), 503 cctp_relay_disabled or relay_busy. The other routes the index lists (/merkle-path, /notes, /insert-commitment, /update-leaf, /metrics/*) serve the Writz app and are not an integration surface. Yield on USDC is the writz-lend lender side, called on the contract directly (above), not through the relayer.

Verified examples

CLI (simulation; the source account is any valid G address and is not charged):
TypeScript (@stellar/stellar-sdk 16.x, read-only):
Runnable: examples/verify-bitcoin-tx. Rust, cross-contract (soroban-sdk 26):
Complete contract with tests, deployed and run against testnet: examples/spv-payment-check.

Checklist before you answer “it works”

  1. get_best_tip returns a height near the signet tip on mempool.space.
  2. The txid is a signet transaction with enough confirmations, in a block above 327,925.
  3. You passed sorobanArgs through unchanged (no extra byte reversal).
  4. You simulated reads (--send=no, simulateTransaction) and only signed real writes.
  5. Your contract checks the output (amount and scriptPubKey) and stores the txid.