x402 v2 Payments 2026: Implementation and Settlement Checks

This hands-on Node.js tutorial adds micropayments to an API with x402 v2. Select payment terms, request new signatures, and verify settlement on Base Sepolia, then check retries and requirements for switching to mainnet.

x402 payments are implemented by including payment requirements in an API's 402 response. Prepare Node.js and a test wallet. First, verify v2 signatures and settlement on Base Sepolia. Configure paid responses to be delivered only after settlement succeeds.

Amounts and chain information are based on the official x402, Base, and Circle documentation linked as sources.

Setup and installation

Prepare a development environment where you can modify the API, along with test wallets. The examples use Node.js 22 and x402 SDK 2.28.0. The installation structure follows the official seller guide.

  1. Prepare an EVM wallet address to receive payments.
  2. Prepare a separate test wallet for making payments.
  3. Obtain Base Sepolia test USDC for the payment wallet.
  4. Create the practice project with the commands below.
  5. Save the TypeScript files that follow in the same folder.
mkdir x402-demo
cd x402-demo
npm init -y
npm pkg set type=module
npm install --save-exact [email protected] [email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected]
npm install --save-dev --save-exact typescript tsx @types/node @types/express

The receiving server does not need the receiving wallet's private key. Configure the payment private key to be read only by the client process. The wallet signing method follows the official buyer guide.

This example has been checked through type checking and a simulated payment flow. It has not been tested through a completed transfer on the actual testnet. Check the on-chain results yourself using the execution steps below.

Comparing x402 v1 and v2

When implementing v2, align both the headers and the body fields. Changing only the header names will leave the data structures incompatible. The following table is based on the official migration guide.

Category v1 v2
Protocol version x402Version: 1 x402Version: 2
Initial payment requirements JSON body of the 402 response PAYMENT-REQUIRED response header
Signed payment data X-PAYMENT request header PAYMENT-SIGNATURE request header
Settlement result X-PAYMENT-RESPONSE response header PAYMENT-RESPONSE response header
Base mainnet identifier base eip155:8453
Base Sepolia identifier base-sepolia eip155:84532
Amount field in payment requirements maxAmountRequired amount
Resource description resource and other fields within payment requirements Top-level resource object
Requirements in the payment payload Top-level scheme, network, and other fields accepted object containing the selected requirements
Example TypeScript package x402-express @x402/express

The location for delivering v1 payment requirements is specified in the HTTP v1 specification. The standard location in v2 is the PAYMENT-REQUIRED header. An implementation may also return a body, but process the requirements using the header.

The HTTP v2 specification describes the status code as follows.

The server indicates payment is required using the HTTP 402 "Payment Required" status code.

Source: x402 HTTP v2 specification

v2 payment headers contain Base64-encoded JSON. Base64 is not encryption. Use HTTPS for externally accessible services.

HTTP 402 requests and the payment sequence

This exercise uses the default authorization flow for the EVM exact scheme. exact is a scheme for paying a specified amount. The sequence follows the x402 v2 core specification.

  1. The client requests a paid API without payment information.
  2. The server returns HTTP 402 and PAYMENT-REQUIRED.
  3. The client selects an allowed set of requirements from accepts.
  4. The SDK signs a payment authorization matching the selected requirements.
  5. The client retries the request with PAYMENT-SIGNATURE.
  6. The server validates the authorization through the facilitator's /verify.
  7. If verification passes, the server processes the resource.
  8. The server requests settlement through /settle.
  9. After settlement succeeds, it delivers the paid body and PAYMENT-RESPONSE.

This is not a fixed sequence shared by all payment schemes. v2 also defines flows with different settlement timing. The code below allows only the default authorization flow.

Roles of the Resource server and facilitator

The Resource server determines prices and conditions for accessing content. The Facilitator handles payment verification and settlement. This division of roles follows the official facilitator documentation.

