Skip to content

· skunxicat

On this page

One Request, One Price: x402 and the Payment Layer HTTP Never Shipped

Two payers, an agent and a wallet, converge on one request; a 402 challenge and a signed retry lead to a single settlement

In January 1997, RFC 2068 defined HTTP/1.1 and gave status code 402 Payment Required a single sentence: “This code is reserved for future use.” Twenty-five years later RFC 9110, the current HTTP semantics, says the same thing in almost the same words. The slot was kept; nothing was ever built behind it.

So the web built around it. Card processors, checkout pages, accounts, API keys, monthly plans. They work because a person can sign up, type a card number and approve a purchase. Marc Andreessen has called the missing payment layer one of the internet’s original sins, and the workarounds are the reason it stayed tolerable: humans absorb the friction.

Software doesn’t. An agent may make hundreds of calls to finish one task, each worth a fraction of a cent, and it has no one to fill in a signup form. Card rails have a fixed cost per transaction that is larger than the payment itself at that size. This is the gap x402 fills: it makes 402 mean what it was reserved to mean.

This post is three things: how I think about x402 after building with it, a live reference implementation you can pay with an agent or a browser, and a Terraform module that adds x402 to paths of an existing CloudFront distribution.

What x402 is

An unpaid request gets a 402 with a PAYMENT-REQUIRED header describing the terms. The client signs a payment authorization and retries with a PAYMENT-SIGNATURE header. The server verifies it, serves the resource, settles the payment, and returns 200 with a PAYMENT-RESPONSE header carrying the settlement evidence.

GET /v1/lambda-runtimes            -> 402  PAYMENT-REQUIRED: {scheme, network, asset, amount, payTo}
GET /v1/lambda-runtimes
    PAYMENT-SIGNATURE: <signed>    -> 200  PAYMENT-RESPONSE: {success, payer, transaction}

That is the whole protocol at the HTTP level, and its shape says a lot about what it is for.

It prices an invocation, not an invoker. There is no account on either side. The server doesn’t know who you are and doesn’t need to; it knows this request carries a valid payment for this resource. The commercial relationship starts with the request and ends with the response. Nothing is remembered tomorrow: no balance, no plan, no entitlement. I’ve come to call this transaction-native rather than account-native, and it is the property that matters. x402 is not a subscription with the login page removed.

The 402 round trip is optional. The challenge exists so a client can learn the terms. A client that already knows them, because the server publishes them as discovery metadata, can send PAYMENT-SIGNATURE on its first request and never see a 402. The negotiation is there for discovery, not as a mandatory toll booth.

The payer signs; someone else submits. With the exact scheme on EVM chains, the payment is an EIP-3009 transfer authorization: typed data the payer signs off-chain. A facilitator verifies it and submits the transfer on-chain. The payer never broadcasts a transaction and needs no ETH.

That last point gets oversold as “gasless”, so it deserves precision. Settlement always costs gas; the payer just isn’t the one paying it. The facilitator fronts it, and that is a service with a price. The one this implementation uses, PayAI, at the time of writing has a free tier of 1,000 settlements in total, then charges $0.001 per settlement. At a $0.001 resource price, that fee is the entire price. You can run your own facilitator instead, and then you pay the gas and own the operations: submission, monitoring, reconciliation, a funded wallet. Micropayments are viable, but their economics live in the settlement layer, not in the protocol, and it is worth knowing which line of your costs that is.

What x402 is not

The useful way to understand a protocol is by the jobs it refuses.

It is not identity. x402 answers has this invocation been paid for. It says nothing about who made the request. That is a different primitive: Web Bot Auth, where a request carries a signature verifiable against keys the signer publishes. A request can be signed and unpaid, or paid and anonymous. Payment is not authentication, and authentication is not evidence of payment.

It is not composition. It is tempting to wire the two together: require a signature, then a payment, and bind the payment into the signed request so it can’t be replayed elsewhere. I planned exactly that, and stopped. x402 is one atomic, self-contained transaction by design; binding identity to payment is a protocol concern, and protocols are appearing that do it natively (MPP’s mppx verifies agent identity across HTTP requests and payment retries). Hand-rolling the binding onto raw x402 would be solving the problem in the wrong layer. Keeping the two primitives separate and proven on their own is what makes it possible to evaluate a composed protocol honestly later.

