> ## Documentation Index
> Fetch the complete documentation index at: https://docs.writz.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# For AI Agents

> Compact, exact integration facts for AI coding agents: Writz testnet contract IDs, network passphrase, RPC URL, function signatures, the relayer proof route, verified examples, and what does not exist.

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](https://docs.writz.xyz/developers/integrate.md), it probably does not exist: do not invent it.

Machine-readable entry points: [docs.writz.xyz/llms.txt](https://docs.writz.xyz/llms.txt) (index), [docs.writz.xyz/llms-full.txt](https://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](https://docs.writz.xyz/developers/for-ai-agents.md).

**Prompt to give your agent:**

```text theme={null}
Integrate Writz on Stellar testnet. Read https://docs.writz.xyz/developers/for-ai-agents.md
first and use only the contract IDs, signatures and endpoints it lists. Use @writz/sdk
from npm for TypeScript and writz-spv-client from crates.io for Soroban contracts. The Bitcoin network is signet. Simulate reads, never sign them.
```

***

## Facts

```text theme={null}
STELLAR_NETWORK        testnet
NETWORK_PASSPHRASE     Test SDF Network ; September 2015
SOROBAN_RPC_URL        https://soroban-testnet.stellar.org
BITCOIN_NETWORK        signet
RELAYER_URL            https://api.testnet.writz.xyz
BITCOIN_SPV_ID         CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO
WRITZ_LEND_ID          CABF7VK7F25ZGDLV6EWSAU3NYQYKDQNDSJIISDMDZAUASMSXUADSHZGG
TEST_USDC_SAC          CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA   (USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5, 7 decimals)
SPV_CHECKPOINT_HEIGHT  327925   (signet; only later blocks can be proven)
SOROBAN_SDK            soroban-sdk = "26"
```

Source of truth for IDs: [`contracts/deployments/testnet.md`](https://github.com/WritzProtocol/writz/blob/main/contracts/deployments/testnet.md), machine-readable in [`contracts/deployments/testnet.json`](https://github.com/WritzProtocol/writz/blob/main/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](https://docs.writz.xyz/developers/contract-reference.md) and [Storage and TTL management](https://docs.writz.xyz/how-it-works/stellar-side.md#storage-and-ttl-management).

## Packages

```text theme={null}
NPM_PACKAGE     @writz/sdk         source packages/sdk                         PUBLISHED, npm install @writz/sdk
CRATE           writz-spv-client   source contracts/contracts/writz-spv-client PUBLISHED, cargo add writz-spv-client
```

`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

```rust theme={null}
// Main primitive. Read-only, no auth.
fn verify_transaction(block_hash: BytesN<32>, merkle_proof: Vec<BytesN<32>>, tx_index: u32,
                      raw_tx: Bytes, min_confirmations: u32) -> Result<SpvVerificationResult, SPVError>;

fn get_best_tip() -> Option<BestTip>;                       // { hash: BytesN<32>, height: u32, chainwork: U256 }
fn get_header(block_hash: BytesN<32>) -> Option<HeaderEntry>;
    // { prev: BytesN<32>, merkle_root: BytesN<32>, height: u32, bits: u32, time: u32, period_start_time: u32, chainwork: U256 }
fn get_canonical_hash(height: u32) -> Option<BytesN<32>>;
fn get_checkpoint() -> Option<Checkpoint>;
fn is_deposit_consumed(txid: BytesN<32>) -> bool;

pub struct SpvVerificationResult {
    pub txid: BytesN<32>,       // SHA256d(raw_tx), INTERNAL byte order
    pub block_hash: BytesN<32>, // INTERNAL byte order
    pub block_height: u32,
    pub confirmations: u32,     // depth below the light client's tip, tip = 1
}
```

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

```rust theme={null}
fn get_protocol_state() -> ProtocolState;
    // { total_scaled_supply: i128, total_borrowed: i128, reserve_usdc: i128, supply_index: i128,
    //   borrow_index: i128, index_ledger: u32, last_keeper_heartbeat: u64 }
fn get_borrow_rate_bp() -> i128;                         // annual, basis points
fn get_supply_rate_bp() -> i128;                         // annual, basis points
fn get_supply_balance(lender: Address) -> i128;          // USDC stroops (7 decimals)
fn get_position(txid: BytesN<32>) -> Option<Position>;   // txid of the BTC deposit, internal order
fn get_health_ratio_bp(txid: BytesN<32>) -> Result<i128, WritzLendError>; // 15000 = 150%, liquidation at 12000
```

```rust theme={null}
// Lender side: permissionless writes, the supplier signs. Amounts in TEST_USDC_SAC stroops.
fn supply_usdc(supplier: Address, amount: i128) -> Result<(), WritzLendError>;
fn withdraw_supply(supplier: Address, amount: i128) -> Result<(), WritzLendError>;
```

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](https://docs.writz.xyz/developers/contract-reference.md).

## Relayer

```text theme={null}
GET  /                                  service index (JSON)
GET  /health                            status
GET  /spv-proof/:txid?confirmations=N   proof for a confirmed signet tx; txid in DISPLAY order; N in 1..20, default 6
GET  /deposit-status/:txid              writz-lend deposit progress: stage (unseen|mempool|confirming|ready|verified), confirmations, required, stellarTxHash
     ?stellarTx=<hash>                  the app's deposit submission: adds stages submitted|failed and `submission` {status, reason}; 400 stellar_tx_mismatch if it is not this deposit
GET  /timeline/:btcTxid                 full loan cycle: { btcTxid, positionId, owner, status, complete, gaps[], events[] }, each event { kind, chain, hash, at, detail?, url }
POST /cctp/relay  {"burnHash":"<64 hex>"}   mints on Base Sepolia a USDC burn sent from Stellar with CCTP V2; returns { kind, ... }
GET  /cctp/relay/:burnHash              transfer status: kind (sent|confirming|confirmed|arrived|failed), mintHash, explorerUrl, reason, retrying, nextRetryAt
```

`/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](https://signetfaucet.com) or [bitcoinsignetfaucet.com](https://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):

```bash theme={null}
SPV=CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO
READER=GB2BSYQS3FRJ5LZSSIDF3ZCSG5MKWJT5SZ3OZO4QRCAMCR357YAVPTWT
TXID=eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269
curl -s "https://api.testnet.writz.xyz/spv-proof/$TXID" > proof.json
stellar contract invoke --id $SPV --network testnet --source-account $READER --send=no -- \
  verify_transaction \
  --block_hash "$(jq -r .sorobanArgs.block_hash proof.json)" \
  --merkle_proof "$(jq -c .sorobanArgs.merkle_proof proof.json)" \
  --tx_index "$(jq -r .sorobanArgs.tx_index proof.json)" \
  --raw_tx "$(jq -r .sorobanArgs.raw_tx proof.json)" \
  --min_confirmations 6
# {"block_hash":"fe324a44...02000000","block_height":327928,"confirmations":7,"txid":"69e20795...5cf9c0ea"}
```

**TypeScript** (`@stellar/stellar-sdk` 16.x, read-only):

```ts theme={null}
import { Account, BASE_FEE, Contract, Keypair, TransactionBuilder, nativeToScVal, rpc, scValToNative, xdr } from "@stellar/stellar-sdk";

const server = new rpc.Server("https://soroban-testnet.stellar.org");
const spv = new Contract("CBX23YKCCJMAMBMSCNP6YVKEXUIYK2CTKSR4TQ3RVBPAFYZFVCW7W4EO");
const hex = (s: string) => Buffer.from(s, "hex");

const txid = "eac0f95c531b4bd5307597f74bedd7379928539056b9a1467ea6d1159507e269";
const a = (await (await fetch(`https://api.testnet.writz.xyz/spv-proof/${txid}`)).json()).sorobanArgs;

const tx = new TransactionBuilder(new Account(Keypair.random().publicKey(), "0"), {
  fee: BASE_FEE,
  networkPassphrase: "Test SDF Network ; September 2015",
})
  .addOperation(spv.call("verify_transaction",
    nativeToScVal(hex(a.block_hash)),
    xdr.ScVal.scvVec(a.merkle_proof.map((h: string) => nativeToScVal(hex(h)))),
    nativeToScVal(a.tx_index, { type: "u32" }),
    nativeToScVal(hex(a.raw_tx)),
    nativeToScVal(6, { type: "u32" })))
  .setTimeout(30)
  .build();

const sim = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(sim)) throw new Error(sim.error); // "...Error(Contract, #N)..."
console.log(scValToNative(sim.result!.retval)); // { txid, block_hash, block_height, confirmations }
```

Runnable: [`examples/verify-bitcoin-tx`](https://github.com/WritzProtocol/writz/tree/main/examples/verify-bitcoin-tx).

**Rust, cross-contract** (soroban-sdk 26):

```rust theme={null}
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SpvVerificationResult { pub txid: BytesN<32>, pub block_hash: BytesN<32>, pub block_height: u32, pub confirmations: u32 }

#[contractclient(name = "BitcoinSpvClient")]
pub trait BitcoinSpv {
    fn verify_transaction(env: Env, block_hash: BytesN<32>, merkle_proof: Vec<BytesN<32>>,
                          tx_index: u32, raw_tx: Bytes, min_confirmations: u32) -> SpvVerificationResult;
}

let proof = match BitcoinSpvClient::new(&env, &spv_id)
    .try_verify_transaction(&block_hash, &merkle_proof, &tx_index, &raw_tx, &6)
{ Ok(Ok(r)) => r, _ => return Err(Error::ProofRejected) };

// Output check, with spv-types as a git dependency:
// spv-types = { git = "https://github.com/WritzProtocol/writz", rev = "0a1065991ee0498ad2c54367e0ffa69ca4a59a9a" }
let sats: Option<u64> = spv_types::btc_parser::find_p2wsh_output(&raw_tx, &expected_script_pubkey); // any script type
```

Complete contract with tests, deployed and run against testnet: [`examples/spv-payment-check`](https://github.com/WritzProtocol/writz/tree/main/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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.