Skip to Content

x402

x402  is an open, HTTP-native payments protocol from the x402 Foundation. The protocol piggybacks on the HTTP 402 Payment Required status code: a resource server challenges a client with one or more payment options, the client submits a signed payment payload, and the server returns the resource along with a settlement receipt. The most common deployment today is USDC payments on Base (mainnet or base-sepolia for development), but the protocol is network-agnostic.

Keel supports x402 across both rungs:

  • Evidence: x402 challenge, payload, and settlement receipt can be bound into a signed permit at resource_attributes_json.x402 for tamper-evident audit.
  • Runtime: Keel’s managed-path dispatcher drives the full HTTP 402 → pay → retry → receipt → bind sequence under one permit. Your app calls POST /v1/execute; Keel handles the rail.

Use this guide when your application wants to:

  • Pay for an x402-protected resource through Keel’s managed-path runtime, OR
  • Bind a self-executed x402 payment into Keel for audit and policy adjudication.

Keel does not custody payer wallet keys for production runtime today. The managed-path runtime is appropriate when Keel holds a delegated key for a fixed wallet under your control, or when you bind self-executed x402 evidence and let Keel adjudicate the policy decision.

Architecture

Customer app | | 1. POST /v1/execute with action_verb=mpp.payment, | target URL, x402 spend authority, requested amount v Keel | | 2. Decide against policy and spend authority | 3. GET <target URL> on the resource server v Resource server | | 4. Return 402 Payment Required with PaymentRequirements | (scheme, network, amount, asset, payTo, ...) v Keel | | 5. Construct signed PaymentPayload with the | delegated x402 wallet | 6. Retry: GET <target URL> with X-PAYMENT header v Resource server | | 7. Verify with facilitator, settle on-chain, | return resource + X-PAYMENT-RESPONSE v Keel | | 8. Bind PaymentRequired, PaymentPayload, and | SettlementResponse into resource_attributes_json.x402 | 9. Substrate-v6 binding hash recursively covers | the x402 evidence — tamper-evident on verify v Customer app

The trust domains stay separate:

DomainOwnerKeel role
Wallet credentialsYour app or Keel-held delegated key (your choice)Either never sees the key (self-executed evidence path), or holds a delegated key bounded by Keel spend authority (runtime path).
Spend decisionKeel permit policyDecide whether this agent may spend this much on this resource.
Settlement railResource server + facilitator (Coinbase, Cloudflare, or self-hosted) + the underlying network (Base, Solana, etc.)Record rail outcome and bind the receipt evidence.
Audit trailKeel + the on-chain transactionPreserve tamper-evident audit evidence for review.

Prerequisites

  • An x402-protected resource server endpoint, or a sandbox facilitator (the x402 Foundation runs a public facilitator suitable for development).
  • A wallet on the target network with funds in the required asset (USDC on base-sepolia is the standard development setup).
  • A Keel project on Production or Enterprise. See Plans & Entitlements.
  • A client-scoped Keel API key for POST /v1/execute.

Set your Keel environment:

export KEEL_BASE_URL="https://api.keelapi.com" export KEEL_API_KEY="keel_sk_your_project_key"

For the managed-path runtime, also configure the delegated wallet key as a Keel project secret (never embed it in client code). See the Keel dashboard for delegated-key configuration.

Quick start

The example below shows the evidence path: your app or agent executes the x402 payment, then sends the captured evidence to Keel for decision and binding.

Python