Component Responsibility State that must not be treated as success
Client Selects requirements and signs payment authorizations Only the signature has been generated
Resource server Declares prices and controls paid responses The client claims success
Facilitator /verify Checks the validity of signatures and payment requirements Only isValid: true has been received
Facilitator /settle Executes the on-chain payment and returns the result in this exercise Submitted, but the result has not been confirmed

/verify is a check that does not change on-chain state. Successful verification therefore does not mean funds have been received. The balance or authorization state may change even after verification.

Block the paid body if verification or settlement fails. Blocking access when success cannot be confirmed is called fail-closed behavior. This behavior can also be seen in the official Express implementation.

Comparing Base mainnet and testnet

Use only Base Sepolia for the first run. Circle states that testnet USDC has no monetary value. The addresses are based on Circle's list of USDC contracts.

Item Base mainnet Base Sepolia testnet
Purpose Payments with real assets Testing implementation and failure handling
Chain ID 8453 84532
x402 v2 network eip155:8453 eip155:84532
USDC contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 0x036CbD53842c5426634e7929541eC2318f3dCF7e
Public RPC https://mainnet.base.org https://sepolia.base.org
Token's monetary value Asset with real monetary value No monetary value

Check the Chain ID and RPC in the Base network documentation. Even for the same wallet address, balances are separate on different chains. Do not record testnet success as actual revenue.

You can request test USDC from the Circle Testnet Faucet. Select Base Sepolia as the network. Enter the test payment wallet's address as the receiving address.

Implementing a paid API in Node.js

Attach paymentMiddleware before the paid route. The example below requires 0.001 USDC per call. The amount uses the example price from the official seller guide. (Verified value: 0.001 USDC per call · Source: docs.x402.org · Checked on 2026-10-28)

Server file

Save the following code as server.ts. The server accepts requests only on the local interface. It reads the receiving address from the PAY_TO environment variable.

import express from "express";
import { getAddress, zeroAddress } from "viem";
import { paymentMiddleware } from "@x402/express";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";

const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const resourceServer = new x402ResourceServer(
  new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }),
).register(network, new ExactEvmScheme());

const app = express();
app.use((_req, res, next) => {
  res.setHeader("Cache-Control", "private, no-store");
  next();
});
app.use(paymentMiddleware({
  "GET /paid-data": {
    accepts: [{ scheme: "exact", price: "$0.001", network, payTo }],
    description: "Sample paid JSON",
    mimeType: "application/json",
  },
}, resourceServer));
app.get("/paid-data", (_req, res) => {
  res.json({ content: "Paid sample content" });
});
app.listen(4021, "127.0.0.1");

https://x402.org/facilitator is the testing address in the official guide. This example uses it for Base Sepolia settlement. For choosing a production facilitator, see the mainnet steps in the seller guide.

Price calculation example

The example price is 1000 in USDC's smallest units. USDC has 6 decimal places. The SDK's price conversion is based on the EVM server implementation.

0.001 USDC × 10^6 = 1000 smallest units
Value to check Expected value in this example
price in the server configuration "$0.001"
amount in the 402 payment requirements "1000"
asset in the payment requirements Base Sepolia USDC contract
payTo in the payment requirements Configured receiving address
network in the payment requirements eip155:84532

price is an SDK configuration value. The actual v2 payment requirements carry amount. Compare integer amounts as strings or bigint.

Running the server and checking the 402 response

Run the server in the first Bash terminal. Enter a public receiving address, not a private key. Keep this terminal open while the server is running.

read -r -p '수취 지갑 주소: ' PAY_TO
export PAY_TO
npx tsx server.ts

Send a request without payment from a second terminal. The command below does not sign a payment. It prints the HTTP status and decoded payment requirements.

node --input-type=module <<'JS'
const response = await fetch("http://127.0.0.1:4021/paid-data");
const header = response.headers.get("PAYMENT-REQUIRED");
console.log(response.status);
console.log(header && JSON.parse(
  Buffer.from(header, "base64").toString("utf8")
));
JS

