
Proof of Spend
An agent that spends money you can audit without trusting the auditor.
Demo
About
Give an AI agent a wallet and you get two bad options. Fund it and hope — it can drain the account before anyone notices. Or approve every transaction yourself, which is just a slower you. Both are arguments about permission, and neither leaves you with evidence. When the spending is over and someone asks what happened, all you have is the agent's own account of itself. That is the actual gap. An agent's logs belong to whoever runs the agent. A spending limit enforced inside the agent's own process is a limit the operator can lift the same afternoon. If a purchase is disputed six weeks later, a justification pulled from that database was, as far as anyone outside can tell, written this morning. Proof of Spend closes the gap with ordering. Before this agent pays for anything, the spend decision — not the receipt, the decision, approved or declined — is published to a Hedera Consensus Service topic and must reach consensus before the payment is allowed to settle. A justification written after a payment can be shaped to fit it. One timestamped by a public network before the settlement exists cannot. Three layers, three jobs. The rail moves the money: a live x402-gated store on Hedera testnet, quoting native HBAR in tinybar, and a buyer client that speaks exactly one rail. The ledger decides what may be spent and records what was: an external receipt ledger the agent queries but does not own, holding a running total, because fifteen payments of 0.15 drain a wallet exactly as one over-cap payment would. The notary proves the ledger did not change its mind: HCS, holding only hashes. No chain can do the ledger's job — a transfer knows an amount and two addresses, not "espresso", and cannot refuse a purchase before it happens. No database can do the notary's job, because it certifies itself. Concretely, four things become checkable by someone who trusts neither party. If the agent is told refuse and pays anyway, the anchored refusal plus the on-chain payment proves it disobeyed, and the operator cannot quietly delete the refusal. If a spend is disputed, the anchored approval's consensus timestamp proves it was authorised before the money moved. If an auditor asks whether spending controls were real, an anchored refusal cannot be manufactured retroactively — unlike a list pulled from the agent's own database the week the auditor calls. And if a vendor claims a purchase attempt never happened, the anchored decision names the amount, the currency and the payee, timestamped, checkable without access to either side's systems. The refusals are the half that usually disappears. When an agent is told no, nothing moves: no payment, no wallet entry, no trace on any chain. The only record that a limit did its job is a database the operator controls — unless it is anchored, which here it is, with the same rigour as an approval. Verification requires nothing from us, and that is a deliberate property of Hedera rather than a generic blockchain claim: the mirror node is an unauthenticated REST API. Two curl commands are the entire tool a third party needs — no wallet, no SDK, no account. Hand someone the receipt file alone and they can hash it themselves, confirm it against the topic, pull the settlement transaction, and re-run the decision-precedes-settlement comparison without asking us for anything. The topic itself is not taken on trust either. Every topic this agent creates carries a submit key scoped to its own key, so Hedera's consensus rules reject anything it did not sign. Reusing a topic goes through the same check on every anchor: the agent reads the topic's submit key from the public mirror node and refuses to anchor unless it genuinely matches — and refuses outright any topic that has an admin key, since that key could later clear the submit key and retroactively undo the guarantee for everything already anchored there. What it does not do, stated plainly: HCS proves integrity, not truth. A ledger that files a wrong receipt and anchors it has anchored a wrong receipt, immutably. This narrows what you have to trust. It does not eliminate it. Anchoring is best-effort by design — if HCS is unreachable the purchase still completes and the receipt is still filed, visibly unanchored, because an unanchored receipt is worth more than a lost one. The store is live and public, and the half that matters most costs a checker nothing: verifying an anchor needs no account, no key and no code of ours — a receipt, curl, and a public mirror node. Buying, naturally, needs a funded Hedera testnet account of your own and access to a receipt ledger.
How it's made
An npm workspace of four packages plus a runnable walkthrough, TypeScript executed directly by Node 24 with no build step, and 393 tests across 22 files under vitest. The package boundaries are load-bearing, not decoration. Two claims in this project would otherwise be assertions. packages/verifier depends on nothing of ours — its package.json lists one public Hedera SDK and node:crypto, so "the verifier agrees" means two independent implementations agree, checkable in five seconds rather than by reading imports across files. packages/buyer speaks exactly one rail: no registry, no funding seam, nothing pluggable — its dependency list has only ever had one rail in it. packages/anchor/src/hash.ts and packages/verifier/src/hash.ts are duplicates on purpose, and the README carries the canonicalisation as a written specification both were implemented from independently. Numbers are rejected outright — a JSON number has no single spelling, and amounts are the one thing in a receipt that must not be ambiguous, so they travel as decimal strings. Keys sort by Unicode code point, including the lone-surrogate case that a UTF-8 byte comparator gets wrong. Anyone can write a third implementation from that text and get the same digest. The store is an x402 service that answers every gated path with a 402 and a challenge quoting native HBAR (asset 0.0.0) in tinybar, settled through Blocky402's open testnet facilitator. It refuses to start unless the facilitator is reachable and actually settles exact/hedera:testnet — otherwise it would bind its port, look healthy, and fail every purchase. It deploys to Vercel as a single function built by esbuild, with CI failing if the committed bundle drifts from its source. Serverless has no boot, so the guarantee a long-running server gets for free had to be rebuilt: the first request of each cold start runs the facilitator preflight, and until it passes every request answers 503 with the reason. A deployment that cannot settle payments does not get to look healthy, and a failed preflight is not cached, so a facilitator outage recovers without a redeploy. The budget lives outside the agent, in a receipt ledger reached over MCP — check_budget before, save_receipt after. The agent refuses to buy at all when neither a ledger credential nor a caller-supplied verdict is present, because an agent that decides for itself whether it may spend is the thing this project exists to prevent. The sharpest thing we got wrong is worth stating: receipts were originally filed in USD while the budget rule counted in HBAR. The ledger filtered them out of its window, spend never accumulated, and the cap silently stopped binding — a failure that looks identical to working correctly from every log. Filing in HBAR was a one-line fix for a bug that would have quietly voided the entire control. The agent-facing contract ships in the repo as a skill, so any agent opening it follows the anchor-before-pay sequence and prints the same four-field report — and the report format is enforced by tests rather than merely documented. Supply chain: a seven-day min-release-age cooldown in .npmrc, with npm ≥ 11.10.0 enforced at preinstall because older npm silently ignores it.