pip install keel-sdk httpx eth-account
import httpx from eth_account import Account from keel_sdk import KeelClient client = KeelClient() target_url = "https://merchant.example/x402/report.pdf" wallet = Account.from_key("0x...your-wallet-private-key...") # 1. Probe the resource server for the 402 challenge. challenge_response = httpx.get(target_url) assert challenge_response.status_code == 402 challenge = challenge_response.json() # 2. Pick the payment option (this example: exact-USDC-on-base-sepolia). accepted = next( option for option in challenge["accepts"] if option["scheme"] == "exact" and option["network"] == "base-sepolia" ) # 3. Construct and sign the PaymentPayload off-the-shelf. # (See x402 SDK examples for the full signing flow; abbreviated here.) payment_payload = build_and_sign_x402_payment( accepted=accepted, resource=challenge["resource"], wallet=wallet, ) # 4. Retry with the X-PAYMENT header to settle the payment. paid_response = httpx.get( target_url, headers={"X-PAYMENT": payment_payload["headerB64"]}, ) paid_response.raise_for_status() settlement_header = paid_response.headers["X-PAYMENT-RESPONSE"] # 5. Send the x402 evidence to Keel for decision and binding. execution = client.execute( action_verb="mpp.payment", spend_authority={ "amount_max": 10000, # 0.01 USDC in atomic units, for example "currency_class": "usd_stablecoin", "cadence": "single_use", "ttl_seconds": 300, }, rail_evidence={ "x402": { "protocol": "x402", "protocol_version": 2, "transport": "http", "payment_required": challenge, "payment_payload": payment_payload["payload"], "settlement_response": parse_x402_response(settlement_header), } }, target_url=target_url, ) print(execution.primary_outcome) # "allowed" if policy passed print(execution.permit_id) # Keel permit binding the x402 evidence

Branch on execution.primary_outcome ("allowed" / "denied" / "challenged"), not only HTTP status.

For the managed-path runtime, your app skips steps 1–4 and lets Keel drive the rail. Pass the target URL and spend authority; Keel returns the bound permit when settlement completes.

Request shape

The x402 rail uses the same POST /v1/execute envelope as Stripe MPP, with the rail evidence carried at resource_attributes_json.x402.

{ "action_verb": "mpp.payment", "target_url": "https://merchant.example/x402/report.pdf", "spend_authority": { "amount_max": 10000, "currency_class": "usd_stablecoin", "cadence": "single_use", "ttl_seconds": 300 }, "resource_attributes": { "x402": { "protocol": "x402", "protocol_version": 2, "transport": "http", "payment_required": { "x402Version": 2, "error": "payment required", "resource": { "url": "https://merchant.example/x402/report.pdf", "description": "Quarterly report", "mimeType": "application/pdf" }, "accepts": [ { "scheme": "exact", "network": "base-sepolia", "amount": "10000", "asset": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "payTo": "0x1111111111111111111111111111111111111111", "maxTimeoutSeconds": 60, "extra": { "name": "USDC", "decimals": 6 } } ] }, "payment_payload": { "x402Version": 2, "resource": { "url": "https://merchant.example/x402/report.pdf" }, "accepted": { "scheme": "exact", "network": "base-sepolia", "amount": "10000", "asset": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "payTo": "0x1111111111111111111111111111111111111111" }, "payload": { "from": "0x2222222222222222222222222222222222222222", "authorization": "0xauthorization", "signature": "0xsignature" } }, "settlement_response": { "success": true, "errorReason": null, "transaction": "0xabc...", "network": "base-sepolia", "payer": "0x2222222222222222222222222222222222222222" } } } }

X402Evidence reference

The structure under resource_attributes_json.x402 mirrors the x402 wire protocol so any third-party verifier can re-derive the payment from the captured payload.

FieldTypeDescription
protocolstringAlways "x402".
protocol_versionintegerx402 protocol version (currently 2).
transportstringWire transport ("http" for standard HTTP 402; reserved for future transports).
payment_requiredobjectThe original 402 challenge returned by the resource server, including the accepts array of PaymentRequirements.
payment_payloadobjectThe signed PaymentPayload submitted on the retry request.
settlement_responseobjectThe receipt returned by the resource server after settlement (parsed from X-PAYMENT-RESPONSE).

The full schemas for each nested object are published in openapi.json under X402Evidence, X402PaymentRequired, X402PaymentRequirements, X402ResourceInfo, X402PaymentPayload, and X402SettlementResponse.

