How it works

One paid call from the 402 to the next call: the deposit, the signed voucher, verify and settle, the credit limit, and the numbers behind them.

On this page

An agent pays a provider with two kinds of messages. A deposit is an on-chain, shielded payment that happens once per window. A voucher is an off-chain signature that goes with every call. The facilitator ties the two together and tells the provider whether to serve.

This page follows one session from the first call to the next hundreds. The numbers are the demo terms: a price of 500 zat per call, a window of 100,000 zat, 25 percent credit and a cap of 200,000 zat.

The parts

PartWhat it isJob
AgentYour code: the stock x402 client plus PrepaidVoucherScheme from @z402-net/client, and a walletMakes deposits, signs vouchers, retries after a refusal
Provider APIAny HTTP server with the x402 middleware and PrepaidVoucherServerScheme from @z402-net/serverNames a price per route, runs the handler once the facilitator says the payment is good
Facilitator@z402/facilitator, shared by many providersVerifies vouchers, finds and reads deposits, settles, keeps the ledger, hosts the catalog
Zcash nodeA lightwalletd-compatible endpoint (Zaino on the demo chain)Shows transactions and blocks to the facilitator and to the agent
call, voucherverify, settledepositreads depositsAgentProvider APIFacilitatorZcash node

The first call, end to end

GET /chunk402 and the offerDeposit 100,000 zat with memoGET /chunk, voucher 500, txidverifyLook up the depositIn the mempoolValid, limit 100,000Run the handlersettleSettled, deposit credited200 and the answerAgentProvider APIFacilitatorZcash nodeAgentProvider APIFacilitatorZcash node
GET/chunk402 andthe offerDeposit 100,000 zat with memoGET/chunk,voucher500, txidverifyLook upthedepositIn themempoolValid, limit100,000RunthehandlersettleSettled,depositcredited200 andtheanswerAgentProviderAPIFacilitatorZcashnodeAgentProviderAPIFacilitatorZcashnode
  1. The agent asks without paying and gets a 402 with the price and the terms.
  2. The agent makes one shielded deposit to the provider's address. The deposit is not confirmed yet.
  3. The agent signs a voucher for 500 zat and sends it with the txid of the deposit.
  4. The provider asks the facilitator to verify. The facilitator finds the deposit in the mempool, reads it with the provider's view-only keyUnified incoming viewing key (UIVK). Whoever holds it can see the payments made to an address, but cannot spend them., and counts it toward the limit.
  5. The provider runs the handler, then asks the facilitator to settle. The facilitator records the voucher and credits the deposit.
  6. The agent gets the answer, with the receipt in the PAYMENT-RESPONSE header.

The rest of this page takes each step in turn.

The 402 offer

The stock x402 client sends every call twice: first without payment, to read the offer, then again with the voucher. The offer is the PAYMENT-REQUIRED header of the 402, base64 JSON. Its accepts entry for z402 looks like this:

JSON
{
  "scheme": "prepaid-voucher",
  "network": "zcash:regtest",
  "amount": "500",
  "asset": "ZEC",
  "payTo": "uregtest1...",
  "maxTimeoutSeconds": 60,
  "extra": {
    "windowZat": "100000",
    "creditRatioBps": 2500,
    "creditCapZat": "200000",
    "zeroConfMinExpiryBlocks": 20,
    "termsVersion": "<16 hex characters>"
  }
}
schemestringRequired
prepaid-voucher, the payment scheme.
networkstringRequired
The Zcash network: zcash:regtest, zcash:testnet or zcash:mainnet.
amountstringRequired
The price of this call in zat, a whole number from 1 to windowZat. The provider sets it for each route.
assetstringRequired
ZEC. All amounts are in zat, which is one hundred millionth of a ZEC.
payTostringRequired
The provider's shielded-only unified address. Deposits go here.
maxTimeoutSecondsnumberRequired
The x402 field for how long a payment may take. The demo API sends 60.
extra.windowZatstringRequired
The size of one deposit. It is also the most unconfirmed money the provider will count.
extra.creditRatioBpsnumberRequired
Credit on confirmed deposits, in basis points. 2500 is 25 percent.
extra.creditCapZatstringRequired
The most credit one session can hold.
extra.zeroConfMinExpiryBlocksnumberRequired
An unconfirmed deposit counts only with at least this many blocks left before it expires.
extra.termsVersionstringRequired
16 hex characters that name the facilitator's terms. The agent ignores it.