The expected response without payment is HTTP 402. Compare the printed requirements with the table above. If paid content is returned immediately, fix the route protection configuration first.

Selecting payment requirements and implementing the client

Restrict the client to signing only approved requirements. This example allows only a fixed price in Base Sepolia USDC. The payment option filter uses registerPolicy from the official x402Client implementation.

  1. Save the code below as client.ts.
  2. Also save check-transfer.ts from the next section.
  3. Once both files are ready, proceed with the execution steps.
import { writeFile } from "node:fs/promises";
import { getAddress, zeroAddress, type Hex, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { decodePaymentResponseHeader } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { checkTransfer } from "./check-transfer.js";

const key = process.env.EVM_PRIVATE_KEY ?? "";
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error("Invalid test key");
const signer = privateKeyToAccount(key as Hex);
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const token = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const client = new x402Client().register(network, new ExactEvmScheme(signer));
client.registerPolicy((version, options) => version === 2
  ? options.filter(r =>
      r.scheme === "exact" && r.network === network &&
      r.asset.toLowerCase() === token.toLowerCase() &&
      r.payTo.toLowerCase() === payTo.toLowerCase() && r.amount === "1000" &&
      (!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === "eip3009") &&
      (!r.extra?.paymentFlow || r.extra.paymentFlow === "authorization"))
  : []);

const paidFetch = wrapFetchWithPayment(fetch, client);
const response = await paidFetch("http://127.0.0.1:4021/paid-data", {
  redirect: "error",
});
const raw = response.headers.get("PAYMENT-RESPONSE");
if (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);
const settlement = decodePaymentResponseHeader(raw);
await writeFile("settlement.json", JSON.stringify(settlement, null, 2));
if (!response.ok || !settlement.success || settlement.network !== network) {
  throw new Error("Payment unresolved; inspect settlement.json before retrying");
}
const evidence = await checkTransfer(
  network, settlement.transaction as Hash, signer.address, payTo, 1000n,
);
await writeFile("transfer.json", JSON.stringify(evidence, null, 2));
console.log(await response.text());

wrapFetchWithPayment handles the 402 response and retries the request with a signature. Its usage follows the official fetch example. If no payment requirements are allowed, payment creation stops.

AI agents can also use this client. However, a per-call price limit is not a cumulative spending limit. Manage the budget for an agent's repeated calls separately.

Verifying USDC Transfer events

Check the USDC transfer logs using the transaction hash from the settlement result. A successful transaction status alone does not confirm the recipient. The Transfer fields follow the ERC-20 standard.

Verification requires the following items.

Verification item Compare against
RPC's Chain ID Chain allowed by the server
Transaction receipt's status Success status
Address that emitted the event Official USDC contract on that chain
Transfer.from Verified payer
Transfer.to Receiving address set by the server
Transfer.value Amount required by the server in the smallest units

Save the code below as check-transfer.ts. The lookup uses viem's getTransactionReceipt. Event parsing is based on parseEventLogs.

import {
  createPublicClient, http, getAddress, parseEventLogs,
  erc20Abi, type Address, type Hash,
} from "viem";
import { base, baseSepolia } from "viem/chains";

const configs = {
  "eip155:84532": {
    chain: baseSepolia,
    rpc: "https://sepolia.base.org",
    token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  },
  "eip155:8453": {
    chain: base,
    rpc: "https://mainnet.base.org",
    token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  },
} as const;

export async function checkTransfer(
  network: keyof typeof configs, hash: Hash,
  payer: Address, payTo: Address, amount: bigint,
) {
  const cfg = configs[network];
  const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });
  if (await rpc.getChainId() !== cfg.chain.id) throw new Error("Wrong chain");
  const receipt = await rpc.getTransactionReceipt({ hash });
  if (receipt.status !== "success") throw new Error("Transaction failed");
  const logs = parseEventLogs({
    abi: erc20Abi,
    eventName: "Transfer",
    logs: receipt.logs.filter(
      log => getAddress(log.address) === getAddress(cfg.token),
    ),
    strict: true,
  });
  const transfer = logs.find(log =>
    getAddress(log.args.from) === getAddress(payer) &&
    getAddress(log.args.to) === getAddress(payTo) &&
    log.args.value === amount,
  );
  if (!transfer) throw new Error("Matching USDC Transfer not found");
  return {
    network, transaction: hash, logIndex: transfer.logIndex,
    blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),
  };
}