It is not fair exchange. x402 can prove money moved. It cannot prove the server returned something worth paying for; a paid 200 with a useless body is still a paid 200. At these prices the buyer’s recourse is the old one: don’t call that endpoint again.

It is not a toll on the open web. This is the one I had to reason my way to. The obvious idea is to put a price on content crawlers take: return 402 to automated requests for an article. It doesn’t work. A 402 on a URL that robots.txt allows and a sitemap lists is, to every indexer, an error on a page it was invited to fetch. The page drops out of search for everyone, not only the crawler you meant to charge. A single URL cannot be both freely discoverable and tolled.

The honest shape keeps two channels apart. The crawler channel (sitemap.xml, robots.txt) lists only free URLs. The agent channel (/.well-known/x402, an OpenAPI document with payment terms, llms.txt) advertises what can be bought. An agent that wants a paid resource finds the terms there and pays on its first request. The server never ambushes anyone with a 402 on a page it advertised as crawlable; payment is something the client chooses to engage.

A reference implementation: two ways to pay

x402.cloudless.sh is a live x402 v2 server built to demonstrate the protocol end to end. It is deliberately not a product: the resources are small, the point is that both kinds of payer work against the same protocol.

A machine pays an API for data. GET /v1/lambda-runtimes returns the currently supported AWS Lambda runtimes, parsed from the AWS documentation by a shell-first Lambda, for $0.001 in USDC on Base mainnet. No human in the loop:

import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.BUYER_PRIVATE_KEY);
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(account));
const pay = wrapFetchWithPayment(fetch, client);

const res = await pay("https://x402.cloudless.sh/v1/lambda-runtimes");
const runtimes = await res.json(); // ["nodejs26.x", "python3.15", ...]