The facilitator owns the terms, so every provider on it offers the same window, credit ratio and cap. The provider owns amount and payTo.

The agent checks the offer before any money moves. It refuses an offer whose scheme is not prepaid-voucher, whose network is not a Zcash network, whose asset is not ZEC, or whose payTo is not a shielded-only address for that network. It also refuses a price of 0, a price above the window, and a window above maxDepositZat, which defaults to 1,000,000 zat.

extra is the same on every call to a route. Per-session values never appear in it, because the server compares the agent's echo of the offer with its own.

The deposit

The agent deposits before its first voucher, because it has nothing to spend yet. It waits for the wallet to broadcast the deposit, then signs.

Amountexactly windowZat, in one output: 100,000 zat
Tothe provider's payTo
MemoA short text that travels encrypted inside a shielded Zcash payment. Only the recipient's keys can read it.the text <sessionKey>:<index>, which binds the deposit to the agent's session. The index counts 1, 2, 3 per session and is informational.
Feeabout 10,000 zat for a deposit with one output and one change output
Time to build5 to 10 seconds, while the wallet makes the proof (5.4, 6.9 and 7.4 seconds were measured)
Expiresabout 40 blocks after it is built, if it is never mined

The agent makes a fresh session keyAn Ed25519 key pair made by the agent for one provider. Its public half, 64 hex characters, is the session key. for each provider it pays. The public half is the 64-character sessionKey. It goes into the memo and into every voucher, which is how the provider ties the deposit and the calls together without an account. Keys are not saved. If the agent restarts, it starts a new session, with a new deposit and no credit.

The voucher

A voucher says how much the agent has used in total with this provider. It is the Ed25519 signature of six lines joined with newlines:

Text
z402-voucher-v1
prepaid-voucher
zcash:regtest
uregtest1...
<sessionKey>
500

The lines are a fixed label, the scheme, the network, the provider's payTo, the session key and the new running total, cumulativeZat. Because the message names the provider's address, a voucher for one provider can never verify at another.

The agent sends the voucher inside the x402 payment, in the PAYMENT-SIGNATURE header. Here are all three headers of a call, decoded:

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "http://localhost:4021/chunk",
    "description": "One numbered chunk of demo data per paid call",
    "mimeType": "application/json",
    "serviceName": "Demo API",
    "tags": ["demo"]
  },
  "accepts": [ { "scheme": "prepaid-voucher", "...": "the offer above" } ]
}

Three rules make a running total safe:

  • The total only grows. The facilitator remembers the last settled total, c_last. A voucher is accepted only if its total is at least c_last plus the price of this call. A replayed old voucher buys nothing.
  • The rule is lenient. A total that jumps by more than the price is still accepted, up to the limit, and the whole difference is charged. That is how a lost response gets paid for by the next voucher.
  • The deposit rides along. The agent attaches the txid of its newest deposit to every voucher until a successful settle acknowledges it.

Verify

The provider's middleware calls POST /verify on the facilitator with the payment, its own requirements and its API key. The key tells the facilitator which provider is asking. The facilitator checks, in this order:

CheckRefusal
The request is well formed: JSON that fits the x402 v2 schemas, with version 2invalid_payload, invalid_x402_version
The requirements are this provider's: the rail's scheme and network, asset ZEC, its payTo, a price from 1 to windowZat, the facilitator's termsunsupported_scheme (another scheme or network), invalid_payment_requirements
The payment agrees with the offer: the same scheme and network, canonical voucher fieldsinvalid_scheme, invalid_network, invalid_payload
The signature verifies under the session keyz402_invalid_signature
The total grew by at least the pricez402_cumulative_mismatch
A deposit the session has not used yet is usable: found, fully shielded, with an output to payTo that carries this session's memoz402_deposit_not_found, z402_chain_unavailable, z402_deposit_transparent, z402_deposit_memo_mismatch, z402_deposit_dead
The total is within the limitz402_awaiting_confirmation, z402_deposit_required

To check a deposit, the facilitator asks the node for the transaction by its txid and decrypts it with the provider's view-only key. A transaction in the mempoolThe pool of transactions that a node has received but that are not in a block yet. is pending. One in a block is confirmed.

If the voucher fits without the new deposit, verify answers at once and looks the deposit up in the background. Otherwise it waits for the lookup, for up to 30 seconds, since a public node behind a load balancer can take about 16 seconds to show a new transaction. The provider allows the facilitator 60 seconds in all.

Once it accepts the API key, the facilitator answers with HTTP 200 and a complete body: isValid, payer (the session key), and extra.session, a snapshot of the session that the agent uses to stay in step:

JSON
{
  "sessionKey": "7c1e…9d",
  "cumulativeZat": "500",
  "limitZat": "100000",
  "headroomZat": "99500",
  "confirmedZat": "0",
  "pendingZat": "100000",
  "creditZat": "0",
  "tip": 119,
  "deposits": [
    { "txid": "3f2a…c1", "valueZat": "100000", "status": "pending", "confirmations": 0, "expiryHeight": 160, "zeroConf": true }
  ]
}

Serve and settle

Only after a valid verify does the handler run. The middleware holds the handler's response until the settle returns. The provider then calls POST /settle, and the facilitator:

  1. repeats the checks, using only what verify already read from the chain, and never waits on the chain;
  2. appends the voucher, and the deposit if it is new, to the ledger;
  3. commits the new running total and credits the deposit.

Steps 1 to 3 run with no await between the last check and the write, so two settles of one voucher can never both succeed.

The ledger is an append-only file with three kinds of events: deposit, voucher and status. The facilitator replays it when it starts, which is why a restart cannot credit a deposit twice or accept an old voucher again. A voucher event records only the running total, never the signature.

The settle reply says success, the charged amount, and, in transaction, the txid of the deposit when it is credited to this session. That is the acknowledgement. The agent counts a call as paid only on success: true, and it stops resending a deposit only after a successful settle acknowledges it. If settle fails after the handler ran, the held response is thrown away and the agent gets a 402 carrying the failure.

The credit limit

The facilitator turns a session's deposits into one number, the limit: the most the provider will accept right now.

Text
confirmed = the deposits that are confirmed or final
pending   = min(window, the unconfirmed deposits that qualify)
credit    = min(creditCap, confirmed x creditRatio)
limit     = confirmed + pending + credit
accept    = the total grew by at least the price, and the total is at most the limit

On the demo terms:

State of the sessionLimit
New session0
First deposit pending100,000
First deposit confirmed125,000
First confirmed, second pending225,000
Both confirmed250,000
Both pending100,000
Eight confirmed1,000,000
One deposit of 1,000,000 zat, pending100,000

Three rules sit behind the table:

  • Unconfirmed money counts for one window at most. Two pending deposits give 100,000, not 200,000.
  • An unconfirmed deposit qualifies when it is at least one full window and has at least 20 blocks left before it expires, counted from the tip when the facilitator first saw it. That is why the first call can be served before any block.
  • Credit is a quarter of what has confirmed, up to 200,000. It is a loan: the agent can spend it before it has paid for it.

For a provider, the worst case per session is one unconfirmed window plus the credit line, plus the value of a credited deposit that a chain reorganization removes before it is final. A cheating agent that makes one deposit and no more gets at most 125,000 zat of service for it, and the provider has seen 100,000 zat confirmed by then.

