You pay first. The money waits until you have read what you bought.
Held is an x402-gated agent service on Hedera where the payment lands in escrow instead of the seller's account. A buyer pays, receives the deliverable immediately, and then decides. Approve and the seller is paid. Reject and the buyer is refunded. Say nothing and a Hedera scheduled transaction pays the seller when the review window expires.
I built it around one question: what has to be true before you hand money to an agent you have never used? x402 settles the instant a response is written, which is correct for a weather API and backwards for work you have to read before you can judge it. Held changes one field to fix that.
Open Held · Watch the demo · Verify it on Hedera · Read the 402 · What can go wrong · Run it locally
Project stage: Hedera testnet only. No mainnet writes and no real funds. The escrow the live site uses is a plain Hedera account, which means the operator holds its key. A contract that needs no such trust is deployed and every ending is proven on it, but it is not what the running service settles through. That is stated here, in
AI-USE.md, and on the site, rather than left for a reader to discover.
Built for ETHOnline 2026, Hedera "AI & Agentic Payments".
Watch the demo on YouTube (2:08). It shows a real paid request settling into escrow, the buyer approving it, and a second run where nobody clicks and Hedera's own scheduled transaction pays the seller.
Paying an agent is not like paying an API. You cannot tell whether you got what you needed until after you have read it, and by then x402 has already paid the seller.
- Escrow by substitution, not by protocol change. The 402 challenge is ordinary x402. Only
payTodiffers, so any client that already speaks x402 can buy without modification. - Immediate settlement, deferred release. The money moves on chain at once, so the seller knows it is real, but it lands where neither party can take it unilaterally.
- Silence resolves forward. A card authorisation nobody captures expires backwards and the payer keeps the money. This expires forwards, so a buyer cannot trap a seller's funds by going quiet.
- Evidence the service does not own. Every state change is appended to a Hedera Consensus Service topic. The service writes the lines; the network orders and timestamps them.
- The build is identified, not just the seller. An agent's version id is a hash of its prompt template, its model, and its own source, because that is what a buyer is actually approving.
flowchart LR
A[Buyer asks] --> B[402: payTo is escrow]
B --> C[Pay via Blocky402]
C --> D[Settles into escrow 0.0.10495061]
D --> E[Deliverable returned at once]
E --> F{Buyer decides}
F -->|approve| G[Escrow pays the seller]
F -->|reject| H[Escrow refunds the buyer]
F -->|silence| I[Scheduled transaction pays the seller at expiry]
G & H & I --> J[Appended to HCS topic 0.0.10495064]
No account, no key, nothing to install. Every expected value below came from running these commands before this README was written.
# 1. Ask for the resource without paying. The 402 names an escrow account, not the seller.
curl -s -X POST https://heldprotocol.xyz/work \
-H 'content-type: application/json' \
-d '{"question":"what does this cost"}' | jq '.accepts[0], .extensions.held'
# payTo "0.0.10495061" <- escrow
# network "hedera:testnet"
# amount "5000000" (0.05 HBAR in tinybars)
# feePayer "0.0.7162784" <- the seller's own account, a different account
# releasePolicy "approve pays the seller, reject refunds you, silence pays the seller at the deadline"
# 2. Confirm that escrow account is real, and holds money, straight from Hedera.
curl -s https://testnet.mirrornode.hedera.com/api/v1/accounts/0.0.10495061 | jq .balance.balance
# 1849999999 (tinybars, read 2026-09-13; it moves as jobs settle)
# 3. Read the evidence topic. Nobody needs our permission for this.
curl -s "https://testnet.mirrornode.hedera.com/api/v1/topics/0.0.10495064/messages?limit=1&order=desc" \
| jq -r '.messages[0].message' | base64 -d | jq
# {"jobId":"...","type":"released","payload":{...},"at":"...","tier":"hcs","degraded":false}The third command is the point. It reaches Hedera directly and returns the same records the service shows, without the service being involved at all.
| Surface | Where | What it is |
|---|---|---|
| Service | heldprotocol.xyz | Seven pages: the argument, the buy flow, the register, per-job pages, live proof, the x402 reference, the invariants |
| Health | /health |
Which tier each subsystem is on and whether any is degraded |
| Escrow account | 0.0.10495061 |
The account payTo names |
| Evidence topic | 0.0.10495064 |
Append-only record of every state change |
| A deadline schedule | 0.0.10495601 |
One armed auto-release |
| Escrow contract | 0x75f1Eb37…6ebB0 |
Release enforced by code, with a permissionless deadline |
- All three endings executed on Hedera testnet with transaction ids anyone can resolve: approve,
reject, and the deadline path. They are listed in
evidence/PROOF.md. - The deadline path was exercised without anyone touching it. Transaction
0.0.10491999-1789311316-678160340was executed by Hedera's scheduled transaction, not by this service. The service only observed the result. - The consensus topic held 219 messages when this was written. It spans every run of the service including development machines, which is why it holds more jobs than any single deployment's own register.
- 13 of 13 tests pass, covering all three endings, the evidence trail, and an injected
schedule-arming failure.
scripts/attack.jsfires five concurrent approvals at one job and exactly one is accepted. - All four contract endings executed on Hedera's EVM, including a partial split (1.25 to the
seller, 0.75 back to the buyer) and an expiry pushed by an account that was neither the buyer, nor
the seller, nor this service. The contract held 0.0 HBAR afterwards, so nothing was stranded.
21 of 21 contract tests pass, including a re-entrant seller that is paid exactly once and a
256-run fuzz proving a split always conserves the amount. See
evidence/CONTRACT.md. - Two invariants were broken and fixed during the build, both visible on chain. Both are written
up with their causes in
evidence/INVARIANTS.mdrather than quietly repaired.
These are single runs on a testnet, recorded because they happened. They do not establish uptime, an SLA, throughput, or behaviour under load, and none of that is claimed.
- Testnet only. No mainnet writes, no real funds.
- The live default is still the account escrow, not the contract. The contract exists, is deployed, and every ending is proven on it (below), but the running service settles through the Hedera account because that is what the recorded demo shows. Switching the default is a deployment decision, not more building.
- The job register is per-deployment. It lives on a mounted volume and starts empty when a new deployment does. The consensus topic is the durable record; the register is a convenience.
DEMO_BUYspends the server's own funded account so a visitor can try the flow without a wallet. It is rate limited and input capped, and it can be switched off, but a determined visitor can still drain that account. On testnet the fix is a refill.- Per-job pricing and partial release are off by default. Both are built.
PRICING=perjobmakes the 402 quote scale with the question and caps it; the contract'ssettlesplits a job between seller and buyer. The defaults stay flat and all-or-nothing so the recorded demo remains accurate. - A contract job id can be squatted.
fundtakes the job id from its caller, so anyone can occupy an id with 1 tinybar and make the real buyer's call revert. Nothing is stolen and no held job is touched; the buyer retries under a new id. Keying jobs by caller and id would remove it, and that is the change to make before this runs anywhere that matters. Found by reviewing the deployed source, written up inevidence/CONTRACT.md. - The worker falls back. If the model provider fails, output is produced by a deterministic stand-in, and every deliverable carries a byline naming which tier answered.
Requires Node 20. A Hedera testnet account is needed only for the live tier; without keys the service runs on a local stand-in and says so on every page and receipt.
npm install
npm test # 13 tests, no network, no keys
npm run seller # http://localhost:4021npm test runs the Hedera transaction dry-run, key handling, worker fallback, and the regression
suite. It builds every Hedera transaction offline and submits none of them.
To run against real Hedera testnet, copy .env.example and fill it in, then:
npm run go-live # creates the escrow account, topic, and a funded buyer
npm run seller
npm run buyer ask "When does a Hedera scheduled transaction execute?"
npm run prove # replays all three endings and rewrites evidence/PROOF.md
npm run test:contract # 21 contract tests, local, no network
npm run prove:contract # deploys to Hedera testnet and runs all four endings on chainsrc/ the service
seller.js x402 resource server, the 402 challenge, approve and reject
settlement.js Hedera transfers, escrow release, the scheduled transaction
evidence.js appends to the consensus topic, reads it back
store.js job state machine; transition() is the double-payout guard
worker.js the agent, with a tiered provider cascade
public/ the site: seven pages, one stylesheet, one script, no build step
contracts/
src/HeldEscrow.sol escrow enforced by code: per-job amounts, partial release, permissionless expiry
test/ 21 tests including re-entrancy and a conservation fuzz
scripts/ go-live, prove-live, prove-contract, the attack and regression suites
evidence/
PROOF.md transaction ids for all three endings, resolvable on HashScan
CONTRACT.md the deployed escrow contract and its four on-chain endings
INVARIANTS.md 13 properties, how each is enforced and tested, and the two that broke
docs/ architecture, deployment, access, and the demo script
spec/ the plan, build log, and design record written as it happened
video/ the demo: Remotion composition, filming scripts, marker files
brand/ the mark, its art direction, and the rejected candidates
- Make the claim no stronger than the evidence, and write the evidence down first.
- A failure that was found is worth more written up than quietly fixed.
- Never present a degraded tier as a full one. Every output says which tier produced it.
- Put the proof somewhere the service does not control, so it survives the service being wrong.
- Probe an unfamiliar API and record the measurement with its date, rather than writing a call from memory.
Claude Code wrote most of the code here and I directed it.
AI-USE.md names each intervention and what changed as a result, and the decisions are
recorded in spec/ as they happened rather than asserted afterwards.
MIT. See LICENSE.
