http-x402
Implements the x402 payment protocol for API Gateway and Function URL handlers, serving protocol v2 and v1 clients side by side (configurable via versions). The version is detected per request from the payment header: v2 clients send PAYMENT-SIGNATURE, v1 clients send X-PAYMENT (v2 wins when both are present). The middleware verifies the payment with a facilitator, runs the handler, then settles the payment on-chain. An unpaid 402 challenges both generations at once: the v2 challenge travels in the PAYMENT-REQUIRED header and the v1 challenge in the JSON body. Settlement results are returned in PAYMENT-RESPONSE (v2) or X-PAYMENT-RESPONSE (v1) on success and failure alike.
After settlement, payer info is available via request.internal.x402 for downstream use (e.g. logging, rate-limiting per wallet).
Consider AWS WAF AI traffic monetization. If you front your handler with Amazon CloudFront, AWS WAF can now return the x402 HTTP 402 challenge and verify payment at the edge, before requests reach Lambda, with no application code. This middleware remains the right fit when you need per-request pricing in handler logic, access to payer info during execution, or you are not on CloudFront. See AWS WAF adds AI traffic monetization capability.
Install
npm install --save @middy/http-x402 @x402/core Options
price(number|string) (required unlessamountis set): Amount to charge in human-readable units (e.g.0.001for $0.001 USDC).amount(string) (optional): Payment amount as an integer string in atomic token units. Takes precedence overpriceanddecimals.payTo(string) (required): Wallet address that receives the payment.asset(string) (required): On-chain asset contract address (e.g. USDC on Base).versions(array of1 | 2) (default[1, 2]): Protocol versions to accept and advertise. A disabled version's payment header is ignored and the client is re-challenged with the enabled formats only, before any facilitator call. Pin[2]to reduce attack surface on new deployments. The default will drop1in the next major version; passversions: [1, 2]explicitly if you need v1 long term.FacilitatorClient(class) (defaultHTTPFacilitatorClientfrom@x402/core): Facilitator client class. Override for custom facilitators.facilitatorUrl(string) (default"https://x402.org/facilitator"): URL of the x402 facilitator service.decimals(integer) (default6): Asset decimal places used to convertpriceto on-chain units.network(string) (default"eip155:8453"): CAIP-2 chain ID, used verbatim for both protocol versions (matching@x402/core2.x). Default is Base mainnet.description(string) (default""): Human-readable description included in the payment requirements.mimeType(string) (default"application/json"): MIME type of the protected resource.extra(object) (optional): Scheme-specific data advertised in the payment requirements and sent to the facilitator, e.g.{ name: 'USDC', version: '2' }(the EIP-712 domain used by theexactscheme on EVM networks). The protocol-reserved keysextra.paymentFlowandextra.assetTransferMethodare rejected: only the authorization flow (verify before the handler, settle after) is implemented.human(function) (optional):(request) => boolean. Returntrueto bypass payment for this request (e.g. to let browser traffic through based onUser-Agent).
Sample usage
import middy from '@middy/core'
import httpX402 from '@middy/http-x402'
export const handler = middy()
.use(
httpX402({
price: 0.001,
payTo: '0xYourWalletAddress',
asset: '0xYourAssetAddress', // USDC on Base
}),
)
.handler(async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Paid content' }),
}
}) Bypass for browser traffic
import middy from '@middy/core'
import httpX402 from '@middy/http-x402'
export const handler = middy()
.use(
httpX402({
price: 0.001,
payTo: '0xYourWalletAddress',
asset: '0xYourAssetAddress',
human: (request) => {
const ua = request.event.headers?.['user-agent'] ?? ''
return /Mozilla|Chrome|Safari/.test(ua)
},
}),
)
.handler(async (event, context) => {
return { statusCode: 200, body: JSON.stringify({ message: 'Content' }) }
}) Harden to v2 only
Pinning versions shrinks the accepted protocol surface: v1 payment headers are ignored, agents are only ever challenged with v2 terms, and no facilitator call is spent on v1 traffic.
import middy from '@middy/core'
import httpX402 from '@middy/http-x402'
export const handler = middy()
.use(
httpX402({
price: 0.001,
payTo: '0xYourWalletAddress',
asset: '0xYourAssetAddress',
versions: [2],
}),
)
.handler(async (event, context) => {
return { statusCode: 200, body: JSON.stringify({ message: 'Paid content' }) }
}) Responses
Per the v2 HTTP transport all protocol information is communicated through headers; v1 clients read the challenge from the 402 body instead. For requests recognized as v2 the JSON body mirrors the decoded PAYMENT-REQUIRED object. Advertisement follows the versions option: with versions: [2] the 402 body mirrors the v2 header instead of carrying a v1 challenge, and with versions: [1] the PAYMENT-REQUIRED header is omitted, so agents are only ever offered terms the server will accept.
| Outcome | Status | Where the protocol object lands |
|---|---|---|
| No payment header | 402 | v2 challenge in PAYMENT-REQUIRED, v1 challenge in the body |
Undecodable payment, unsupported x402Version, requirements mismatch, or verification failure (v2 request) | 402 | PAYMENT-REQUIRED with error set |
Undecodable payment, unsupported x402Version, requirements mismatch, or verification failure (v1 request) | 402 | v1 challenge in the body with error set |
| Settlement failure | 402 | PAYMENT-RESPONSE (v2) or X-PAYMENT-RESPONSE (v1) with success: false and errorReason |
| Payment settled | handler's status | PAYMENT-RESPONSE (v2) or X-PAYMENT-RESPONSE (v1) with the settlement result |
Internal storage
After a successful payment, the middleware stores settlement details in request.internal.x402:
payload: Decoded payment header (itsx402Versiontells you which protocol version paid)requirements: Canonical payment requirements used for verification and settlement (v2PaymentRequirements, or v1PaymentRequirementsV1withmaxAmountRequired). The v2 object is frozen: it is shared across warm invocations, so treat it as read-only.payer: Wallet address of the payer (available after settlement)transaction: Settlement transaction hashnetwork: Chain ID of the settlement
Bundling
Add @x402/core to your bundler's external list if you include it as a Lambda layer, otherwise bundle it with your handler.
Last updated: