HBAR (via the JSON-RPC relay)
- → result / status is SUCCESS or 0x1
- → tx.to is one of the payee's address forms
- → tx.from is one of the declared payer's address forms
- → tx.amount (tinybars) ≥ 1,000,000 = 0.01 HBAR
Hedera has no public x402 facilitator, so this app is both halves: the endpoint issues the challenge and the endpoint verifies settlement against the ledger. Below is every record it reads, every check it runs, and every reason it says no. The live endpoint is /api/public/x402-paid-content.
An unauthenticated GET returns HTTP 402 with an x402 v2 body: { x402Version: 2, accepts: [...] }. Two requirements are offered — native HBAR and HTS USDC — both scheme "exact" on network hedera:testnet. The same JSON is mirrored, base64-encoded, in a PAYMENT-REQUIRED header.
Hedera has no public facilitator, so the client pays on-chain itself, waits for consensus, and only then retries the resource. Retrying immediately produces settlement_not_found even though the payment is perfectly fine.
The retry carries PAYMENT-SIGNATURE: base64 of { x402Version: 2, accepted: <the requirement echoed back>, payload: { transactionId, payer } }. There is no signature to check here — the on-chain record is the proof, so the envelope only has to name it.
The server re-derives the requirement itself and compares scheme, network, asset, amount and payTo field by field. Only then does it read the mirror node. Nothing the client says about the payment is trusted.
// the retry header, decoded
{
"x402Version": 2,
"accepted": { "scheme": "exact", "network": "hedera:testnet",
"asset": "0.0.429274", "amount": "10000",
"payTo": "0.0.9822626" },
"payload": { "transactionId": "0x…", "payer": "0.0.x or 0x…" }
}The transaction id in the envelope decides which record is authoritative. A 0x hash means the payment went through the relay and lives on the EVM contract-result endpoint; a 0.0.x-seconds-nanos id means it was submitted natively and lives on the consensus transaction endpoint.
This is the single most important record on the page, because it is the only place a relay-submitted HTS transfer proves its amount. Read logs[] off the contract result and match on address plus topic:
logs[i].address = 0x0000000000000000000000000000000000068cda // USDC on Hedera testnet
topics[0] = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
topics[1] = payer (32-byte padded)
topics[2] = payee (32-byte padded)
data = amount (atomic, 6 decimals — 0x2710 = 10000 = 0.01 USDC)
const unpad = (t) => `0x${t.replace(/^0x/, "").slice(-40)}`;
credited += BigInt(log.data); // only when payer AND payee forms both matchThe consensus timestamp on the same record is what bounds the validity window: older than maxTimeoutSeconds (300s) or more than 60s in the future and the settlement is refused.
A failed verification returns another 402 with the same accepts[] plus an error string. Those strings are the only diagnostic a client gets, so they are printed verbatim in the demo's flow log.
Two more guards sit in front of all of this: a per-IP rate limit (12 requests a minute) and a spent-transaction set, so one settlement can never unlock the resource twice.
HTS movements caused by an ERC-20 call are NOT on the parent consensus record — its token_transfers array is empty — and /contracts/results/{hash} carries no transaction_id to hop to the child CRYPTOTRANSFER. The authoritative signal ships with the EVM record: the ERC-20 Transfer log.
tx.from / tx.to and the 32-byte event topics use different address shapes. Collect both the long-zero form and the ECDSA evm_address alias from /accounts/{id} and match against the set; un-pad topics with 0x + topic.slice(-40).
The mirror node lags consensus by seconds. Never read once — poll with a bounded retry loop (8–10 attempts, ~1.2–1.5s apart) on both /transactions/{id} and /contracts/results/{hash}.
A relay transfer at a low gas limit returns a hash and fails on-chain with INSUFFICIENT_GAS. Budget ~900,000 gas and treat a returned hash as "submitted", never "settled".
The address-form and gas traps are the same seam documented in detail on the EVM ↔ native page.