Deposit status

seen in the mempoolmined10 confirmationsexpired or droppedmined after allpendingconfirmedfinaldead
StatusMeaningCounts toward the limit
pendingIn the mempoolUp to one window, if it qualifies
confirmedMined, 1 to 9 confirmationsIn full, and it earns credit
final10 or more confirmationsThe same as confirmed, and no longer polled
orphanedMined on a side chainNowhere
deadExpired unmined, or not found for 120 seconds and 2 blocksNowhere

The facilitator checks the chain tip every 5 seconds and reads its deposits again whenever the tip changes. Before it refuses a session that has a pending deposit, it reads that deposit once more, at most once per 5 seconds per session, so a block that landed a moment ago is not missed.

The next calls

Later calls need no chain traffic at all. Each one is a voucher with a larger total.

GET /chunk402 and the offerGET /chunk, voucher 1,000verifyValidsettleSettled, charged 500200 and the answerAgentProvider APIFacilitatorAgentProvider APIFacilitator
GET /chunk402 and theofferGET /chunk,voucher 1,000verifyValidsettleSettled, charged500200 and theanswerAgentProviderAPIFacilitatorAgentProviderAPIFacilitator

The agent signs c_last + price for each call and keeps one call in flight per session, so totals never collide.

The agent also stays ahead of its limit. When the last snapshot shows nothing pending and the headroom (limit minus the total) falls below one window, it starts the next deposit in the background and keeps calling. Here is a recorded run on the demo terms, one call per second and a block every 25 seconds:

CallWhat happenedTotalLimitConfirmedPendingCredit
1First deposit sent inline, call served on it500100,0000100,0000
15First deposit mined, credit appears7,500125,000100,000025,000
51Headroom is under 100,000, second deposit starts in the background25,500125,000100,000025,000
58Second deposit pending29,000225,000100,000100,00025,000
65Second deposit mined32,500250,000200,000050,000

No call waited for a block. Only call 1 waited for anything, and that was the deposit's proof.

When the limit is reached

An agent that stops depositing runs into its limit. The provider refuses the voucher, and the refusal carries the session snapshot, so the agent learns the exact state.

[nothing is pending]alt[a deposit is pending]GET /chunk, voucher over limitverifyRefused, reason and snapshot402, error is the reasonWait for it to confirmMake a new depositGET /chunk, new voucher200 and the answerAgentProvider APIFacilitatorAgentProvider APIFacilitator
[nothing is pending]alt[a deposit ispending]GET /chunk,voucher over limitverifyRefused,reason andsnapshot402, error is thereasonWait for itto confirmMake a newdepositGET /chunk, newvoucher200 and the answerAgentProviderAPIFacilitatorAgentProviderAPIFacilitator

The reason is a code, and the code carries the action. The stock client allows one recovery per call.

ReasonMeaningWhat the agent does
z402_awaiting_confirmationOver the limit, a deposit is pendingNever deposits again. Waits until the pending deposit has one confirmation, then retries once.
z402_deposit_requiredOver the limit, nothing is pendingMakes a deposit, then retries once.
z402_cumulative_mismatchThe total did not grow by the priceTakes the facilitator's total from the snapshot and retries once.
z402_deposit_deadThe deposit expired or was droppedMarks it dead, makes a new deposit, retries once.
z402_deposit_not_foundThe facilitator cannot see the depositChecks the chain itself, keeps the txid, retries.
z402_chain_unavailableThe facilitator's node is not answeringWaits 5 seconds and retries once.
z402_deposit_memo_mismatchThe deposit cannot be credited to this sessionStops. The address is wrong, or there is a bug.
any other codeStops and reports it.

The agent waits at most 10 minutes for a confirmation, and then gives up and returns the 402 to its caller. Errors and reason codes lists all 19 codes, and Troubleshooting says what to do about each.

