- No
planId— Returns HTTP 400 directing the client toGET /discover. - Challenge —
planIdin the body, noPAYMENT-SIGNATUREheader. Returns HTTP 402 with payment requirements and achallengeId. - Settlement (subscription) —
planIdin the body and aPAYMENT-SIGNATUREheader. Settles the payment on-chain and returns HTTP 200 with anAccessGrant(JWT). - Settlement (route-based standalone) — Same as (3) but with a
routeIdandresourcein the body. Key0 proxies to the backend and returns aResourceResponse(backend data, no token).
Case 1: No planId
Sending a body withoutplanId returns HTTP 400 with a pointer to the discovery endpoint.
- Request
- Response
Case 2: Challenge
Send aplanId in the body without a PAYMENT-SIGNATURE header to purchase a subscription. For standalone route calls, send routeId plus a resource field without a PAYMENT-SIGNATURE header. The server creates a PENDING challenge record and returns payment requirements.
- Request
- Response
Subscription plan:Route-based standalone call:
| Field | Type | Required | Description |
|---|---|---|---|
planId | string | Subscription only | Must match a Plan.planId from the seller’s catalog. |
routeId | string | Route-based only | Must match a Route.routeId from the seller’s route catalog. |
requestId | string | No | Client-generated UUID. Auto-generated if omitted. Used as an idempotency key. |
resourceId | string | No | Defaults to "default" for subscription flows. |
resource | { method, path } | Route-based only | Required for standalone route calls. Specifies the backend route to call after payment. |
Case 3: Settlement
Resend the same request with aPAYMENT-SIGNATURE header containing a base64url-encoded X402PaymentPayload. The server settles the payment on-chain and returns either an AccessGrant (subscription) or a ResourceResponse (route-based standalone).
- Request
- Response
PAYMENT-SIGNATURE header is a base64url-encoded JSON object with the following structure:accepted field echoes back the payment requirements from the 402 response. The payload.signature is the EIP-3009 transferWithAuthorization signature. The payload.authorization contains the EIP-3009 parameters.If
planId or routeId is missing from the request body but present in the PAYMENT-SIGNATURE payload’s accepted.extra, the server extracts it automatically. This supports standard x402 clients that replay the exact same request with only the header added.Settlement Strategies
Key0 supports two settlement strategies, configured viaSellerConfig:
| Strategy | Config | Description |
|---|---|---|
| Facilitator (default) | facilitatorUrl or network default | Sends the EIP-3009 authorization to the Coinbase facilitator for verification and on-chain settlement. The facilitator pays gas. |
| Gas Wallet | gasWalletPrivateKey | Verifies and settles the EIP-3009 authorization directly using your own gas wallet. You pay gas. Supports distributed locking via Redis for multi-instance deployments. |
Case 4: Route-Based Standalone Response
When settlement succeeds for a route-based call (withproxyTo or fetchResource configured), the response is a ResourceResponse containing the backend’s data — no JWT is issued:
PAID (awaiting refund) and the response includes the backend’s error body.
Pre-Settlement Check
Before settling a payment, the server checks for existing challenge records:- Already DELIVERED: Returns the cached
AccessGrantimmediately (HTTP 200). No on-chain transaction. - EXPIRED or CANCELLED: Returns an error (HTTP 410). The client must start a new flow.
- PENDING or PAID: Proceeds with settlement.
Error Responses
| HTTP Status | Error Code | When |
|---|---|---|
| 400 | TIER_NOT_FOUND | planId does not match any plan in the catalog. |
| 400 | INVALID_REQUEST | Malformed PAYMENT-SIGNATURE header or request body. |
| 400 | RESOURCE_REQUIRED | Route-based standalone request missing the resource field. |
| 402 | PAYMENT_FAILED | EIP-3009 verification or on-chain settlement failed. |
| 409 | TX_ALREADY_REDEEMED | The transaction hash was already used for a different challenge. |
| 410 | CHALLENGE_EXPIRED | The challenge expired or was cancelled before payment arrived. |
| 500 | INTERNAL_ERROR | Concurrent state transition conflict or unexpected error. |
A2A-Native Clients (Express only)
When using the Express integration, the samePOST /x402/access endpoint also handles A2A JSON-RPC requests. A2A-native clients (e.g. the Google ADK A2AClient) signal their intent by including the X-A2A-Extensions header in the request:
Key0Executor). The response conforms to the A2A protocol (task lifecycle with AccessRequest and payment metadata).
Header-based A2A routing is Express only. The Hono and Fastify integrations serve the standard x402 HTTP flow on
POST /x402/access only. A2A-native agents should use the Express integration or deploy the Key0 standalone Docker image.| Header | Value | Effect |
|---|---|---|
| (absent) | — | Standard x402 HTTP flow (discovery / challenge / settlement) |
X-A2A-Extensions | any value | Delegates to A2A JSON-RPC handler (Express only) |
Related
x402 HTTP Flow
End-to-end walkthrough of the x402 payment protocol over HTTP.
A2A Flow
How A2A-native agents discover and pay for access via Key0Executor.
Data Models
TypeScript types for X402PaymentPayload, X402SettleResponse, and more.
ChallengeEngine
The state machine that processes challenges created by this endpoint.

