x402 and HTTP 402

How a paid HTTP call works in x402 v2: the 402 offer, the signed payment, the facilitator, and what z402 adds on Zcash.

On this page

HTTP reserves a status code for payments: 402 Payment Required. RFC 9110 marks it as reserved for future use. x402An open standard that lets a web server ask for payment with the HTTP status 402 Payment Required, and lets a client pay and ask again. gives it a job. A server answers 402 with what it wants to be paid, the client pays, and the same request goes through.

z402 is a payment scheme for x402. It settles in shielded Zcash. Most of this page is plain x402 v2, which z402 uses as it is: the stock client and server code run unchanged, with one scheme registered. What z402 adds lists the differences.

Who is in the flow

RoleIn x402In z402
ClientThe buyer. It makes the HTTP request and pays.An agent with a wallet and a session key.
Resource serverThe seller. It serves the resource and answers 402.A provider's API or service.
FacilitatorThe service a resource server asks to check and record payments. It keeps the server from running payment code itself.Verifies and settles payments for resource servers.One hosted rail that many providers share. It reads deposits from the Zcash chain.

The client never talks to the facilitator to pay. The resource server does, with an API key. That keeps chain access and payment logic out of every API that sells something.

One paid call

GET /resource402 + PAYMENT-REQUIREDpick an offer, build the paymentGET /resource + PAYMENT-SIGNATUREPOST /verifyisValidrun the handlerPOST /settlesuccess200 + PAYMENT-RESPONSEClientResource serverFacilitatorClientResource serverFacilitator
GET /resource402 +PAYMENT-REQUIREDpick anoffer, buildthepaymentGET /resource +PAYMENT-SIGNATUREPOST /verifyisValidrun thehandlerPOST /settlesuccess200 +PAYMENT-RESPONSEClientResourceserverFacilitatorClientResourceserverFacilitator
  1. Ask

    The client sends the request with no payment.
  2. Offer

    The server answers 402 and a PAYMENT-REQUIRED header that lists what it accepts.
  3. Pay

    The client picks an offer, builds a payment for it, and repeats the request with a PAYMENT-SIGNATURE header.
  4. Verify

    The server sends the payment and the offer it matched to the facilitator's /verify. Verify checks and reads. It commits no voucher and credits no deposit.
  5. Serve

    If the payment is valid, the server runs its handler. A handler that fails with a status of 400 or more ends the flow here, and nothing is settled.
  6. Settle

    The server calls /settle. The facilitator commits the payment. The server returns the handler's response with a PAYMENT-RESPONSE header.

This order, verify then handler then settle, is the x402 authorization flow. z402 uses it and nothing else. The SDK's default is the same, so a z402 scheme sets no paymentFlow key.

The three headers

HeaderDirectionCarries
PAYMENT-REQUIREDServer to client, on the 402The offers: a PaymentRequired object.
PAYMENT-SIGNATUREClient to server, on the retryThe chosen offer and the proof: a PaymentPayload object.
PAYMENT-RESPONSEServer to client, with the resultThe outcome of settlement: a SettleResponse object.

Each value is the UTF-8 JSON of the object, encoded as standard base64. It is not base64url, and the decoder refuses other characters. A z402 server sends {} as the body of its 402: everything is in the header.

curl -s -D - -o /dev/null http://127.0.0.1:4021/chunk | tr -d '\r' \
  | awk -F': ' 'tolower($1) == "payment-required" { print $2 }' | base64 -d | jq '.accepts[0]'

The offer: PaymentRequired

A server that wants payment sends one PaymentRequired object.

x402VersionnumberRequired
The protocol version. z402 speaks version 2 only, and answers anything else with invalid_x402_version.
errorstring
Why payment is needed. An unpaid request gets Payment required. A refused payment carries the facilitator's reason code, such as z402_deposit_required.
resourceobjectRequired
What is for sale: a url, and optionally a description, mimeType, serviceName, tags and iconUrl.
acceptsPaymentRequirements[]Required
The offers. The client picks one it knows how to pay. A z402 service lists one.
extensionsobject
Extra data keyed by extension name. The bazaar extension describes the route for the catalog. The z402 extension names the billing unit and an optional privacy note. The stock client copies the object into its payment.

Each offer is a PaymentRequirements object:

schemestringRequired
How to pay. For z402, prepaid-voucher.
networkstringRequired
Where to pay, as a CAIP-2 style id. For z402, zcash:regtest, zcash:testnet or zcash:mainnet.
assetstringRequired
What to pay in. For z402, ZEC.
amountstringRequired
The price of this call, as a decimal string in the asset's smallest unit. For z402, zat.
payTostringRequired
Who is paid. For z402, the provider's shielded-only unified address.
maxTimeoutSecondsnumberRequired
How long the payment may take. z402 routes use 60.
extraobjectRequired
Scheme-specific terms. For z402, the window, the credit rules and a terms version. Prepaid vouchers explains them.

This is a decoded PAYMENT-REQUIRED from the URL to markdown service, with the address shortened and the bazaar schema left out:

PAYMENT-REQUIRED, decoded
{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "https://markdown.example.com/v1/markdown",
    "description": "Fetches one public web page and returns its main content as clean markdown.",
    "mimeType": "application/json",
    "serviceName": "URL to markdown",
    "tags": ["web", "markdown", "scrape"]
  },
  "accepts": [
    {
      "scheme": "prepaid-voucher",
      "network": "zcash:regtest",
      "amount": "1000",
      "asset": "ZEC",
      "payTo": "uregtest1...",
      "maxTimeoutSeconds": 60,
      "extra": {
        "windowZat": "100000",
        "creditRatioBps": 2500,
        "creditCapZat": "200000",
        "zeroConfMinExpiryBlocks": 20,
        "termsVersion": "cb7f148af0fda19a"
      }
    }
  ],
  "extensions": {
    "bazaar": {
      "info": {
        "input": { "type": "http", "queryParams": { "url": "https://example.com" }, "method": "GET" },
        "output": { "type": "json", "example": { "title": "Example Domain", "length": 16 } }
      },
      "schema": "..."
    },
    "z402": { "unit": "page" }
  }
}

The payment: PaymentPayload

The client answers with a PaymentPayload in the PAYMENT-SIGNATURE header.

x402VersionnumberRequired
Echoes the version it was given.
acceptedPaymentRequirementsRequired
The offer the client chose, copied back unchanged. The server refuses a payment whose accepted differs from what it offers, with No matching payment requirements.
payloadobjectRequired
The proof of payment. Its shape belongs to the scheme. For z402 it is a voucher: sessionKey, cumulativeZat, sig and, while a deposit is not yet acknowledged, depositTxid.
resourceobject
The resource of the offer, echoed.
extensionsobject
The extensions of the offer, echoed.
PAYMENT-SIGNATURE, decoded
{
  "x402Version": 2,
  "payload": {
    "sessionKey": "7dc02259...2609",
    "cumulativeZat": "1000",
    "sig": "76b6ee6c...4a03",
    "depositTxid": "cd266215...0a50"
  },
  "accepted": { "scheme": "prepaid-voucher", "network": "zcash:regtest", "amount": "1000", "...": "as offered" },
  "resource": { "url": "https://markdown.example.com/v1/markdown" },
  "extensions": { "...": "as offered" }
}

The hex values are shortened. A missing or undecodable PAYMENT-SIGNATURE is not an error to the server: it counts as no payment and gets a fresh 402.

The facilitator

A facilitator exposes three routes to resource servers.

RouteAuthWhat it does
GET /supportedPublic (a key that is presented must belong to a provider, or the answer is 401)Lists what the facilitator can settle: kinds (version, scheme, network, extra), extensions and signers.
POST /verifyAPI keyChecks a payment against an offer. Read-only: it must not commit payment state. Answers isValid, with invalidReason and invalidMessage when not.
POST /settleAPI keyCommits the payment. Answers success, transaction and network, with errorReason and errorMessage when not.

A resource server reads /supported when it starts, and it refuses to start a route whose exact scheme and network the facilitator does not list. For that reason the facilitator starts before the API.

GET /supported
{
  "kinds": [
    {
      "x402Version": 2,
      "scheme": "prepaid-voucher",
      "network": "zcash:regtest",
      "extra": {
        "windowZat": "100000",
        "creditRatioBps": 2500,
        "creditCapZat": "200000",
        "zeroConfMinExpiryBlocks": 20,
        "termsVersion": "cb7f148af0fda19a"
      }
    }
  ],
  "extensions": ["bazaar"],
  "signers": { "zcash:*": [] }
}

signers is empty because a z402 facilitator signs nothing on chain. The extra of a kind holds the terms that apply to every provider on the rail.

Once it accepts the API key, the z402 facilitator answers every /verify and /settle with HTTP 200, success or not, and puts the outcome in the body. A non-200 answer would leak its body into the client's error, and a /settle answer without transaction makes the stock server return 502.

What the client sees