When the outcome of a call is unknown, for example when the connection breaks after the voucher was sent, the agent assumes the provider may have charged it and advances its total. The next voucher then pays for that call too.

Many providers, one rail

The facilitator is one rail that many providers share, each with its own address, key and prices. A provider registers once, with its payTo and its view-only key, and receives an API key. It sends that key as a bearer token with every verify and settle.

API key AAPI key BAgentProvider AProvider BFacilitatorLedger of ALedger of B
API key AAPI key BAgentProvider AProvider BFacilitatorLedger of ALedger of B
  • Separate state. The facilitator opens a runtime for each provider, with its own ledger. Deposits decrypt only with that provider's key, and credit is kept per provider. Nothing is shared between two providers' sessions.
  • One session per provider. The agent keeps one session, with its own session key and deposit, for each payTo it pays. A deposit to provider A never funds calls at provider B.
  • A price per route. Each route sets its own price, from 1 zat up to the window. The routes of one provider share a session, and each voucher advances the total by the price of its own call.
  • The key picks the provider. The facilitator refuses any requirements whose payTo is not the one registered to the key presented.

The Providers and API keys page covers registration and rotation.

Finding a provider

Providers list their routes in the facilitator's public catalog, the x402 BazaarThe public catalog of paid APIs that x402 facilitators host. Agents read it to find services.. An agent reads the whole list, with GET /discovery/resources, and chooses on its own side, so no server learns what it is looking for. The catalog only advertises. The 402 that a route returns stays authoritative: the agent pays what the offer says. See Discovery.

Timing

WhatHow long
Building and broadcasting a deposit5 to 10 seconds
A new transaction appearing in the node's mempoolabout half a second
The facilitator's check of the chain tipevery 5 seconds
A block on the demo chainevery 25 seconds, set by Z402_BLOCK_SECONDS
A block on mainnetabout 75 seconds on average
A deposit turning final10 confirmations
Verify waiting for a deposit it cannot see yetup to 30 seconds
The provider waiting for the facilitatorup to 60 seconds
Zero-conf margin before a deposit expiresat least 20 blocks
A deposit judged dead when it is never found120 seconds and 2 blocks

A deposit therefore turns confirmed in the facilitator's view a few seconds after its block: the lag of the node, plus up to one 5-second poll.

References

  • docs/spec/protocol.md: the wire contract. Sections 2 to 8 cover the offer, the payload, the voucher, deposits, the credit limit, the facilitator's answers and the reason codes.
  • docs/superpowers/specs/2026-10-27-z402-facilitator-design.md: the deposit lifecycle, the verify and settle steps, the ledger.
  • docs/superpowers/specs/2026-10-27-z402-client-design.md: what the agent does, and in which order.
  • docs/superpowers/specs/2026-10-28-z402-multi-provider-design.md: providers, API keys, per-route prices and the catalog.
  • docs/research/live-run.md: the measured timings and the recorded run in the table above (section 8.2).
  • docs/research/x402-e2e-spike.md: the x402 headers and the order of the calls, as measured with the stock SDK.
  • packages/protocol/src/ramp.ts: computeLimit, zeroConfEligible and checkVoucher.
  • packages/protocol/src/voucher.ts: the signed message and its verification.
  • packages/protocol/src/memo.ts: the deposit memo.
  • packages/protocol/src/reasons.ts: the closed list of reason codes.
  • packages/client/src/scheme.ts: the client scheme and its deposit and recovery rules.
  • packages/client/src/session.ts: when the agent deposits inline, in the background, or not at all.
  • packages/facilitator/src/scheme.ts: the facilitator's verify, settle and limit decision.
  • packages/server/src/scheme.ts: the provider's server scheme.
  • docs/demo.md: the demo run, call by call.
  • x402 specification: the payment headers and the verify and settle contract.
  • ZIP 302: the memo field format.
  • ZIP 317: the proportional transaction fee.
  • RFC 8032: Ed25519 signatures.