Spend authority

x402 uses the same spend authority shape as Stripe MPP, with currency_class extended to cover stablecoins and on-chain assets:

FieldNotes
amount_maxAtomic units in the asset’s base denomination (e.g. for USDC with 6 decimals, 10000 = 0.01 USDC).
currency_class"usd_stablecoin" for USDC, "usd_fiat" for fiat-denominated rails, others as defined.
cadence"single_use" for one-shot purchases; "recurring" for subscriptions.
ttl_secondsBounded; Keel rejects authorities older than the TTL at execution time.

Response shape

The POST /v1/execute response uses the standard execution envelope. For successful x402 payments:

{ "execution_id": "ex_x402_001", "permit_id": "perm_x402_001", "primary_outcome": "allowed", "rail_outcome": "paid", "binding_version": "v6", "binding": { "binding_canonical_hash": "sha256:...", "resource_attributes_canonical_hash": "sha256:..." }, "provider_attestation": { "settlement_network": "base-sepolia", "transaction_hash": "0xabc...", "payer": "0x2222...", "amount_paid": "10000", "asset": "0xeeee..." } }
  • primary_outcome reflects the policy decision ("allowed", "denied", "challenged").
  • rail_outcome reflects the settlement state ("paid", "failed", "pending").
  • The substrate-v6 binding hash recursively covers all rail evidence. Any post-issuance change to resource_attributes_json.x402 is detectable on verification.

Error handling

ScenarioBehavior
Resource server does not return 402Keel does not infer payment is required. Your app must initiate the x402 flow explicitly.
Policy denies the paymentprimary_outcome: "denied". No settlement is attempted. The denial decision is bound into the permit.
Policy requires step-upprimary_outcome: "challenged". The execution holds; settle the challenge before retrying.
Settlement failsrail_outcome: "failed" with provider_attestation.error_reason. The permit still records the attempted payment for audit.
Settlement times outrail_outcome: "pending". Use the reconciliation API to query for the final state.

Audit trail

The signed permit binds the x402 evidence into the v6 resource_attributes_canonical_hash. To verify offline:

  1. Export the permit through Signed Exports.

  2. Install keel-verifier:

    pip install keel-verifier keel-verify export <export-file> --json
  3. The verifier recomputes resource_attributes_canonical_hash from the raw resource_attributes_json and compares it to the signed value. Any tampering with the x402 payment payload, settlement response, or any other rail evidence is detected as permit.binding.v6.resource_attributes_canonical_hash_mismatch.

You can also verify with any RFC 8785 implementation in any language — no Keel-specific code is required. See Independent Verification.

FAQ

Does Keel support networks other than Base?

The x402 protocol is network-agnostic. Keel’s evidence path accepts any network the x402 spec defines (base, base-sepolia, Solana, Stellar, future EVM networks). The runtime path is currently optimized for Base (mainnet + sepolia); other networks are supported on the evidence path and runtime support follows customer signal.

What schemes are supported?

Today: exact (the most common, used for single-purchase pricing). The x402 protocol defines upto and batch-settlement schemes; Keel’s evidence path accepts them, and runtime support tracks customer demand.

Do I have to use a public facilitator?

No. The facilitator handles payment verification and settlement on the resource server’s side. Public facilitators (Coinbase, Cloudflare) are convenient for development; production deployments often self-host. Keel’s evidence path is facilitator-agnostic; the settlement_response shape is the same regardless.

Can I combine x402 with other Keel features?

Yes. x402 permits compose with Budget Envelopes, Parent-Child Verb Delegation, Workflow Intent, and the rest of the Keel substrate identically to Stripe MPP. The mpp.payment action verb is shared.

Is x402 evidence tamper-evident even after Keel signs it?

Yes. Substrate-v6 binds the full resource_attributes_json mapping (including resource_attributes_json.x402) into the signed permit binding hash. Any change to any nested field of the x402 evidence is detectable on independent verification.

Last updated on Edit this page on GitHub