From Simulation to Settlement: Live Testnet BSV for AI Agents
The previous post laid out the vision: sovereign AI agents using Clojure + BSV as two primitives that collapse six layers of vendor dependencies into zero. That post ended at “steps 4 and 5 are the frontier — the glue layer that has not been built yet.”
Today, that frontier gets paved. This post walks through the live testnet implementation of the BSV Agent Coordination Layer — a Clojure monorepo that turns simulated stubs into real BSV testnet transactions. 100 tests, 242 assertions, zero failures.
The Gap: What Was Simulated
The original codebase had working coordination logic — identity protocols, atomic wallet reserves, policy engines, escrow workflows — but every interaction with the ledger was mocked:
| Layer | Simulated | Live Replacement |
|---|---|---|
| Address derivation | Hardcoded "mxx..." string |
secp256k1 pubkey -> RIPEMD160(SHA256) -> Base58Check |
| Transaction spending | (str "tx-" (random-nonce)) |
UTXO selection, P2PKH build, DER signing, broadcast |
| OP_RETURN anchoring | Local SHA-256 hash only | Real OP_RETURN tx with change output |
| Balance | In-memory atom | Bitails API (get-balance) |
| UTXOs | Manually set | Bitails API (get-unspent) |
| Faucet | No-op | WhatsonChain API + poll-for-confirmation |
| Network calls | No retry | Exponential backoff (3 retries) |
The architecture required six modules with acyclic dependencies:
The goal: replace the dotted-line simulations with solid, broadcast-ready transactions without changing a single coordination-logic test.
Step 1: Address Derivation Into the Identity Protocol
The first fix was architectural. Address derivation (pubkey ->
Base58Check address) lived in testnet.client as a utility.
But every agent identity is an address — it should be a
first-class protocol method.
Before (testnet/client.clj, leaky):
(defn agent->address [agent]
(pubkey->address (identity/public-key (:identity agent))))
After (coordination/identity.clj, protocol):
(defprotocol IAgentIdentity
(agent-id [_])
(public-key [_])
(sign [_ data])
(verify [_ data signature])
(address [_] [_ version-byte]))
Every identity now answers (address identity) returning
"m..." or "n..." for testnet,
"1..." for mainnet. The 25-byte Base58Check encoding is
self-contained:
(defn pubkey->address
([pubkey-bytes] (pubkey->address pubkey-bytes 0x6f))
([pubkey-bytes version-byte]
(let [hash160 (ripemd160 (sha256 pubkey-bytes))
versioned (byte-array (inc (alength hash160)))]
(aset versioned 0 (byte version-byte))
;; ... checksum, encode-base58 ...
)))
The testnet client delegates to identity now, removing the cycle that forced forward-declarations.
Step 2: Live Transaction Building
The core of the work. A valid BSV P2PKH transaction requires:
- UTXO selection from Bitails API
- Transaction serialization (version, inputs, outputs, locktime)
- SIGHASH preimage construction for each input
- ECDSA signing with SIGHASH_ALL appended
- Broadcast via Bitails
/tx/broadcast
UTXO Selection
Coin selection with fee estimation. The simplest approach: sort UTXOs
descending, grab until we cover amount + fee:
(defn select-utxos [utxos target-amount estimated-fee]
(let [needed (+ target-amount estimated-fee)
sorted (sort-by (comp - :satoshis) utxos)
selected (reduce (fn [acc u]
(if (>= (:total acc) needed)
(reduced acc)
(update acc :selected conj u)
(update acc :total + (:satoshis u))))
{:selected [] :total 0}
sorted)]
(if (>= (:total selected) needed)
(assoc selected :change (- (:total selected) target-amount))
(throw (ex-info "Insufficient UTXOs" {:needed needed})))))
Transaction Serialization
Every byte matters. The BSV wire format is little-endian, with varint prefixes for variable-length fields:
The serialization is a straightforward byte-array concatenation:
(defn- serialize-tx [tx]
(let [ver (le32 (:version tx))
in-count (varint (count (:inputs tx)))
in-bytes (apply concat (for [in (:inputs tx)]
(concat (hex->bytes (reverse-hex (:txid-hex in)))
(le32 (:vout in))
(varint (count (:script-hex in)))
(hex->bytes (:script-hex in))
(le32 (:sequence in)))))
out-count (varint (count (:outputs tx)))
out-bytes (apply concat (for [out (:outputs tx)]
(concat (le64 (:satoshis out))
(varint (count (:script-hex out)))
(hex->bytes (:script-hex out)))))
locktime (le32 (:locktime tx))]
(byte-array (concat ver in-count in-bytes
out-count out-bytes locktime))))
SIGHASH + Signing
The Bitcoin signing algorithm: double-SHA256 of the sighash preimage,
ECDSA sign, append 0x01 (SIGHASH_ALL):
The signing function uses the same Bouncy Castle
SHA256withECDSA that the identity protocol uses — butnote
the double-hash:
(defn- sign-input [tx input-index prevout-script-hex key-material]
(let [preimage (sighash-preimage tx input-index prevout-script-hex)
hash (sha256 (sha256 preimage)) ;; Bitcoin double-SHA256
sig-der (ecdsa-sign key-material hash)
sig-with-hash (byte-array (inc (alength sig-der)))]
(System/arraycopy sig-der 0 sig-with-hash 0 (alength sig-der))
(aset sig-with-hash (alength sig-der) (byte 0x01)) ;; SIGHASH_ALL
;; Build scriptSig: <varint length> <sig+hashbyte> <varint length> <pubkey>
...))
Full Spend Flow
The live-spend function ties it all together:
Step 3: Real OP_RETURN Anchoring
The audit module’s simulate-anchor was a SHA-256 hash
with a fake txid. The replacement live-anchor builds a
transaction where:
- Input: One or more UTXOs from the agent’s wallet (to fund the fee)
- Output 0: OP_RETURN with
6aprefix + data push + bundle hash hex - Output 1 (optional): Change output back to agent’s address (P2PKH)
(defn live-anchor [identity wallet data-hex & {:keys [fee-satoshis]}]
(let [utxos (get-unspent-with-retry (identity/address identity))
selection (select-utxos utxos 0 fee)
inputs (mapv utxo->input (:selected selection))
anchor-script (str "6a" (format "%02x" (quot (count data-hex) 2)) data-hex)
outputs [{:satoshis 0 :script-hex anchor-script}]
outputs (if (pos? (:change selection))
(conj outputs {:satoshis (:change selection)
:script-hex (p2pkh-script (pubkey->hash160 pubkey))})
outputs)
signed (sign-all-inputs {:version 1 :inputs inputs :outputs outputs} ...)
raw-hex (bytes->hex (serialize-tx signed))
result (broadcast-tx-with-retry raw-hex)]
...))
The bundle-and-anchor function now takes an optional
:wallet parameter. When present, it broadcasts to testnet.
When absent, it falls back to simulation — making all existing tests
pass without modification.
Step 4: BRC-105 Ring Middleware
HTTP 402 Payment Required is the mechanism. The BRCC-105 spec says:
server returns 402 with x-bsv-payment-* headers, client
pays and retries with x-bsv-payment header.
The Ring middleware wraps any handler and intercepts configured routes:
(defn wrap-brc105-challenge [handler & {:keys [challenge-routes price-satoshis identity-key]}]
(fn [request]
(let [payment-header (get-in request [:headers "x-bsv-payment"])]
(cond
payment-header
(if (verify-payment-header payment-header price-satoshis identity-key)
(handler request)
{:status 402 :body "invalid-payment"})
((set challenge-routes) (:uri request))
(let [challenge-headers (service-response->challenge price-satoshis identity-key)]
{:status 402 :headers challenge-headers :body "payment-required"})
:else (handler request)))))
Step 5: Retry + Resilience
Every external API call (Bitails, WhatsonChain) is wrapped with exponential backoff:
(defn with-retry [f & {:keys [max-retries base-delay-ms factor]
:or {max-retries 3, base-delay-ms 1000, factor 2}}]
(loop [attempt 1]
(let [result (try {:value (f) :error nil}
(catch Exception e {:value nil :error e}))]
(if (:error result)
(if (< attempt max-retries)
(do (Thread/sleep (* base-delay-ms (long (Math/pow factor (dec attempt)))))
(recur (inc attempt)))
(throw (:error result)))
(:value result)))))
The refresh-utxos function provides a recovery path
after failed broadcasts:
(defn refresh-utxos [identity]
(let [addr (identity/address identity)
utxos (get-unspent-with-retry addr)]
{:address addr :utxos utxos}))
The Test Suite: 100 Tests, 242 Assertions
Every layer has test coverage. The new code added tests for:
| Test Group | Tests | What It Proves |
|---|---|---|
| Address derivation | 6 | Protocol works, testnet/mainnet bytes, consistency |
| Varint/hex/LE encoding | 5 | Wire format helpers are correct |
| UTXO selection | 2 | Coin selection for sufficient/insufficient funds |
| Transaction structure | 2 | Build P2PKH tx, serialize roundtrip |
| Signing flow | 1 | ScriptSig is non-empty after signing |
| Retry logic | 2 | Returns value, throws after exhaustion |
| BRC-105 middleware | 5 | 402 issued, verified, rejected |
| Faucet/hash160/refresh | 4 | Offline handling, byte lengths |
;; From the REPL:
;; Ran 100 tests containing 242 assertions.
;; 0 failures, 0 errors.
The Architecture Decoupling
The critical design constraint: coordination.audit
needed to call coordination.testnet.client/live-anchor, but
testnet.client already depended on audit for
bytes->hex. This created a circular dependency.
The solution: pull hex->bytes and
bytes->hex into testnet.client as local
utilities (they are one-liners), then make audit depend on
testnet.client only optionally through a
:wallet key in bundle-and-anchor.
No cycles. No shared mutable state. Each module is independently testable.
The Agile Angle
This implementation followed the Scrum framework from the course: one-week sprints, daily self-inspection, Friday freezes. The product backlog was ordered by dependency (addresses before transactions, transactions before anchoring, anchoring before middleware).
The empirical pillars — transparency (open PRs), inspection (test suite), adaptation (breaking the audit cycle) — are not theoretical. They directly shaped the implementation.
What This Enables
An agent with a BSV keypair and this coordination layer can now:
- Derive its own testnet address
- Fetch live UTXO balances from Bitails
- Build and sign real P2PKH transactions
- Anchor OP_RETURN audit trails to testnet
- Request tBSV from the WhatsonChain faucet with confirmation polling
- Serve and verify BRC-105 HTTP 402 payments via Ring middleware
- Retry failed network calls with exponential backoff
The next step is the frontier the previous post described: two agents where one hires the other using these primitives. The orchestrator writes a task contract to OP_RETURN, the worker reads it, bonds a micropayment, executes, and settles — all on BSV testnet, all in Clojure, all verifiable by a third party.
The glue is laid. The transactions are real. The test suite says green.