This function confirms that a USDC transfer occurred in the transaction. Whether it belongs to a specific API order must be checked separately. On a production server, use the stored order requirements as the comparison values.

Do not trust a client-uploaded transfer.json as proof for granting access. This example's server uses the facilitator's verification and settlement results. If an independent lookup is a condition for granting access, it must run on the server.

A failed receipt lookup does not necessarily mean the payment failed. If an RPC error occurs, look up the stored transaction hash again. Set the number of additional confirmations and the response to chain reorganizations through your production policy.

Running a test payment and checking the results

After saving all the files, run the example with a test wallet. Type checking does not sign anything or transfer funds. The commands below use the TypeScript tools installed earlier.

  1. Check the types with the following command.
  2. Enter the same receiving address as the server in the second terminal.
  3. Enter the test payment wallet's private key with input hidden.
  4. Run the client once.
  5. Check the saved settlement result and transfer log.
npx tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext server.ts client.ts check-transfer.ts
read -r -p '같은 수취 지갑 주소: ' PAY_TO
read -r -s -p '테스트 결제 지갑 개인키: ' EVM_PRIVATE_KEY
export PAY_TO EVM_PRIVATE_KEY
npx tsx client.ts
unset EVM_PRIVATE_KEY
Output Meaning What to check
settlement.json Settlement result returned by the server success, network, transaction
transfer.json Transfer record found through the RPC lookup Chain, transaction hash, log position, block information
Paid body in the console Data printed after this client's verification Confirm that it is the requested content

Running the client again may create a new payment. This minimal example has no per-order duplicate prevention store. Add retries only after meeting the requirements in the next section.

Handling verification failure conditions

In failure tests, check that the paid body is not exposed. Checking only the HTTP status may miss cases where content is not blocked. Refer to the official Express middleware for the baseline behavior.

Test condition Expected handling Additional check
No payment header Return 402 and payment requirements No paid body
Different token or recipient offered Reject through the client policy No signature generated
Invalid signature Block due to verification failure Paid handler does not run
Insufficient balance Block due to verification or settlement failure Not recorded as successful settlement
Failed settlement result Block the paid response Do not send even a prepared body
Delayed facilitator response Keep pending because success is unconfirmed Do not automatically generate a new authorization
RPC receipt lookup error Keep the transaction hash and query again Do not conclude that payment failed

In the default authorization flow, the handler runs before settlement. A settlement failure does not roll back external operations. Start with queries that have no side effects, as in this example.

Sending emails or creating external orders requires a separate design. Record payment and task states together. Define which operations should run again after a failure to reduce duplicate work.

Lost responses after settlement and recovery through redelivery

Store payment completion and content delivery completion as separate states. Payment may finish while the response connection is lost. Handling this situation is covered in the Payment-Identifier extension documentation.

The property of not processing the same operation twice when it is requested again is called idempotency. payment-identifier provides a payment identifier for this purpose. Adding the identifier alone does not automatically complete the feature.

Comparing duplicate payments and redelivery

Preventing reuse of the same signature and preventing duplicate orders must be treated separately. EIP-3009 prevents reuse of an authorization nonce that has already been used. This behavior is defined in the ERC-3009 specification.

Situation On-chain authorization perspective API operations perspective
Settling the same authorization again Subject to the check for an already-used nonce Existing result must be retrieved
Creating a new authorization for the same order May be treated as a separate authorization Duplicate payment prevention is needed
Response lost after successful settlement Asset transfer may already be complete Content must be delivered again
Timeout after a settlement request Result is still unclear Pending state and settlement confirmation are needed