What happenedWhat the client gets
Paid and settled200, the handler's body, and PAYMENT-RESPONSE with success: true.
Verify refused402 and a new PAYMENT-REQUIRED whose error is the facilitator's invalidReason.
Settle refused402 and PAYMENT-RESPONSE with success: false. The handler's output is discarded.
The handler failed with a status of 400 or moreThe handler's response. Nothing is settled.
The facilitator timed out502 with neither header.
The facilitator could not be reached402 whose error is the network error text, such as fetch failed.
PAYMENT-SIGNATURE missing or undecodableA fresh 402 with error: "Payment required".
accepted does not match any offer402 with error: "No matching payment requirements".

The stock fetch wrapper returns a refused call as a 402 response. It does not throw, so a loop must check res.status. The wrapper makes one paid attempt, plus one recovery attempt when the scheme's hook asks for it. It never makes a third.

agent.ts
import { decodePaymentResponseHeader } from "@x402/core/http";
import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
import { LightwalletdTxStatus, PrepaidVoucherScheme, ZalletRpcWallet } from "@z402-net/client";
import { createLightwalletdClient } from "@z402-net/lightwalletd";
import { parseSessionSnapshot } from "@z402-net/protocol";

// The endpoints and credentials of the local Z3 regtest stack
const lwd = createLightwalletdClient({ url: "http://127.0.0.1:28137" });
const scheme = new PrepaidVoucherScheme({
  wallet: new ZalletRpcWallet({ url: "http://127.0.0.1:50232", user: "zebra", password: "zebra" }),
  chain: new LightwalletdTxStatus(lwd),
});

const client = new x402Client();
client.register("zcash:regtest", scheme); // the one line that adds Zcash
const paidFetch = wrapFetchWithPayment(fetch, client);

const res = await paidFetch("http://127.0.0.1:4021/chunk");
const receipt = res.headers.get("PAYMENT-RESPONSE");
if (res.status === 200 && receipt !== null) {
  // The receipt carries the session's state next to the outcome
  const settled = decodePaymentResponseHeader(receipt);
  const session = parseSessionSnapshot(settled.extra?.["session"]);
  console.log(`paid ${settled.amount} zat, ${session?.headroomZat} zat of headroom left`);
} else {
  console.log(`refused with ${res.status}`);
}

await scheme.close();
lwd.close();

What z402 adds

x402 asks forz402 chooses
A scheme id that names the semanticsprepaid-voucher
A network id in CAIP-2 formzcash:regtest, zcash:testnet, zcash:mainnet. The syntax is valid CAIP-2, but no zcash namespace is registered, so the ids are provisional.
An asset and an amount in its smallest unitZEC and zat. One ZEC is 100,000,000 zat.
A recipientA shielded-only unified address. Shielded payments covers addresses.
Scheme-specific terms in extraThe deposit window, the credit rules and a terms version.
Scheme-specific proof in payloadA signed cumulative voucher, plus the txid of a deposit when one is not yet acknowledged.
A payer in verify and settle answersThe session key. A shielded payer has no address to show.

Three differences matter when you read the SDK's docs for other schemes.

  • Settle moves no money. The money moved earlier, in the agent's deposit. Settle credits that deposit once, records the voucher, and returns the session's state.
  • The client keeps state. The scheme remembers a session per provider, serializes calls on it, and decides when to deposit. The stock client only hands it the offer and the outcome.
  • A refusal carries data. When verify refuses a voucher, the server scheme adds the session's state to the new 402, as extra.session on the offer. The client reads it to resync and to decide what to do next.

The stock client's spend controls reject an asset that its scheme cannot price. The z402 scheme implements findDefaultAsset for ZEC, so the stock client accepts it.

z402 pins @x402/core, @x402/fetch, @x402/express and @x402/extensions to exactly 2.28.0.

References

  • docs/research/x402-spec.md: the v2 objects, header encodings, status codes and facilitator duties, checked against SDK 2.28.0.
  • docs/spec/protocol.md: sections 2, 3, 7 and 8, the z402 offer, payload, answers and codes.
  • packages/protocol/src/constants.ts: the scheme id, the network ids and the asset.
  • packages/server/src/scheme.ts: the server scheme and the corrective 402.
  • packages/client/src/scheme.ts: PrepaidVoucherScheme and findDefaultAsset.
  • apps/api/src/app.ts: a resource server with one paid route, GET /chunk.
  • apps/agent/src/agent.ts: the stock client and the one register line.
  • x402 specification v2: the objects and headers.
  • x402 HTTP transport: header names, encodings and status codes.
  • RFC 9110, 402 Payment Required: the HTTP status code.
  • CAIP-2: the network id format.