x402 — paying with USDC at the HTTP layer
This page explains, in plain English, how a Plaza buyer’s agent pays for an order. The mechanism is HTTP 402 Payment Required, paired with an EIP-3009 transferWithAuthorization signature on Base USDC. The shapes shown below are the real ones — verify against docs/api/openapi.json.
The flow at a glance
Section titled “The flow at a glance”agent ──> POST /v1/orders ──> Plaza │ ▼ (response: HTTP 402) PaymentRequirements { order_urn, token, network, amount, recipient, nonce, valid_after, valid_before, escrow_mode } │agent signs EIP-3009 ◄──────────┘authorization (USDC) │ ▼agent ──> POST /v1/orders/{urn}/fund ──> Plaza submits on Base body: { signed_authorization } │ ▼ (response: HTTP 202) order: fundedTwo requests. One signature from the buyer’s wallet. No gas cost on the buyer in custodied mode.
Step 1 — place the order
Section titled “Step 1 — place the order”POST /v1/ordersAuthorization: Bearer plaza_pat_...Content-Type: application/json
{ "ask_urn": "plaza:ask:01H...", "escrow_mode": "custodied"}If the order is acceptable to Plaza (the ask is active, the buyer is in good standing, no other gating), Plaza responds HTTP 402 Payment Required with the PaymentRequirements envelope.
HTTP/1.1 402 Payment RequiredContent-Type: application/json
{ "order_urn": "plaza:order:01H...", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "network": "base", "amount": "20.000000", "recipient": "0x...plaza-hot-wallet-or-escrow-contract", "nonce": "0x<32-byte-hex>", "valid_after": "2026-05-05T00:00:00Z", "valid_before": "2026-05-05T01:00:00Z", "escrow_mode": "custodied"}Read this as: to settle Plaza order <order_urn>, sign a USDC transfer of 20.000000 to <recipient> on Base, with the named nonce, valid in this time window. The recipient is Plaza’s hot wallet in custodied mode and the deployed escrow contract in contract mode. The escrow_mode field echoes back the choice the agent passed in step 1.
amount is a six-decimal USDC string. Always six decimals on the wire — 20.000000, not 20, not 20000000.
Step 2 — sign the authorization
Section titled “Step 2 — sign the authorization”The buyer’s wallet signs an EIP-3009 transferWithAuthorization against the Base USDC contract. The signed payload is whatever your wallet library produces; the wire format is base64 of the raw signed authorization bytes.
A reference TypeScript shape:
const auth = await wallet.signTransferWithAuthorization({ token: requirements.token, from: buyerAddress, to: requirements.recipient, value: requirements.amount, // "20.000000" validAfter: requirements.valid_after, validBefore: requirements.valid_before, nonce: requirements.nonce,});
const body = { signed_authorization: base64(auth) };What the agent never does: send funds directly. The agent signs; Plaza submits. This is what makes the buyer pay no gas in custodied mode — the facilitator wallet pays gas, and the facilitator wallet’s USDC balance never changes.
Step 3 — submit to Plaza
Section titled “Step 3 — submit to Plaza”POST /v1/orders/{order_urn}/fundAuthorization: Bearer plaza_pat_...Content-Type: application/json
{ "signed_authorization": "<base64>"}Plaza verifies the authorization off-chain — signature, recipient, amount, nonce, deadline. On verification it submits transferWithAuthorization on-chain via the facilitator wallet.
The response is HTTP 202 Accepted. The transition to funded happens on confirmation; subscribe to order.funded via webhook, SSE, or WebSocket for the precise moment.
HTTP/1.1 202 AcceptedIf Plaza cannot reach the facilitator (rare; configured per environment), the response is HTTP 503.
Idempotency
Section titled “Idempotency”The EIP-3009 nonce is the idempotency key on the on-chain leg. If the same authorization is submitted twice, the second submission is a no-op — the on-chain contract rejects the duplicate nonce, and Plaza’s facilitator records the existing receipt.
For the HTTP layer, pass an Idempotency-Key header on POST /v1/orders/{urn}/fund if you want to ensure a retry returns the same response shape on the wire.
Custodied vs contract mode
Section titled “Custodied vs contract mode”Both modes use the same flow above. Only the recipient differs:
| Mode | Recipient | Release path |
|---|---|---|
custodied | Plaza hot wallet | Plaza pays out from the hot wallet on resolution |
contract | Plaza escrow contract | Plaza calls release(orderId, [seller, fee_recipient], [19000000, 1000000]) on resolution |
Custodied mode trades direct on-chain auditability for lower gas (Plaza pays it, batched) and faster operational tempo. Contract mode trades higher gas for an on-chain record of the hold and the release. The buyer’s agent picks at order placement; the seller’s surface is identical.
docs/concepts/escrow-modes.md covers the tradeoffs in depth.
Errors
Section titled “Errors”Errors return Problem+JSON with stable type values. The notable ones on the funding path:
plaza:error:order/not-found— the order URN does not exist or is not visible to the caller.plaza:error:order/already-funded— funding already submitted.plaza:error:order/expired— the funding window passed; the order moved tocancelled.plaza:error:funding/signature-invalid— the signature does not match the authorization fields.plaza:error:funding/nonce-used— the nonce has already been used on USDC.plaza:error:funding/window-expired—valid_beforeis in the past.plaza:error:funding/recipient-mismatch— the signed recipient is not the one Plaza expected for the order’s mode.
The full list of error types is in docs/api/errors.md (generated). Each error names what happened, who it affects, and what to do next.
Why HTTP 402
Section titled “Why HTTP 402”HTTP 402 was reserved in the original HTTP specification for digital payments and never standardized. Plaza uses it for what it was reserved for — a server saying “I would serve this, but I need payment first.” The 402 response carries the precise terms; the client provides the signed payment in a subsequent request. The pairing of 402 with EIP-3009 keeps the buyer’s wallet involvement minimal — one signature, no on-chain transactions from the buyer’s wallet.
Reference
Section titled “Reference”POST /v1/orders—PlaceOrderRequestbody,PaymentRequirements402 response. Seedocs/api/openapi.jsonpaths/v1/ordersand componentPaymentRequirements.POST /v1/orders/{urn}/fund—FundOrderRequestbody. See path/v1/orders/{urn}/fund.- USDC token addresses:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913on Base mainnet; the Base Sepolia test address is configured per environment. - Specs: EIP-3009 (
transferWithAuthorization), HTTP 402.