The following procedure is a design proposal for connecting the official extension to a production data store. It is not included in the minimal code above. Add persistent storage and concurrent request controls before using it in production.

  1. Declare support for the payment-identifier extension on the server.
  2. Keep the identifier for the logical request on the client.
  3. Associate the identifier with the payer and request contents on the server.
  4. Record the processing state atomically to prevent concurrent execution.
  5. Save the transaction evidence and response result when settlement succeeds.
  6. Return the stored result if the repeated request has the same authorization and contents.
  7. If the settlement result is unclear, check the existing authorization first.

A payment identifier is not a password or access token. Do not return cached paid content to a request that merely knows the identifier. Also check whether it is being reused by another user or with a different request body.

Keep the chain, transaction hash, and log position together in production records. Link this record to the order to prevent reuse for other orders. A matching Transfer alone does not prove which order it belongs to.

Common payment implementation mistakes

Confusing version fields and success signals leads to incorrect payment decisions. Use the table below during deployment review. Version differences are based on the x402 migration guide.

Common mistake Correct approach
Interpreting extra.version: "2" as the x402 version Check the protocol version through x402Version
Using maxAmountRequired in v2 Use amount for the amount in payment requirements
Creating an X-PAYMENT-REQUIRED header arbitrarily Use the body in v1 and PAYMENT-REQUIRED in v2
Treating isValid: true as completed receipt of funds Check the /settle result separately
Treating the transaction's top-level to as the USDC recipient Check USDC Transfer.to
Assuming the signer is the transaction sender Compare the authorizing payer with Transfer.from
Adding testnet receipts to revenue Keep mainnet and testnet ledgers separate
Creating a new payment on every timeout Recover the existing order and authorization state first
Treating the SDK package version as the protocol version Manage the package version and x402Version separately

USDC signature domain information also contains a version string. That value serves a different purpose from the x402 protocol version. See the ERC-3009 specification for the signing domain structure.

Security checks and trust boundaries for external services

Control payment success decisions on the server. Responses from an external facilitator are also an explicit trust dependency. The architecture, which also allows self-hosting, is explained in the facilitator documentation.

The pinned dependencies in this exercise still have security warnings. npm audit reported a zod-related advisory. See the GitHub security advisory for details.

npm audit

Before deploying to production, check SDK compatibility with the patched version. Forcing a major version replacement may change behavior. Do not use this exercise as a deployment configuration that has passed security review.

Comparing a self-hosted server and Cloudflare Gateway

Using Cloudflare Gateway changes how the origin server verifies payments. The documentation dated September 30, 2026 specifies the use of x402 v2. The following comparison is based on the Cloudflare x402 documentation.

Category Express approach in this article Cloudflare Monetization Gateway
Client payment processing Express middleware Gateway
Origin server verification point Integration with the selected facilitator PAYMENT-CONTEXT JWT verification
Settlement result delivery Middleware's PAYMENT-RESPONSE Gateway generates a settlement receipt
Additional checks Facilitator's supported chains and authentication JWT signature verification and origin access protection

PAYMENT-CONTEXT is a header between the Gateway and the origin server. It is not a signature header for a regular x402 client. Follow the Cloudflare Payment validation documentation for origin verification.

Even when using the Gateway, simply decoding the JWT is not enough. Verify the signature using the keys specified in the documentation. Block paid content if the header is missing or verification fails.

Distinguishing actual usage from settlement records

The number of on-chain transfers alone cannot prove content consumption. Compile production metrics by linking settlement with response delivery. This is a design recommendation based on the payment flow and redelivery behavior.

Record Question it can answer Additional record needed
402 response How often payment requirements were requested Whether payment actually occurred
Successful verification Whether the authorization was valid Settlement result
USDC transfer Whether assets moved Order association
Settlement completed per order Whether a paid request was paid for Response delivery status
Redelivery record Whether an already-paid request was recovered Identifier distinguishing it from a new payment

