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.x402for 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 appThe trust domains stay separate:
| Domain | Owner | Keel role |
|---|---|---|
| Wallet credentials | Your 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 decision | Keel permit policy | Decide whether this agent may spend this much on this resource. |
| Settlement rail | Resource server + facilitator (Coinbase, Cloudflare, or self-hosted) + the underlying network (Base, Solana, etc.) | Record rail outcome and bind the receipt evidence. |
| Audit trail | Keel + the on-chain transaction | Preserve 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-sepoliais 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-accountimport 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 evidenceBranch 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.
| Field | Type | Description |
|---|---|---|
protocol | string | Always "x402". |
protocol_version | integer | x402 protocol version (currently 2). |
transport | string | Wire transport ("http" for standard HTTP 402; reserved for future transports). |
payment_required | object | The original 402 challenge returned by the resource server, including the accepts array of PaymentRequirements. |
payment_payload | object | The signed PaymentPayload submitted on the retry request. |
settlement_response | object | The 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:
| Field | Notes |
|---|---|
amount_max | Atomic 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_seconds | Bounded; 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_outcomereflects the policy decision ("allowed","denied","challenged").rail_outcomereflects the settlement state ("paid","failed","pending").- The substrate-v6 binding hash recursively covers all rail evidence. Any post-issuance change to
resource_attributes_json.x402is detectable on verification.
Error handling
| Scenario | Behavior |
|---|---|
| Resource server does not return 402 | Keel does not infer payment is required. Your app must initiate the x402 flow explicitly. |
| Policy denies the payment | primary_outcome: "denied". No settlement is attempted. The denial decision is bound into the permit. |
| Policy requires step-up | primary_outcome: "challenged". The execution holds; settle the challenge before retrying. |
| Settlement fails | rail_outcome: "failed" with provider_attestation.error_reason. The permit still records the attempted payment for audit. |
| Settlement times out | rail_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:
-
Export the permit through Signed Exports.
-
Install
keel-verifier:pip install keel-verifier keel-verify export <export-file> --json -
The verifier recomputes
resource_attributes_canonical_hashfrom the rawresource_attributes_jsonand compares it to the signed value. Any tampering with the x402 payment payload, settlement response, or any other rail evidence is detected aspermit.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.
Related pages
- Payment Rails Overview — all nine rails at a glance
- Stripe MPP — the other rail with evidence + runtime
- Independent Verification — verify rail evidence with any RFC 8785 implementation
- Execute — the substrate-level execute reference
- Permits — the permit primitive
- x402 Foundation — protocol reference