A human pays for content from a browser wallet. The market report page sells a PDF for the same price. You click pay, MetaMask (or any EIP-1193 wallet) asks you to sign the authorization, and the PDF downloads once settlement succeeds. No account, no checkout. The finding I didn’t expect: the browser runs the same @x402/* SDK the agent uses, bundled for the browser, with the wallet as the signer instead of a private key. There is no separate “web payments” path; it is one protocol with two signers. The page also sets a client-side spend cap, so a misconfigured server can’t ask the wallet for more than a cent.

The two routes are also an architecture comparison. The agent route is gated at the CloudFront edge by Lambda@Edge, around an origin that knows nothing about payments. The report route is a regional Lambda that does the whole lifecycle in one handler. The edge wraps payment around anything; the regional origin gets environment variables, simpler state and a body it controls. Which is better depends on the resource, and having both live makes the trade-off concrete instead of theoretical.

The machine-readable side is published the way the two-channel rule says it should be. The sitemap lists the free pages. /.well-known/x402, /openapi.json and llms.txt advertise the paid routes and their terms. Independent indexers found and verified the server on their own: it is listed on x402scan, with an ownership proof that ties the receiving wallet to the domain, and on x402lens. For honesty about those listings: the settlement counts they show are almost all my own test payments. The listings are evidence of participation, not of demand.

The settlements themselves are public. Two recent ones, $0.001 each, buyer to seller on Base:

The edge pattern

The edge route follows the x402-foundation CloudFront example, which maps the protocol onto CloudFront’s request lifecycle:

viewer -> CloudFront, paid behavior (no caching)
            origin-request  : no or invalid payment -> 402 + PAYMENT-REQUIRED
                              valid payment         -> forward to the origin
            origin          : your backend, unchanged
            origin-response : origin < 400          -> settle, add PAYMENT-RESPONSE
                              origin >= 400         -> pass through, no charge

Verification happens before the origin is called; settlement happens only after the origin succeeds. The client is never charged for a request that failed, and the origin never learns it is behind a paywall. That separation is the appeal: payment becomes something you wrap around an existing resource, not something you build into it.

It also comes with a set of facts that are easy to get wrong, each of which fails quietly:

  • Lambda@Edge has no environment variables. The facilitator, network, payee, price and routes have to reach the function some other way.
  • Caching must be disabled on paid paths. The origin-request function only runs on a cache miss. A cached paid response is served without anyone checking for payment.
  • payment-signature has to be forwarded. The function only sees headers the origin request policy passes along. Forget it and every paid request looks unpaid.
  • The advertised host has to be set. The function only knows the *.cloudfront.net name, not your domain, so the challenge would point clients at the wrong URL.
  • Functions live in us-east-1, run x86_64, and are referenced by version. Deleting one that CloudFront still uses fails until the replicas are gone, which can take hours.

I got each of these wrong at least once. That is the argument for a module.

The module: x402 for an existing distribution

ql4b/x402-edge/aws packages the edge pattern so it plugs into a distribution you already have. It does not create a distribution. It creates the two edge functions and the policies a paid path needs, and hands you outputs to attach to the behaviors you want to charge for:

module "x402" {
  source    = "ql4b/x402-edge/aws"
  version   = "~> 1.0"
  providers = { aws = aws.us_east_1 }

  facilitator_url = "https://facilitator.payai.network"
  network         = "eip155:8453" # Base mainnet
  pay_to          = "0xYourReceivingWallet"
  public_host     = "api.example.com"

  routes = {
    "/api/premium/*" = {
      price       = "$0.01"
      description = "Premium data, paid per request"
      mime_type   = "application/json"
    }
  }

  namespace = "myorg"
  name      = "api"
}

# In your distribution:
ordered_cache_behavior {
  path_pattern             = "/api/premium/*"
  target_origin_id         = "api"
  viewer_protocol_policy   = "https-only"
  allowed_methods          = ["GET", "HEAD", "OPTIONS"]
  cached_methods           = ["GET", "HEAD"]
  cache_policy_id          = module.x402.cache_policy_id
  origin_request_policy_id = module.x402.origin_request_policy_id

  dynamic "lambda_function_association" {
    for_each = module.x402.lambda_function_associations
    content {
      event_type   = lambda_function_association.value.event_type
      lambda_arn   = lambda_function_association.value.lambda_arn
      include_body = lambda_function_association.value.include_body
    }
  }
}

The origin behind that behavior can be anything: S3, an ALB, a Function URL. It stays unaware of payments.

The part I’m most pleased with is how it gets around the missing environment variables. The module ships one prebuilt, generic handler. Your inputs are rendered into a config.json and added to the deployment zip next to it at plan time. There is no build step on your side, and changing a price, a payee or a route is an ordinary terraform apply that publishes a new function version. The handler refuses to start if the config is missing or invalid, rather than letting requests through unpaid.

The rules from the list above are built into the module instead of left to the reader. The cache policy it outputs is CachingDisabled. The origin request policy it creates always forwards payment-signature, and nothing else unless you add headers. The plan fails if the provider isn’t us-east-1, if the network isn’t an EVM chain the handler supports, or if a route pattern or payee address is malformed. Routes use x402’s own patterns, so /api/*, /items/[id] and GET /v1/data all work.

It isn’t a lab artifact. Before tagging 1.0.0 I moved x402.cloudless.sh onto the module: an in-place switch, nothing destroyed, the existing functions adopted rather than replaced. The live 402 challenge was identical, field for field, before and after, and the first paid request through it settled on Node.js 22 (the second transaction above). Source and maintainer notes are at ql4b/terraform-aws-x402-edge, with a deployable example that puts one paid path in front of httpbin.org on Base Sepolia.

What’s still open

  • Safe retries. The edge is stateless: it settles every successful origin response. A client that retries the same purchase pays twice. Fixing that needs an operation journal reachable from every edge location, which is real distributed-systems work, not something to bolt on quietly.
  • Discovery from one source. The module renders the 402, but the OpenAPI and /.well-known/x402 documents are still maintained separately. Generating them from the same routes is the next feature, so the challenge and the catalog can’t drift.
  • Identity and payment together. Evaluating a protocol that binds the two natively, against primitives already proven on their own, is the next experiment on this track.
  • Fragmented discovery. Indexes are tied to facilitators. A payment settled through one facilitator shows up in some directories and not others. That is a property of the ecosystem today, and worth knowing before you choose a facilitator.

Try it

curl -i https://x402.cloudless.sh/v1/lambda-runtimes    # 402 and the terms

Pay it from code with @x402/fetch, or open the market report page and pay from a wallet. To put a price on a path of your own distribution, start with the module and its example.

HTTP kept 402 reserved for almost thirty years. It turns out the missing piece wasn’t a status code, it was a way for a client that has never met a server to pay it for one response and walk away. That piece exists now. If you’re building on it, or want to compare notes on running it at the edge, write to hello@cloudless.sh.