Separate test requests from actual usage metrics. Mark retries for the same order separately as well. Do not describe transaction volume itself as API demand or a guarantee of revenue.

Steps for switching to Base mainnet

Review mainnet use after checking both successful and failed testnet paths. Changing the network string alone does not complete the switch. The baseline procedure follows the official seller guide.

  1. Confirm that the selected facilitator supports Base mainnet.
  2. Check /supported for v2, exact, and eip155:8453 support.
  3. Configure the authentication and terms of use required by the provider.
  4. Change the network on both the server and client.
  5. Change the client's allowed USDC address to the mainnet address.
  6. Recheck the amount in the 402 response against the actual receiving address.
  7. Connect the duplicate request store and failure recovery procedures.
  8. Complete security checks on production dependencies.
  9. Verify the actual transfer and content delivery using the small amount you configured.

Multichain support does not mean every combination is supported. Check the support offered by both the SDK and the facilitator. Use the network and token support documentation as the reference.

FAQ

How does x402 differ from a regular USDC deposit confirmation?

It sends payment requirements during the HTTP request process and retries the request with a signed payment authorization. Unlike reconciling deposit records later, it links the payment process to requests for paid resources.

What are the payment headers in x402 v2?

Payment requirements are sent via PAYMENT-REQUIRED, and the payment payload via PAYMENT-SIGNATURE. The settlement result is sent via PAYMENT-RESPONSE.

Does v1 have an X-PAYMENT-REQUIRED header?

The official HTTP v1 specification puts the initial payment requirements in the JSON response body. It uses X-PAYMENT for the payment payload and X-PAYMENT-RESPONSE for the settlement result.

Is payment complete if /verify succeeds?

A successful /verify means the payment authorization is valid. In this exercise, you must separately confirm that /settle succeeded.

Can Base Sepolia test USDC be considered actual revenue?

Circle states that testnet USDC has no financial value. Record testnet settlements separately from actual sales.

Do I need to put a private key on the receiving server?

Only a public receiving address is configured on the receiving server in this example. The private key used for payment authorization is read by the payment client.

What is the amount value for 0.001 USDC?

With USDC's 6 decimal places, this is 1000 in the smallest unit. Use the string "1000" in the v2 payment requirements.

If the transaction receipt shows success, does that also confirm receipt of USDC?

A successful transaction status alone is not enough. Check the payer, recipient, and amount against the Transfer event from the official USDC contract.

Is it safe to resend the same payment request?

Preventing reuse of the same authorization nonce and preventing duplicate orders are separate concerns. Check the existing payment status before retrying, and implement idempotency for each order.

If PAYMENT-RESPONSE is missing, should I pay again?

If the response was lost or the request timed out, the settlement result may be unclear. Do not start by creating a new authorization; check the settlement status of the existing request.

Does x402 require you to use only a specific company's facilitator?

The official architecture allows both using an external facilitator and running your own. Check whether your chosen implementation supports the required protocol version, payment method, and chain.

Is an AI agent's spending also limited automatically?

This example only limits payment requirements per request. Manage cumulative spending from repeated calls with a separate budget policy.

Is Cloudflare PAYMENT-CONTEXT a standard payment signature header?

PAYMENT-CONTEXT is a JWT that Gateway passes to the origin server. It serves a different purpose from the client's PAYMENT-SIGNATURE.

Can I deploy the example directly to mainnet?

This code is a minimal setup for testing. Before deploying to production, check dependency security warnings, duplicate request handling, facilitator authentication, and actual settlement.

Sources

Images

A man in a dark gray sweater examines a tablet resting on a server rack shelf.
A man in a dark gray sweater examines a tablet resting on a server rack shelf.
A woman types on a laptop opened flat, with a list and two paths converging on one endpoint on its screen.
A woman types on a laptop opened flat, with a list and two paths converging on one endpoint on its screen.