What x402 is
x402 is a payment handshake built on the HTTP 402 Payment Required status. A resource that costs money answers an unpaid request with a 402 that quotes a price; the caller pays and retries the same request with a header proving payment; the resource verifies and serves. That is the whole protocol. It is designed for machine callers, which is why it fits agent traffic: there is no redirect to a checkout page, no session, and no human in the loop, just a status code and a header a program can handle on its own.
Most of the time you do not implement x402 yourself. On the pay side, createX402Client from @blockchain0x/x402 runs the handshake; on the receive side, the Fastify and Express adapters do, as in how-to-add-x402-to-my-api. This page is for when you are on a stack the adapters do not cover, or when you want to understand exactly what the wire looks like. The payment API product page is the broader reference.
The handshake in four steps
The protocol is four steps, and everything else is detail.
- The caller requests a paid resource without paying.
- The server responds
402with a body listing what it accepts: theexact-usdcscheme, the network, thepayToAddress, and the amount. - The caller settles the payment in USDC and retries the same request with an
X-Paymentheader carrying proof. - The server verifies that header against the requirement and, if it holds, serves the response.
Steps 2 and 4 are the receiver's job; steps 1 and 3 are the payer's. The wire helpers in @blockchain0x/x402 cover the parsing and formatting at each step.
Prerequisites
- A Blockchain0x account and a
sk_test_API key. npm install @blockchain0x/node @blockchain0x/x402.- Node 18 or newer. The SDK targets
>=18.
The 402 response body
The body a server returns on step 2 has a fixed shape. It is version: 1, a resource string, and a non-empty accepts array of requirements:
{
"version": 1,
"resource": "POST /v1/enrich",
"accepts": [
{
"scheme": "exact-usdc",
"network": "testnet",
"chainId": "eip155:84532",
"payToAddress": "0x...",
"amountWeiUsdc": "10000",
"paymentRequestId": "pr_..."
}
]
}amountWeiUsdc is the price in 6-decimal USDC base units, so 10000 is one cent. On the payer side you do not parse this by hand; parse402Response validates the shape and returns it typed, throwing an X402WireError if the body is missing, malformed, or carries an unrecognized requirement.
The X-Payment header
On step 3 the caller retries with a header in the form scheme:base64(payload):
X-Payment: exact-usdc:eyJ...base64...The scheme is the same exact-usdc the requirement quoted, and the payload is the encoded payment. buildPaymentHeader produces this string and parsePaymentHeader decodes it back to a typed payment, each rejecting an unknown scheme or a malformed payload with an X402WireError so you can branch cleanly. The header travels on the retried request to the same resource, not a separate endpoint.
Implement the payer side
The payer reads the 402, settles, and retries. The settlement, producing a valid payment, is what createX402Client exists to do, so the honest implementation leans on it rather than hand-building the on-chain authorization:
import { createClient } from "@blockchain0x/node";
import { createX402Client } from "@blockchain0x/x402/client";
import { parse402Response } from "@blockchain0x/x402";
const sdk = createClient({ apiKey: process.env.B0X_API_KEY! });
const fetchWithPay = createX402Client({ sdk });
// The client does the four-step handshake internally. If you want to inspect
// the requirement before paying, parse a raw 402 yourself first:
const probe = await fetch("https://api.example.com/v1/enrich", { method: "POST" });
if (probe.status === 402) {
const x402 = await parse402Response(probe.clone()); // typed: version, resource, accepts[]
console.log(x402.accepts[0].amountWeiUsdc, x402.accepts[0].network);
}
// Then let the client handle settle + retry:
const res = await fetchWithPay("https://api.example.com/v1/enrich", { method: "POST" });Note the probe.clone(): parse402Response consumes the body, so clone the response if you need it again. That single gotcha trips people who parse the same response twice.
Implement the receiver side
The receiver returns the 402 and verifies the retry. You can construct the 402 body yourself, and you decode the header with parsePaymentHeader, but the verification, confirming the payment really settled, goes through paymentRequests.settle rather than trusting the decoded header:
import { parsePaymentHeader, X402WireError } from "@blockchain0x/x402";
function handle(req, res) {
const header = req.headers["x-payment"];
if (!header) {
res.statusCode = 402;
return res.end(JSON.stringify(build402Body())); // version 1, resource, accepts[]
}
try {
const payment = parsePaymentHeader(header); // typed; throws X402WireError on bad input
// verify the payment really settled before serving:
// await sdk.paymentRequests.settle({ paymentRequestId, body: { txHash, payerAddress, amountUsdcVerified } });
return serveResult(req, res);
} catch (err) {
if (err instanceof X402WireError) {
res.statusCode = 402;
return res.end(JSON.stringify({ code: err.code }));
}
throw err;
}
}The key discipline is the same as any paid integration: never trust the decoded header alone, confirm settlement with the SDK. The helpers parse and format the wire; paymentRequests.settle is what makes the payment real. If you are on Fastify or Express, the adapter does all of this for you and you should use it; the hand-rolled version above is for the stacks it does not cover.
Handle the wire errors
When you implement by hand, the wire helpers do not throw vague errors; they throw an X402WireError with a specific code, and handling each one well is the difference between a debuggable integration and a mysterious one. The codes fall into two groups.
Parsing a 402 on the payer side can raise response.not_402 (the response was not a 402 at all), response.body_missing (no body), or response.body_malformed (the body did not match the version-1 shape). These usually mean the upstream is not speaking x402, or not the version you expect, so treat them as "this endpoint is not a payable x402 resource" rather than retrying.
Parsing the X-Payment header on the receiver side can raise header.missing, header.malformed, header.unknown_scheme (a scheme other than exact-usdc), or header.payload_malformed. These tell you exactly why a caller's payment did not parse, so log the code on every rejection. In the first week of a hand-rolled receiver, that one field is what tells you whether callers are sending nothing, sending garbage, or speaking a scheme you do not support, and each points at a different fix.
Common pitfalls
Three traps.
Draining the 402 body twice. parse402Response reads the body. Clone the response first if you also need it elsewhere, or you will get an empty-body error.
Trusting the parsed header as proof. Decoding the X-Payment header is not verification. A decoded payment still has to be confirmed with paymentRequests.settle. Skipping that is how a hand-rolled receiver leaks revenue.
Reimplementing what the adapter already does. On Fastify or Express, the server adapter and createX402Client are the supported path. Hand-roll only when your stack genuinely has no adapter.
What to ship today
If you are on Fastify or Express, use the adapter and the client; do not hand-roll. If you are not, build the 402 body in the documented shape, parse the retry with parsePaymentHeader, and verify with paymentRequests.settle. Either way, test the handshake on Base Sepolia first. The adapter version is in how-to-add-x402-to-my-api, and the MCP version in how-to-add-payments-to-mcp-server. Pricing is on the pricing page.