{"content_id":"gyaevhk2bq","slug":"x402-payments-2026-v2-base-usdc-settlement-guide","locale":"en","schema_type":"HowTo","category":"tutorial","category_name":"Tutorial","title":"x402 v2 Payments 2026: Implementation and Settlement Checks","summary":"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.","sponsorship_disclosure":null,"affiliate_disclosure":null,"commerce_disclosure":null,"author":{"name":"Injoys Editorial Team","url":"https://injoys.com/ko/about"},"key_points":["Prepare a receiving address and a test payment wallet, and install the official x402 SDK.","Connect payment middleware for Base Sepolia and check the 402 response to requests without payment.","On the client, select and sign only payment terms that match the allowed chain, token, amount, and recipient.","Use the transaction hash in the settlement response to look up USDC Transfer events.","Test blocking on failure and recovery from duplicate requests, then review the mainnet settings."],"content_markdown":"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.\n\nAmounts and chain information are based on the official x402, Base, and Circle documentation linked as sources.\n\n## Setup and installation\n\nPrepare 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](https://docs.x402.org/getting-started/quickstart-for-sellers).\n\n1. Prepare an EVM wallet address to receive payments.\n2. Prepare a separate test wallet for making payments.\n3. Obtain Base Sepolia test USDC for the payment wallet.\n4. Create the practice project with the commands below.\n5. Save the TypeScript files that follow in the same folder.\n\n```bash\nmkdir x402-demo\ncd x402-demo\nnpm init -y\nnpm pkg set type=module\nnpm install --save-exact express@5.3.0 viem@2.57.12 @x402/core@2.28.0 @x402/express@2.28.0 @x402/evm@2.28.0 @x402/fetch@2.28.0\nnpm install --save-dev --save-exact typescript tsx @types/node @types/express\n```\n\nThe 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](https://docs.x402.org/getting-started/quickstart-for-buyers).\n\nThis 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.\n\n## Comparing x402 v1 and v2\n\nWhen 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](https://docs.x402.org/guides/migration-v1-to-v2).\n\n| Category | v1 | v2 |\n|---|---|---|\n| Protocol version | `x402Version: 1` | `x402Version: 2` |\n| Initial payment requirements | JSON body of the 402 response | `PAYMENT-REQUIRED` response header |\n| Signed payment data | `X-PAYMENT` request header | `PAYMENT-SIGNATURE` request header |\n| Settlement result | `X-PAYMENT-RESPONSE` response header | `PAYMENT-RESPONSE` response header |\n| Base mainnet identifier | `base` | `eip155:8453` |\n| Base Sepolia identifier | `base-sepolia` | `eip155:84532` |\n| Amount field in payment requirements | `maxAmountRequired` | `amount` |\n| Resource description | `resource` and other fields within payment requirements | Top-level `resource` object |\n| Requirements in the payment payload | Top-level `scheme`, `network`, and other fields | `accepted` object containing the selected requirements |\n| Example TypeScript package | `x402-express` | `@x402/express` |\n\nThe location for delivering v1 payment requirements is specified in the [HTTP v1 specification](https://github.com/x402-foundation/x402/blob/main/specs/transports-v1/http.md). The standard location in v2 is the `PAYMENT-REQUIRED` header. An implementation may also return a body, but process the requirements using the header.\n\nThe [HTTP v2 specification](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md) describes the status code as follows.\n\n\u003e The server indicates payment is required using the HTTP 402 \"Payment Required\" status code.\n\u003e\n\u003e Source: x402 HTTP v2 specification\n\nv2 payment headers contain Base64-encoded JSON. Base64 is not encryption. Use HTTPS for externally accessible services.\n\n## HTTP 402 requests and the payment sequence\n\nThis 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](https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md).\n\n1. The client requests a paid API without payment information.\n2. The server returns HTTP 402 and `PAYMENT-REQUIRED`.\n3. The client selects an allowed set of requirements from `accepts`.\n4. The SDK signs a payment authorization matching the selected requirements.\n5. The client retries the request with `PAYMENT-SIGNATURE`.\n6. The server validates the authorization through the facilitator's `/verify`.\n7. If verification passes, the server processes the resource.\n8. The server requests settlement through `/settle`.\n9. After settlement succeeds, it delivers the paid body and `PAYMENT-RESPONSE`.\n\nThis 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.\n\n### Roles of the Resource server and facilitator\n\nThe 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](https://github.com/x402-foundation/x402/blob/main/docs/core-concepts/facilitator.md).\n\n| Component | Responsibility | State that must not be treated as success |\n|---|---|---|\n| Client | Selects requirements and signs payment authorizations | Only the signature has been generated |\n| Resource server | Declares prices and controls paid responses | The client claims success |\n| Facilitator `/verify` | Checks the validity of signatures and payment requirements | Only `isValid: true` has been received |\n| Facilitator `/settle` | Executes the on-chain payment and returns the result in this exercise | Submitted, but the result has not been confirmed |\n\n`/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.\n\nBlock 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](https://github.com/x402-foundation/x402/blob/main/typescript/packages/http/express/src/index.ts).\n\n## Comparing Base mainnet and testnet\n\nUse 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](https://developers.circle.com/stablecoins/usdc-contract-addresses).\n\n| Item | Base mainnet | Base Sepolia testnet |\n|---|---|---|\n| Purpose | Payments with real assets | Testing implementation and failure handling |\n| Chain ID | `8453` | `84532` |\n| x402 v2 network | `eip155:8453` | `eip155:84532` |\n| USDC contract | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |\n| Public RPC | `https://mainnet.base.org` | `https://sepolia.base.org` |\n| Token's monetary value | Asset with real monetary value | No monetary value |\n\nCheck the Chain ID and RPC in the [Base network documentation](https://docs.base.org/get-started/connect-to-base). Even for the same wallet address, balances are separate on different chains. Do not record testnet success as actual revenue.\n\nYou can request test USDC from the [Circle Testnet Faucet](https://faucet.circle.com/). Select Base Sepolia as the network. Enter the test payment wallet's address as the receiving address.\n\n## Implementing a paid API in Node.js\n\nAttach `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)\n\n### Server file\n\nSave 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.\n\n```typescript\nimport express from \"express\";\nimport { getAddress, zeroAddress } from \"viem\";\nimport { paymentMiddleware } from \"@x402/express\";\nimport { HTTPFacilitatorClient, x402ResourceServer } from \"@x402/core/server\";\nimport { ExactEvmScheme } from \"@x402/evm/exact/server\";\n\nconst payTo = getAddress(process.env.PAY_TO ?? \"\");\nif (payTo === zeroAddress) throw new Error(\"Invalid payTo\");\nconst network = \"eip155:84532\";\nconst resourceServer = new x402ResourceServer(\n  new HTTPFacilitatorClient({ url: \"https://x402.org/facilitator\" }),\n).register(network, new ExactEvmScheme());\n\nconst app = express();\napp.use((_req, res, next) =\u003e {\n  res.setHeader(\"Cache-Control\", \"private, no-store\");\n  next();\n});\napp.use(paymentMiddleware({\n  \"GET /paid-data\": {\n    accepts: [{ scheme: \"exact\", price: \"$0.001\", network, payTo }],\n    description: \"Sample paid JSON\",\n    mimeType: \"application/json\",\n  },\n}, resourceServer));\napp.get(\"/paid-data\", (_req, res) =\u003e {\n  res.json({ content: \"Paid sample content\" });\n});\napp.listen(4021, \"127.0.0.1\");\n```\n\n`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](https://docs.x402.org/getting-started/quickstart-for-sellers#running-on-mainnet).\n\n### Price calculation example\n\nThe 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](https://github.com/x402-foundation/x402/blob/main/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts).\n\n```text\n0.001 USDC × 10^6 = 1000 smallest units\n```\n\n| Value to check | Expected value in this example |\n|---|---|\n| `price` in the server configuration | `\"$0.001\"` |\n| `amount` in the 402 payment requirements | `\"1000\"` |\n| `asset` in the payment requirements | Base Sepolia USDC contract |\n| `payTo` in the payment requirements | Configured receiving address |\n| `network` in the payment requirements | `eip155:84532` |\n\n`price` is an SDK configuration value. The actual v2 payment requirements carry `amount`. Compare integer amounts as strings or `bigint`.\n\n### Running the server and checking the 402 response\n\nRun 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.\n\n```bash\nread -r -p '수취 지갑 주소: ' PAY_TO\nexport PAY_TO\nnpx tsx server.ts\n```\n\nSend 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.\n\n```bash\nnode --input-type=module \u003c\u003c'JS'\nconst response = await fetch(\"http://127.0.0.1:4021/paid-data\");\nconst header = response.headers.get(\"PAYMENT-REQUIRED\");\nconsole.log(response.status);\nconsole.log(header \u0026\u0026 JSON.parse(\n  Buffer.from(header, \"base64\").toString(\"utf8\")\n));\nJS\n```\n\nThe 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.\n\n## Selecting payment requirements and implementing the client\n\nRestrict 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](https://github.com/x402-foundation/x402/blob/main/typescript/packages/core/src/client/x402Client.ts).\n\n1. Save the code below as `client.ts`.\n2. Also save `check-transfer.ts` from the next section.\n3. Once both files are ready, proceed with the execution steps.\n\n```typescript\nimport { writeFile } from \"node:fs/promises\";\nimport { getAddress, zeroAddress, type Hex, type Hash } from \"viem\";\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport { x402Client, wrapFetchWithPayment } from \"@x402/fetch\";\nimport { decodePaymentResponseHeader } from \"@x402/core/http\";\nimport { ExactEvmScheme } from \"@x402/evm/exact/client\";\nimport { checkTransfer } from \"./check-transfer.js\";\n\nconst key = process.env.EVM_PRIVATE_KEY ?? \"\";\nif (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error(\"Invalid test key\");\nconst signer = privateKeyToAccount(key as Hex);\nconst payTo = getAddress(process.env.PAY_TO ?? \"\");\nif (payTo === zeroAddress) throw new Error(\"Invalid payTo\");\nconst network = \"eip155:84532\";\nconst token = \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\";\nconst client = new x402Client().register(network, new ExactEvmScheme(signer));\nclient.registerPolicy((version, options) =\u003e version === 2\n  ? options.filter(r =\u003e\n      r.scheme === \"exact\" \u0026\u0026 r.network === network \u0026\u0026\n      r.asset.toLowerCase() === token.toLowerCase() \u0026\u0026\n      r.payTo.toLowerCase() === payTo.toLowerCase() \u0026\u0026 r.amount === \"1000\" \u0026\u0026\n      (!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === \"eip3009\") \u0026\u0026\n      (!r.extra?.paymentFlow || r.extra.paymentFlow === \"authorization\"))\n  : []);\n\nconst paidFetch = wrapFetchWithPayment(fetch, client);\nconst response = await paidFetch(\"http://127.0.0.1:4021/paid-data\", {\n  redirect: \"error\",\n});\nconst raw = response.headers.get(\"PAYMENT-RESPONSE\");\nif (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);\nconst settlement = decodePaymentResponseHeader(raw);\nawait writeFile(\"settlement.json\", JSON.stringify(settlement, null, 2));\nif (!response.ok || !settlement.success || settlement.network !== network) {\n  throw new Error(\"Payment unresolved; inspect settlement.json before retrying\");\n}\nconst evidence = await checkTransfer(\n  network, settlement.transaction as Hash, signer.address, payTo, 1000n,\n);\nawait writeFile(\"transfer.json\", JSON.stringify(evidence, null, 2));\nconsole.log(await response.text());\n```\n\n`wrapFetchWithPayment` handles the 402 response and retries the request with a signature. Its usage follows the [official fetch example](https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients/fetch). If no payment requirements are allowed, payment creation stops.\n\nAI 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.\n\n## Verifying USDC Transfer events\n\nCheck 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](https://eips.ethereum.org/EIPS/eip-20).\n\nVerification requires the following items.\n\n| Verification item | Compare against |\n|---|---|\n| RPC's Chain ID | Chain allowed by the server |\n| Transaction receipt's `status` | Success status |\n| Address that emitted the event | Official USDC contract on that chain |\n| `Transfer.from` | Verified payer |\n| `Transfer.to` | Receiving address set by the server |\n| `Transfer.value` | Amount required by the server in the smallest units |\n\nSave the code below as `check-transfer.ts`. The lookup uses viem's [getTransactionReceipt](https://viem.sh/docs/actions/public/getTransactionReceipt). Event parsing is based on [parseEventLogs](https://viem.sh/docs/contract/parseEventLogs).\n\n```typescript\nimport {\n  createPublicClient, http, getAddress, parseEventLogs,\n  erc20Abi, type Address, type Hash,\n} from \"viem\";\nimport { base, baseSepolia } from \"viem/chains\";\n\nconst configs = {\n  \"eip155:84532\": {\n    chain: baseSepolia,\n    rpc: \"https://sepolia.base.org\",\n    token: \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\",\n  },\n  \"eip155:8453\": {\n    chain: base,\n    rpc: \"https://mainnet.base.org\",\n    token: \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\n  },\n} as const;\n\nexport async function checkTransfer(\n  network: keyof typeof configs, hash: Hash,\n  payer: Address, payTo: Address, amount: bigint,\n) {\n  const cfg = configs[network];\n  const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });\n  if (await rpc.getChainId() !== cfg.chain.id) throw new Error(\"Wrong chain\");\n  const receipt = await rpc.getTransactionReceipt({ hash });\n  if (receipt.status !== \"success\") throw new Error(\"Transaction failed\");\n  const logs = parseEventLogs({\n    abi: erc20Abi,\n    eventName: \"Transfer\",\n    logs: receipt.logs.filter(\n      log =\u003e getAddress(log.address) === getAddress(cfg.token),\n    ),\n    strict: true,\n  });\n  const transfer = logs.find(log =\u003e\n    getAddress(log.args.from) === getAddress(payer) \u0026\u0026\n    getAddress(log.args.to) === getAddress(payTo) \u0026\u0026\n    log.args.value === amount,\n  );\n  if (!transfer) throw new Error(\"Matching USDC Transfer not found\");\n  return {\n    network, transaction: hash, logIndex: transfer.logIndex,\n    blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),\n  };\n}\n```\n\nThis 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.\n\nDo 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.\n\nA 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.\n\n## Running a test payment and checking the results\n\nAfter 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.\n\n1. Check the types with the following command.\n2. Enter the same receiving address as the server in the second terminal.\n3. Enter the test payment wallet's private key with input hidden.\n4. Run the client once.\n5. Check the saved settlement result and transfer log.\n\n```bash\nnpx tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext server.ts client.ts check-transfer.ts\nread -r -p '같은 수취 지갑 주소: ' PAY_TO\nread -r -s -p '테스트 결제 지갑 개인키: ' EVM_PRIVATE_KEY\nexport PAY_TO EVM_PRIVATE_KEY\nnpx tsx client.ts\nunset EVM_PRIVATE_KEY\n```\n\n| Output | Meaning | What to check |\n|---|---|---|\n| `settlement.json` | Settlement result returned by the server | `success`, `network`, `transaction` |\n| `transfer.json` | Transfer record found through the RPC lookup | Chain, transaction hash, log position, block information |\n| Paid body in the console | Data printed after this client's verification | Confirm that it is the requested content |\n\nRunning 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.\n\n## Handling verification failure conditions\n\nIn 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](https://github.com/x402-foundation/x402/blob/main/typescript/packages/http/express/src/index.ts) for the baseline behavior.\n\n| Test condition | Expected handling | Additional check |\n|---|---|---|\n| No payment header | Return 402 and payment requirements | No paid body |\n| Different token or recipient offered | Reject through the client policy | No signature generated |\n| Invalid signature | Block due to verification failure | Paid handler does not run |\n| Insufficient balance | Block due to verification or settlement failure | Not recorded as successful settlement |\n| Failed settlement result | Block the paid response | Do not send even a prepared body |\n| Delayed facilitator response | Keep pending because success is unconfirmed | Do not automatically generate a new authorization |\n| RPC receipt lookup error | Keep the transaction hash and query again | Do not conclude that payment failed |\n\nIn 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.\n\nSending 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.\n\n## Lost responses after settlement and recovery through redelivery\n\nStore 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](https://docs.x402.org/extensions/payment-identifier).\n\nThe 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.\n\n### Comparing duplicate payments and redelivery\n\nPreventing 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](https://eips.ethereum.org/EIPS/eip-3009).\n\n| Situation | On-chain authorization perspective | API operations perspective |\n|---|---|---|\n| Settling the same authorization again | Subject to the check for an already-used nonce | Existing result must be retrieved |\n| Creating a new authorization for the same order | May be treated as a separate authorization | Duplicate payment prevention is needed |\n| Response lost after successful settlement | Asset transfer may already be complete | Content must be delivered again |\n| Timeout after a settlement request | Result is still unclear | Pending state and settlement confirmation are needed |\n\nThe 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.\n\n1. Declare support for the `payment-identifier` extension on the server.\n2. Keep the identifier for the logical request on the client.\n3. Associate the identifier with the payer and request contents on the server.\n4. Record the processing state atomically to prevent concurrent execution.\n5. Save the transaction evidence and response result when settlement succeeds.\n6. Return the stored result if the repeated request has the same authorization and contents.\n7. If the settlement result is unclear, check the existing authorization first.\n\nA 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.\n\nKeep 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.\n\n## Common payment implementation mistakes\n\nConfusing 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](https://docs.x402.org/guides/migration-v1-to-v2).\n\n| Common mistake | Correct approach |\n|---|---|\n| Interpreting `extra.version: \"2\"` as the x402 version | Check the protocol version through `x402Version` |\n| Using `maxAmountRequired` in v2 | Use `amount` for the amount in payment requirements |\n| Creating an `X-PAYMENT-REQUIRED` header arbitrarily | Use the body in v1 and `PAYMENT-REQUIRED` in v2 |\n| Treating `isValid: true` as completed receipt of funds | Check the `/settle` result separately |\n| Treating the transaction's top-level `to` as the USDC recipient | Check USDC `Transfer.to` |\n| Assuming the signer is the transaction sender | Compare the authorizing payer with `Transfer.from` |\n| Adding testnet receipts to revenue | Keep mainnet and testnet ledgers separate |\n| Creating a new payment on every timeout | Recover the existing order and authorization state first |\n| Treating the SDK package version as the protocol version | Manage the package version and `x402Version` separately |\n\nUSDC signature domain information also contains a version string. That value serves a different purpose from the x402 protocol version. See the [ERC-3009 specification](https://eips.ethereum.org/EIPS/eip-3009) for the signing domain structure.\n\n## Security checks and trust boundaries for external services\n\nControl 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](https://github.com/x402-foundation/x402/blob/main/docs/core-concepts/facilitator.md).\n\n- Do not release content based on a client's success message.\n- Compare the chain, token, amount, and recipient declared by the server.\n- Do not log private keys or seed phrases.\n- Exclude full authorization signatures from ordinary access logs as well.\n- Limit payment header size and request processing time.\n- Check that paid responses do not remain in shared caches.\n- Store test keys and production keys in separate locations.\n- Do not create an unpaid bypass when the facilitator fails.\n- Actually test authorization expiration and duplicate requests.\n\nThe pinned dependencies in this exercise still have security warnings. `npm audit` reported a zod-related advisory. See the [GitHub security advisory](https://github.com/advisories/GHSA-mrhx-c7xc-m3w5) for details.\n\n```bash\nnpm audit\n```\n\nBefore 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.\n\n### Comparing a self-hosted server and Cloudflare Gateway\n\nUsing 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](https://developers.cloudflare.com/monetization-gateway/x402/).\n\n| Category | Express approach in this article | Cloudflare Monetization Gateway |\n|---|---|---|\n| Client payment processing | Express middleware | Gateway |\n| Origin server verification point | Integration with the selected facilitator | `PAYMENT-CONTEXT` JWT verification |\n| Settlement result delivery | Middleware's `PAYMENT-RESPONSE` | Gateway generates a settlement receipt |\n| Additional checks | Facilitator's supported chains and authentication | JWT signature verification and origin access protection |\n\n`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](https://developers.cloudflare.com/monetization-gateway/configuration/payment-validation/) for origin verification.\n\nEven 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.\n\n## Distinguishing actual usage from settlement records\n\nThe 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.\n\n| Record | Question it can answer | Additional record needed |\n|---|---|---|\n| 402 response | How often payment requirements were requested | Whether payment actually occurred |\n| Successful verification | Whether the authorization was valid | Settlement result |\n| USDC transfer | Whether assets moved | Order association |\n| Settlement completed per order | Whether a paid request was paid for | Response delivery status |\n| Redelivery record | Whether an already-paid request was recovered | Identifier distinguishing it from a new payment |\n\nSeparate 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.\n\n## Steps for switching to Base mainnet\n\nReview 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](https://docs.x402.org/getting-started/quickstart-for-sellers#running-on-mainnet).\n\n1. Confirm that the selected facilitator supports Base mainnet.\n2. Check `/supported` for v2, `exact`, and `eip155:8453` support.\n3. Configure the authentication and terms of use required by the provider.\n4. Change the network on both the server and client.\n5. Change the client's allowed USDC address to the mainnet address.\n6. Recheck the amount in the 402 response against the actual receiving address.\n7. Connect the duplicate request store and failure recovery procedures.\n8. Complete security checks on production dependencies.\n9. Verify the actual transfer and content delivery using the small amount you configured.\n\nMultichain 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](https://docs.x402.org/core-concepts/network-and-token-support) as the reference.","content_html":"\u003cp\u003ex402 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.\u003c/p\u003e\n\u003cp\u003eAmounts and chain information are based on the official x402, Base, and Circle documentation linked as sources.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#setup-and-installation\" class=\"anchor\" id=\"setup-and-installation\"\u003e\u003c/a\u003eSetup and installation\u003c/h2\u003e\n\u003cp\u003ePrepare 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 \u003ca href=\"https://docs.x402.org/getting-started/quickstart-for-sellers\"\u003eofficial seller guide\u003c/a\u003e.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePrepare an EVM wallet address to receive payments.\u003c/li\u003e\n\u003cli\u003ePrepare a separate test wallet for making payments.\u003c/li\u003e\n\u003cli\u003eObtain Base Sepolia test USDC for the payment wallet.\u003c/li\u003e\n\u003cli\u003eCreate the practice project with the commands below.\u003c/li\u003e\n\u003cli\u003eSave the TypeScript files that follow in the same folder.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003emkdir\u003c/span\u003e\u003cspan\u003e x402-demo\n\u003c/span\u003e\u003cspan\u003ecd\u003c/span\u003e\u003cspan\u003e x402-demo\n\u003c/span\u003e\u003cspan\u003enpm\u003c/span\u003e\u003cspan\u003e init\u003c/span\u003e\u003cspan\u003e -y\n\u003c/span\u003e\u003cspan\u003enpm\u003c/span\u003e\u003cspan\u003e pkg set type=module\n\u003c/span\u003e\u003cspan\u003enpm\u003c/span\u003e\u003cspan\u003e install\u003c/span\u003e\u003cspan\u003e --save-exact\u003c/span\u003e\u003cspan\u003e express@5.3.0 viem@2.57.12 @x402/core@2.28.0 @x402/express@2.28.0 @x402/evm@2.28.0 @x402/fetch@2.28.0\n\u003c/span\u003e\u003cspan\u003enpm\u003c/span\u003e\u003cspan\u003e install\u003c/span\u003e\u003cspan\u003e --save-dev --save-exact\u003c/span\u003e\u003cspan\u003e typescript tsx @types/node @types/express\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe 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 \u003ca href=\"https://docs.x402.org/getting-started/quickstart-for-buyers\"\u003eofficial buyer guide\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#comparing-x402-v1-and-v2\" class=\"anchor\" id=\"comparing-x402-v1-and-v2\"\u003e\u003c/a\u003eComparing x402 v1 and v2\u003c/h2\u003e\n\u003cp\u003eWhen 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 \u003ca href=\"https://docs.x402.org/guides/migration-v1-to-v2\"\u003eofficial migration guide\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCategory\u003c/th\u003e\n\u003cth\u003ev1\u003c/th\u003e\n\u003cth\u003ev2\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eProtocol version\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\u003ccode\u003ex402Version: 1\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\u003ccode\u003ex402Version: 2\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eInitial payment requirements\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003eJSON body of the 402 response\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\n\u003ccode\u003ePAYMENT-REQUIRED\u003c/code\u003e response header\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eSigned payment data\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\n\u003ccode\u003eX-PAYMENT\u003c/code\u003e request header\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\n\u003ccode\u003ePAYMENT-SIGNATURE\u003c/code\u003e request header\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eSettlement result\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\n\u003ccode\u003eX-PAYMENT-RESPONSE\u003c/code\u003e response header\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\n\u003ccode\u003ePAYMENT-RESPONSE\u003c/code\u003e response header\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eBase mainnet identifier\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\u003ccode\u003ebase\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\u003ccode\u003eeip155:8453\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eBase Sepolia identifier\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\u003ccode\u003ebase-sepolia\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\u003ccode\u003eeip155:84532\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eAmount field in payment requirements\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\u003ccode\u003emaxAmountRequired\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\u003ccode\u003eamount\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eResource description\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\n\u003ccode\u003eresource\u003c/code\u003e and other fields within payment requirements\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003eTop-level \u003ccode\u003eresource\u003c/code\u003e object\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eRequirements in the payment payload\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003eTop-level \u003ccode\u003escheme\u003c/code\u003e, \u003ccode\u003enetwork\u003c/code\u003e, and other fields\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\n\u003ccode\u003eaccepted\u003c/code\u003e object containing the selected requirements\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eExample TypeScript package\u003c/td\u003e\n\u003ctd data-label=\"v1\"\u003e\u003ccode\u003ex402-express\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"v2\"\u003e\u003ccode\u003e@x402/express\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eThe location for delivering v1 payment requirements is specified in the \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/specs/transports-v1/http.md\"\u003eHTTP v1 specification\u003c/a\u003e. The standard location in v2 is the \u003ccode\u003ePAYMENT-REQUIRED\u003c/code\u003e header. An implementation may also return a body, but process the requirements using the header.\u003c/p\u003e\n\u003cp\u003eThe \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md\"\u003eHTTP v2 specification\u003c/a\u003e describes the status code as follows.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThe server indicates payment is required using the HTTP 402 \"Payment Required\" status code.\u003c/p\u003e\n\u003cp\u003eSource: x402 HTTP v2 specification\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003ev2 payment headers contain Base64-encoded JSON. Base64 is not encryption. Use HTTPS for externally accessible services.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#http-402-requests-and-the-payment-sequence\" class=\"anchor\" id=\"http-402-requests-and-the-payment-sequence\"\u003e\u003c/a\u003eHTTP 402 requests and the payment sequence\u003c/h2\u003e\n\u003cp\u003eThis exercise uses the default authorization flow for the EVM \u003ccode\u003eexact\u003c/code\u003e scheme. \u003ccode\u003eexact\u003c/code\u003e is a scheme for paying a specified amount. The sequence follows the \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md\"\u003ex402 v2 core specification\u003c/a\u003e.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eThe client requests a paid API without payment information.\u003c/li\u003e\n\u003cli\u003eThe server returns HTTP 402 and \u003ccode\u003ePAYMENT-REQUIRED\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eThe client selects an allowed set of requirements from \u003ccode\u003eaccepts\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eThe SDK signs a payment authorization matching the selected requirements.\u003c/li\u003e\n\u003cli\u003eThe client retries the request with \u003ccode\u003ePAYMENT-SIGNATURE\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eThe server validates the authorization through the facilitator's \u003ccode\u003e/verify\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eIf verification passes, the server processes the resource.\u003c/li\u003e\n\u003cli\u003eThe server requests settlement through \u003ccode\u003e/settle\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAfter settlement succeeds, it delivers the paid body and \u003ccode\u003ePAYMENT-RESPONSE\u003c/code\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThis 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 \u003ccode\u003eauthorization\u003c/code\u003e flow.\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#roles-of-the-resource-server-and-facilitator\" class=\"anchor\" id=\"roles-of-the-resource-server-and-facilitator\"\u003e\u003c/a\u003eRoles of the Resource server and facilitator\u003c/h3\u003e\n\u003cp\u003eThe Resource server determines prices and conditions for accessing content. The Facilitator handles payment verification and settlement. This division of roles follows the \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/docs/core-concepts/facilitator.md\"\u003eofficial facilitator documentation\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eComponent\u003c/th\u003e\n\u003cth\u003eResponsibility\u003c/th\u003e\n\u003cth\u003eState that must not be treated as success\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Component\"\u003eClient\u003c/td\u003e\n\u003ctd data-label=\"Responsibility\"\u003eSelects requirements and signs payment authorizations\u003c/td\u003e\n\u003ctd data-label=\"State that must not be treated as success\"\u003eOnly the signature has been generated\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Component\"\u003eResource server\u003c/td\u003e\n\u003ctd data-label=\"Responsibility\"\u003eDeclares prices and controls paid responses\u003c/td\u003e\n\u003ctd data-label=\"State that must not be treated as success\"\u003eThe client claims success\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Component\"\u003eFacilitator \u003ccode\u003e/verify\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-label=\"Responsibility\"\u003eChecks the validity of signatures and payment requirements\u003c/td\u003e\n\u003ctd data-label=\"State that must not be treated as success\"\u003eOnly \u003ccode\u003eisValid: true\u003c/code\u003e has been received\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Component\"\u003eFacilitator \u003ccode\u003e/settle\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-label=\"Responsibility\"\u003eExecutes the on-chain payment and returns the result in this exercise\u003c/td\u003e\n\u003ctd data-label=\"State that must not be treated as success\"\u003eSubmitted, but the result has not been confirmed\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003e\u003ccode\u003e/verify\u003c/code\u003e 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.\u003c/p\u003e\n\u003cp\u003eBlock 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 \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/typescript/packages/http/express/src/index.ts\"\u003eofficial Express implementation\u003c/a\u003e.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#comparing-base-mainnet-and-testnet\" class=\"anchor\" id=\"comparing-base-mainnet-and-testnet\"\u003e\u003c/a\u003eComparing Base mainnet and testnet\u003c/h2\u003e\n\u003cp\u003eUse only Base Sepolia for the first run. Circle states that testnet USDC has no monetary value. The addresses are based on \u003ca href=\"https://developers.circle.com/stablecoins/usdc-contract-addresses\"\u003eCircle's list of USDC contracts\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eItem\u003c/th\u003e\n\u003cth\u003eBase mainnet\u003c/th\u003e\n\u003cth\u003eBase Sepolia testnet\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003ePurpose\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003ePayments with real assets\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003eTesting implementation and failure handling\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003eChain ID\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003e\u003ccode\u003e8453\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003e\u003ccode\u003e84532\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003ex402 v2 network\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003e\u003ccode\u003eeip155:8453\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003e\u003ccode\u003eeip155:84532\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003eUSDC contract\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003e\u003ccode\u003e0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003e\u003ccode\u003e0x036CbD53842c5426634e7929541eC2318f3dCF7e\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003ePublic RPC\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003e\u003ccode\u003ehttps://mainnet.base.org\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003e\u003ccode\u003ehttps://sepolia.base.org\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Item\"\u003eToken's monetary value\u003c/td\u003e\n\u003ctd data-label=\"Base mainnet\"\u003eAsset with real monetary value\u003c/td\u003e\n\u003ctd data-label=\"Base Sepolia testnet\"\u003eNo monetary value\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eCheck the Chain ID and RPC in the \u003ca href=\"https://docs.base.org/get-started/connect-to-base\"\u003eBase network documentation\u003c/a\u003e. Even for the same wallet address, balances are separate on different chains. Do not record testnet success as actual revenue.\u003c/p\u003e\n\u003cp\u003eYou can request test USDC from the \u003ca href=\"https://faucet.circle.com/\"\u003eCircle Testnet Faucet\u003c/a\u003e. Select Base Sepolia as the network. Enter the test payment wallet's address as the receiving address.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#implementing-a-paid-api-in-nodejs\" class=\"anchor\" id=\"implementing-a-paid-api-in-nodejs\"\u003e\u003c/a\u003eImplementing a paid API in Node.js\u003c/h2\u003e\n\u003cp\u003eAttach \u003ccode\u003epaymentMiddleware\u003c/code\u003e before the paid route. The example below requires \u003cstrong\u003e0.001 USDC\u003c/strong\u003e 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)\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#server-file\" class=\"anchor\" id=\"server-file\"\u003e\u003c/a\u003eServer file\u003c/h3\u003e\n\u003cp\u003eSave the following code as \u003ccode\u003eserver.ts\u003c/code\u003e. The server accepts requests only on the local interface. It reads the receiving address from the \u003ccode\u003ePAY_TO\u003c/code\u003e environment variable.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003eimport express from \"express\";\n\u003c/span\u003e\u003cspan\u003eimport { getAddress, zeroAddress } from \"viem\";\n\u003c/span\u003e\u003cspan\u003eimport { paymentMiddleware } from \"@x402/express\";\n\u003c/span\u003e\u003cspan\u003eimport { HTTPFacilitatorClient, x402ResourceServer } from \"@x402/core/server\";\n\u003c/span\u003e\u003cspan\u003eimport { ExactEvmScheme } from \"@x402/evm/exact/server\";\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003econst payTo = getAddress(process.env.PAY_TO ?? \"\");\n\u003c/span\u003e\u003cspan\u003eif (payTo === zeroAddress) throw new Error(\"Invalid payTo\");\n\u003c/span\u003e\u003cspan\u003econst network = \"eip155:84532\";\n\u003c/span\u003e\u003cspan\u003econst resourceServer = new x402ResourceServer(\n\u003c/span\u003e\u003cspan\u003e  new HTTPFacilitatorClient({ url: \"https://x402.org/facilitator\" }),\n\u003c/span\u003e\u003cspan\u003e).register(network, new ExactEvmScheme());\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003econst app = express();\n\u003c/span\u003e\u003cspan\u003eapp.use((_req, res, next) =\u0026gt; {\n\u003c/span\u003e\u003cspan\u003e  res.setHeader(\"Cache-Control\", \"private, no-store\");\n\u003c/span\u003e\u003cspan\u003e  next();\n\u003c/span\u003e\u003cspan\u003e});\n\u003c/span\u003e\u003cspan\u003eapp.use(paymentMiddleware({\n\u003c/span\u003e\u003cspan\u003e  \"GET /paid-data\": {\n\u003c/span\u003e\u003cspan\u003e    accepts: [{ scheme: \"exact\", price: \"$0.001\", network, payTo }],\n\u003c/span\u003e\u003cspan\u003e    description: \"Sample paid JSON\",\n\u003c/span\u003e\u003cspan\u003e    mimeType: \"application/json\",\n\u003c/span\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003cspan\u003e}, resourceServer));\n\u003c/span\u003e\u003cspan\u003eapp.get(\"/paid-data\", (_req, res) =\u0026gt; {\n\u003c/span\u003e\u003cspan\u003e  res.json({ content: \"Paid sample content\" });\n\u003c/span\u003e\u003cspan\u003e});\n\u003c/span\u003e\u003cspan\u003eapp.listen(4021, \"127.0.0.1\");\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003ehttps://x402.org/facilitator\u003c/code\u003e is the testing address in the official guide. This example uses it for Base Sepolia settlement. For choosing a production facilitator, see the \u003ca href=\"https://docs.x402.org/getting-started/quickstart-for-sellers#running-on-mainnet\"\u003emainnet steps in the seller guide\u003c/a\u003e.\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#price-calculation-example\" class=\"anchor\" id=\"price-calculation-example\"\u003e\u003c/a\u003ePrice calculation example\u003c/h3\u003e\n\u003cp\u003eThe example price is \u003ccode\u003e1000\u003c/code\u003e in USDC's smallest units. USDC has 6 decimal places. The SDK's price conversion is based on the \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts\"\u003eEVM server implementation\u003c/a\u003e.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003e0.001 USDC × 10^6 = 1000 smallest units\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eValue to check\u003c/th\u003e\n\u003cth\u003eExpected value in this example\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Value to check\"\u003e\n\u003ccode\u003eprice\u003c/code\u003e in the server configuration\u003c/td\u003e\n\u003ctd data-label=\"Expected value in this example\"\u003e\u003ccode\u003e\"$0.001\"\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Value to check\"\u003e\n\u003ccode\u003eamount\u003c/code\u003e in the 402 payment requirements\u003c/td\u003e\n\u003ctd data-label=\"Expected value in this example\"\u003e\u003ccode\u003e\"1000\"\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Value to check\"\u003e\n\u003ccode\u003easset\u003c/code\u003e in the payment requirements\u003c/td\u003e\n\u003ctd data-label=\"Expected value in this example\"\u003eBase Sepolia USDC contract\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Value to check\"\u003e\n\u003ccode\u003epayTo\u003c/code\u003e in the payment requirements\u003c/td\u003e\n\u003ctd data-label=\"Expected value in this example\"\u003eConfigured receiving address\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Value to check\"\u003e\n\u003ccode\u003enetwork\u003c/code\u003e in the payment requirements\u003c/td\u003e\n\u003ctd data-label=\"Expected value in this example\"\u003e\u003ccode\u003eeip155:84532\u003c/code\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003e\u003ccode\u003eprice\u003c/code\u003e is an SDK configuration value. The actual v2 payment requirements carry \u003ccode\u003eamount\u003c/code\u003e. Compare integer amounts as strings or \u003ccode\u003ebigint\u003c/code\u003e.\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#running-the-server-and-checking-the-402-response\" class=\"anchor\" id=\"running-the-server-and-checking-the-402-response\"\u003e\u003c/a\u003eRunning the server and checking the 402 response\u003c/h3\u003e\n\u003cp\u003eRun 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.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003eread \u003c/span\u003e\u003cspan\u003e-r -p \u003c/span\u003e\u003cspan\u003e'\u003c/span\u003e\u003cspan\u003e수취 지갑 주소: \u003c/span\u003e\u003cspan\u003e' PAY_TO\n\u003c/span\u003e\u003cspan\u003eexport \u003c/span\u003e\u003cspan\u003ePAY_TO\n\u003c/span\u003e\u003cspan\u003enpx\u003c/span\u003e\u003cspan\u003e tsx server.ts\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eSend 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.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003enode\u003c/span\u003e\u003cspan\u003e --input-type\u003c/span\u003e\u003cspan\u003e=module \u0026lt;\u0026lt;'\u003c/span\u003e\u003cspan\u003eJS\u003c/span\u003e\u003cspan\u003e'\n\u003c/span\u003e\u003cspan\u003econst response = await fetch(\"http://127.0.0.1:4021/paid-data\");\n\u003c/span\u003e\u003cspan\u003econst header = response.headers.get(\"PAYMENT-REQUIRED\");\n\u003c/span\u003e\u003cspan\u003econsole.log(response.status);\n\u003c/span\u003e\u003cspan\u003econsole.log(header \u0026amp;\u0026amp; JSON.parse(\n\u003c/span\u003e\u003cspan\u003e  Buffer.from(header, \"base64\").toString(\"utf8\")\n\u003c/span\u003e\u003cspan\u003e));\n\u003c/span\u003e\u003cspan\u003eJS\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#selecting-payment-requirements-and-implementing-the-client\" class=\"anchor\" id=\"selecting-payment-requirements-and-implementing-the-client\"\u003e\u003c/a\u003eSelecting payment requirements and implementing the client\u003c/h2\u003e\n\u003cp\u003eRestrict the client to signing only approved requirements. This example allows only a fixed price in Base Sepolia USDC. The payment option filter uses \u003ccode\u003eregisterPolicy\u003c/code\u003e from the \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/typescript/packages/core/src/client/x402Client.ts\"\u003eofficial x402Client implementation\u003c/a\u003e.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSave the code below as \u003ccode\u003eclient.ts\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eAlso save \u003ccode\u003echeck-transfer.ts\u003c/code\u003e from the next section.\u003c/li\u003e\n\u003cli\u003eOnce both files are ready, proceed with the execution steps.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003eimport { writeFile } from \"node:fs/promises\";\n\u003c/span\u003e\u003cspan\u003eimport { getAddress, zeroAddress, type Hex, type Hash } from \"viem\";\n\u003c/span\u003e\u003cspan\u003eimport { privateKeyToAccount } from \"viem/accounts\";\n\u003c/span\u003e\u003cspan\u003eimport { x402Client, wrapFetchWithPayment } from \"@x402/fetch\";\n\u003c/span\u003e\u003cspan\u003eimport { decodePaymentResponseHeader } from \"@x402/core/http\";\n\u003c/span\u003e\u003cspan\u003eimport { ExactEvmScheme } from \"@x402/evm/exact/client\";\n\u003c/span\u003e\u003cspan\u003eimport { checkTransfer } from \"./check-transfer.js\";\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003econst key = process.env.EVM_PRIVATE_KEY ?? \"\";\n\u003c/span\u003e\u003cspan\u003eif (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error(\"Invalid test key\");\n\u003c/span\u003e\u003cspan\u003econst signer = privateKeyToAccount(key as Hex);\n\u003c/span\u003e\u003cspan\u003econst payTo = getAddress(process.env.PAY_TO ?? \"\");\n\u003c/span\u003e\u003cspan\u003eif (payTo === zeroAddress) throw new Error(\"Invalid payTo\");\n\u003c/span\u003e\u003cspan\u003econst network = \"eip155:84532\";\n\u003c/span\u003e\u003cspan\u003econst token = \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\";\n\u003c/span\u003e\u003cspan\u003econst client = new x402Client().register(network, new ExactEvmScheme(signer));\n\u003c/span\u003e\u003cspan\u003eclient.registerPolicy((version, options) =\u0026gt; version === 2\n\u003c/span\u003e\u003cspan\u003e  ? options.filter(r =\u0026gt;\n\u003c/span\u003e\u003cspan\u003e      r.scheme === \"exact\" \u0026amp;\u0026amp; r.network === network \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e      r.asset.toLowerCase() === token.toLowerCase() \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e      r.payTo.toLowerCase() === payTo.toLowerCase() \u0026amp;\u0026amp; r.amount === \"1000\" \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e      (!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === \"eip3009\") \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e      (!r.extra?.paymentFlow || r.extra.paymentFlow === \"authorization\"))\n\u003c/span\u003e\u003cspan\u003e  : []);\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003econst paidFetch = wrapFetchWithPayment(fetch, client);\n\u003c/span\u003e\u003cspan\u003econst response = await paidFetch(\"http://127.0.0.1:4021/paid-data\", {\n\u003c/span\u003e\u003cspan\u003e  redirect: \"error\",\n\u003c/span\u003e\u003cspan\u003e});\n\u003c/span\u003e\u003cspan\u003econst raw = response.headers.get(\"PAYMENT-RESPONSE\");\n\u003c/span\u003e\u003cspan\u003eif (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);\n\u003c/span\u003e\u003cspan\u003econst settlement = decodePaymentResponseHeader(raw);\n\u003c/span\u003e\u003cspan\u003eawait writeFile(\"settlement.json\", JSON.stringify(settlement, null, 2));\n\u003c/span\u003e\u003cspan\u003eif (!response.ok || !settlement.success || settlement.network !== network) {\n\u003c/span\u003e\u003cspan\u003e  throw new Error(\"Payment unresolved; inspect settlement.json before retrying\");\n\u003c/span\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003cspan\u003econst evidence = await checkTransfer(\n\u003c/span\u003e\u003cspan\u003e  network, settlement.transaction as Hash, signer.address, payTo, 1000n,\n\u003c/span\u003e\u003cspan\u003e);\n\u003c/span\u003e\u003cspan\u003eawait writeFile(\"transfer.json\", JSON.stringify(evidence, null, 2));\n\u003c/span\u003e\u003cspan\u003econsole.log(await response.text());\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003ewrapFetchWithPayment\u003c/code\u003e handles the 402 response and retries the request with a signature. Its usage follows the \u003ca href=\"https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients/fetch\"\u003eofficial fetch example\u003c/a\u003e. If no payment requirements are allowed, payment creation stops.\u003c/p\u003e\n\u003cp\u003eAI 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#verifying-usdc-transfer-events\" class=\"anchor\" id=\"verifying-usdc-transfer-events\"\u003e\u003c/a\u003eVerifying USDC Transfer events\u003c/h2\u003e\n\u003cp\u003eCheck the USDC transfer logs using the transaction hash from the settlement result. A successful transaction status alone does not confirm the recipient. The \u003ccode\u003eTransfer\u003c/code\u003e fields follow the \u003ca href=\"https://eips.ethereum.org/EIPS/eip-20\"\u003eERC-20 standard\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eVerification requires the following items.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eVerification item\u003c/th\u003e\n\u003cth\u003eCompare against\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003eRPC's Chain ID\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eChain allowed by the server\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003eTransaction receipt's \u003ccode\u003estatus\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eSuccess status\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003eAddress that emitted the event\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eOfficial USDC contract on that chain\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003e\u003ccode\u003eTransfer.from\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eVerified payer\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003e\u003ccode\u003eTransfer.to\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eReceiving address set by the server\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Verification item\"\u003e\u003ccode\u003eTransfer.value\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Compare against\"\u003eAmount required by the server in the smallest units\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eSave the code below as \u003ccode\u003echeck-transfer.ts\u003c/code\u003e. The lookup uses viem's \u003ca href=\"https://viem.sh/docs/actions/public/getTransactionReceipt\"\u003egetTransactionReceipt\u003c/a\u003e. Event parsing is based on \u003ca href=\"https://viem.sh/docs/contract/parseEventLogs\"\u003eparseEventLogs\u003c/a\u003e.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003eimport {\n\u003c/span\u003e\u003cspan\u003e  createPublicClient, http, getAddress, parseEventLogs,\n\u003c/span\u003e\u003cspan\u003e  erc20Abi, type Address, type Hash,\n\u003c/span\u003e\u003cspan\u003e} from \"viem\";\n\u003c/span\u003e\u003cspan\u003eimport { base, baseSepolia } from \"viem/chains\";\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003econst configs = {\n\u003c/span\u003e\u003cspan\u003e  \"eip155:84532\": {\n\u003c/span\u003e\u003cspan\u003e    chain: baseSepolia,\n\u003c/span\u003e\u003cspan\u003e    rpc: \"https://sepolia.base.org\",\n\u003c/span\u003e\u003cspan\u003e    token: \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\",\n\u003c/span\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003cspan\u003e  \"eip155:8453\": {\n\u003c/span\u003e\u003cspan\u003e    chain: base,\n\u003c/span\u003e\u003cspan\u003e    rpc: \"https://mainnet.base.org\",\n\u003c/span\u003e\u003cspan\u003e    token: \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\n\u003c/span\u003e\u003cspan\u003e  },\n\u003c/span\u003e\u003cspan\u003e} as const;\n\u003c/span\u003e\u003cspan\u003e\n\u003c/span\u003e\u003cspan\u003eexport async function checkTransfer(\n\u003c/span\u003e\u003cspan\u003e  network: keyof typeof configs, hash: Hash,\n\u003c/span\u003e\u003cspan\u003e  payer: Address, payTo: Address, amount: bigint,\n\u003c/span\u003e\u003cspan\u003e) {\n\u003c/span\u003e\u003cspan\u003e  const cfg = configs[network];\n\u003c/span\u003e\u003cspan\u003e  const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });\n\u003c/span\u003e\u003cspan\u003e  if (await rpc.getChainId() !== cfg.chain.id) throw new Error(\"Wrong chain\");\n\u003c/span\u003e\u003cspan\u003e  const receipt = await rpc.getTransactionReceipt({ hash });\n\u003c/span\u003e\u003cspan\u003e  if (receipt.status !== \"success\") throw new Error(\"Transaction failed\");\n\u003c/span\u003e\u003cspan\u003e  const logs = parseEventLogs({\n\u003c/span\u003e\u003cspan\u003e    abi: erc20Abi,\n\u003c/span\u003e\u003cspan\u003e    eventName: \"Transfer\",\n\u003c/span\u003e\u003cspan\u003e    logs: receipt.logs.filter(\n\u003c/span\u003e\u003cspan\u003e      log =\u0026gt; getAddress(log.address) === getAddress(cfg.token),\n\u003c/span\u003e\u003cspan\u003e    ),\n\u003c/span\u003e\u003cspan\u003e    strict: true,\n\u003c/span\u003e\u003cspan\u003e  });\n\u003c/span\u003e\u003cspan\u003e  const transfer = logs.find(log =\u0026gt;\n\u003c/span\u003e\u003cspan\u003e    getAddress(log.args.from) === getAddress(payer) \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e    getAddress(log.args.to) === getAddress(payTo) \u0026amp;\u0026amp;\n\u003c/span\u003e\u003cspan\u003e    log.args.value === amount,\n\u003c/span\u003e\u003cspan\u003e  );\n\u003c/span\u003e\u003cspan\u003e  if (!transfer) throw new Error(\"Matching USDC Transfer not found\");\n\u003c/span\u003e\u003cspan\u003e  return {\n\u003c/span\u003e\u003cspan\u003e    network, transaction: hash, logIndex: transfer.logIndex,\n\u003c/span\u003e\u003cspan\u003e    blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),\n\u003c/span\u003e\u003cspan\u003e  };\n\u003c/span\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThis 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.\u003c/p\u003e\n\u003cp\u003eDo not trust a client-uploaded \u003ccode\u003etransfer.json\u003c/code\u003e 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.\u003c/p\u003e\n\u003cp\u003eA 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#running-a-test-payment-and-checking-the-results\" class=\"anchor\" id=\"running-a-test-payment-and-checking-the-results\"\u003e\u003c/a\u003eRunning a test payment and checking the results\u003c/h2\u003e\n\u003cp\u003eAfter 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.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCheck the types with the following command.\u003c/li\u003e\n\u003cli\u003eEnter the same receiving address as the server in the second terminal.\u003c/li\u003e\n\u003cli\u003eEnter the test payment wallet's private key with input hidden.\u003c/li\u003e\n\u003cli\u003eRun the client once.\u003c/li\u003e\n\u003cli\u003eCheck the saved settlement result and transfer log.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003enpx\u003c/span\u003e\u003cspan\u003e tsc\u003c/span\u003e\u003cspan\u003e --noEmit --strict --skipLibCheck --target\u003c/span\u003e\u003cspan\u003e ES2022\u003c/span\u003e\u003cspan\u003e --module\u003c/span\u003e\u003cspan\u003e NodeNext\u003c/span\u003e\u003cspan\u003e --moduleResolution\u003c/span\u003e\u003cspan\u003e NodeNext server.ts client.ts check-transfer.ts\n\u003c/span\u003e\u003cspan\u003eread \u003c/span\u003e\u003cspan\u003e-r -p \u003c/span\u003e\u003cspan\u003e'\u003c/span\u003e\u003cspan\u003e같은 수취 지갑 주소: \u003c/span\u003e\u003cspan\u003e' PAY_TO\n\u003c/span\u003e\u003cspan\u003eread \u003c/span\u003e\u003cspan\u003e-r -s -p \u003c/span\u003e\u003cspan\u003e'\u003c/span\u003e\u003cspan\u003e테스트 결제 지갑 개인키: \u003c/span\u003e\u003cspan\u003e' EVM_PRIVATE_KEY\n\u003c/span\u003e\u003cspan\u003eexport \u003c/span\u003e\u003cspan\u003ePAY_TO EVM_PRIVATE_KEY\n\u003c/span\u003e\u003cspan\u003enpx\u003c/span\u003e\u003cspan\u003e tsx client.ts\n\u003c/span\u003e\u003cspan\u003eunset\u003c/span\u003e\u003cspan\u003e EVM_PRIVATE_KEY\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eOutput\u003c/th\u003e\n\u003cth\u003eMeaning\u003c/th\u003e\n\u003cth\u003eWhat to check\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Output\"\u003e\u003ccode\u003esettlement.json\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Meaning\"\u003eSettlement result returned by the server\u003c/td\u003e\n\u003ctd data-label=\"What to check\"\u003e\n\u003ccode\u003esuccess\u003c/code\u003e, \u003ccode\u003enetwork\u003c/code\u003e, \u003ccode\u003etransaction\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Output\"\u003e\u003ccode\u003etransfer.json\u003c/code\u003e\u003c/td\u003e\n\u003ctd data-label=\"Meaning\"\u003eTransfer record found through the RPC lookup\u003c/td\u003e\n\u003ctd data-label=\"What to check\"\u003eChain, transaction hash, log position, block information\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Output\"\u003ePaid body in the console\u003c/td\u003e\n\u003ctd data-label=\"Meaning\"\u003eData printed after this client's verification\u003c/td\u003e\n\u003ctd data-label=\"What to check\"\u003eConfirm that it is the requested content\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eRunning 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#handling-verification-failure-conditions\" class=\"anchor\" id=\"handling-verification-failure-conditions\"\u003e\u003c/a\u003eHandling verification failure conditions\u003c/h2\u003e\n\u003cp\u003eIn 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 \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/typescript/packages/http/express/src/index.ts\"\u003eofficial Express middleware\u003c/a\u003e for the baseline behavior.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTest condition\u003c/th\u003e\n\u003cth\u003eExpected handling\u003c/th\u003e\n\u003cth\u003eAdditional check\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eNo payment header\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eReturn 402 and payment requirements\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eNo paid body\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eDifferent token or recipient offered\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eReject through the client policy\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eNo signature generated\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eInvalid signature\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eBlock due to verification failure\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003ePaid handler does not run\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eInsufficient balance\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eBlock due to verification or settlement failure\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eNot recorded as successful settlement\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eFailed settlement result\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eBlock the paid response\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eDo not send even a prepared body\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eDelayed facilitator response\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eKeep pending because success is unconfirmed\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eDo not automatically generate a new authorization\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Test condition\"\u003eRPC receipt lookup error\u003c/td\u003e\n\u003ctd data-label=\"Expected handling\"\u003eKeep the transaction hash and query again\u003c/td\u003e\n\u003ctd data-label=\"Additional check\"\u003eDo not conclude that payment failed\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eIn 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.\u003c/p\u003e\n\u003cp\u003eSending 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#lost-responses-after-settlement-and-recovery-through-redelivery\" class=\"anchor\" id=\"lost-responses-after-settlement-and-recovery-through-redelivery\"\u003e\u003c/a\u003eLost responses after settlement and recovery through redelivery\u003c/h2\u003e\n\u003cp\u003eStore 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 \u003ca href=\"https://docs.x402.org/extensions/payment-identifier\"\u003ePayment-Identifier extension documentation\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThe property of not processing the same operation twice when it is requested again is called idempotency. \u003ccode\u003epayment-identifier\u003c/code\u003e provides a payment identifier for this purpose. Adding the identifier alone does not automatically complete the feature.\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#comparing-duplicate-payments-and-redelivery\" class=\"anchor\" id=\"comparing-duplicate-payments-and-redelivery\"\u003e\u003c/a\u003eComparing duplicate payments and redelivery\u003c/h3\u003e\n\u003cp\u003ePreventing 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 \u003ca href=\"https://eips.ethereum.org/EIPS/eip-3009\"\u003eERC-3009 specification\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eSituation\u003c/th\u003e\n\u003cth\u003eOn-chain authorization perspective\u003c/th\u003e\n\u003cth\u003eAPI operations perspective\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Situation\"\u003eSettling the same authorization again\u003c/td\u003e\n\u003ctd data-label=\"On-chain authorization perspective\"\u003eSubject to the check for an already-used nonce\u003c/td\u003e\n\u003ctd data-label=\"API operations perspective\"\u003eExisting result must be retrieved\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Situation\"\u003eCreating a new authorization for the same order\u003c/td\u003e\n\u003ctd data-label=\"On-chain authorization perspective\"\u003eMay be treated as a separate authorization\u003c/td\u003e\n\u003ctd data-label=\"API operations perspective\"\u003eDuplicate payment prevention is needed\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Situation\"\u003eResponse lost after successful settlement\u003c/td\u003e\n\u003ctd data-label=\"On-chain authorization perspective\"\u003eAsset transfer may already be complete\u003c/td\u003e\n\u003ctd data-label=\"API operations perspective\"\u003eContent must be delivered again\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Situation\"\u003eTimeout after a settlement request\u003c/td\u003e\n\u003ctd data-label=\"On-chain authorization perspective\"\u003eResult is still unclear\u003c/td\u003e\n\u003ctd data-label=\"API operations perspective\"\u003ePending state and settlement confirmation are needed\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eThe 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.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eDeclare support for the \u003ccode\u003epayment-identifier\u003c/code\u003e extension on the server.\u003c/li\u003e\n\u003cli\u003eKeep the identifier for the logical request on the client.\u003c/li\u003e\n\u003cli\u003eAssociate the identifier with the payer and request contents on the server.\u003c/li\u003e\n\u003cli\u003eRecord the processing state atomically to prevent concurrent execution.\u003c/li\u003e\n\u003cli\u003eSave the transaction evidence and response result when settlement succeeds.\u003c/li\u003e\n\u003cli\u003eReturn the stored result if the repeated request has the same authorization and contents.\u003c/li\u003e\n\u003cli\u003eIf the settlement result is unclear, check the existing authorization first.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eA 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.\u003c/p\u003e\n\u003cp\u003eKeep 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 \u003ccode\u003eTransfer\u003c/code\u003e alone does not prove which order it belongs to.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#common-payment-implementation-mistakes\" class=\"anchor\" id=\"common-payment-implementation-mistakes\"\u003e\u003c/a\u003eCommon payment implementation mistakes\u003c/h2\u003e\n\u003cp\u003eConfusing version fields and success signals leads to incorrect payment decisions. Use the table below during deployment review. Version differences are based on the \u003ca href=\"https://docs.x402.org/guides/migration-v1-to-v2\"\u003ex402 migration guide\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCommon mistake\u003c/th\u003e\n\u003cth\u003eCorrect approach\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eInterpreting \u003ccode\u003eextra.version: \"2\"\u003c/code\u003e as the x402 version\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eCheck the protocol version through \u003ccode\u003ex402Version\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eUsing \u003ccode\u003emaxAmountRequired\u003c/code\u003e in v2\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eUse \u003ccode\u003eamount\u003c/code\u003e for the amount in payment requirements\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eCreating an \u003ccode\u003eX-PAYMENT-REQUIRED\u003c/code\u003e header arbitrarily\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eUse the body in v1 and \u003ccode\u003ePAYMENT-REQUIRED\u003c/code\u003e in v2\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eTreating \u003ccode\u003eisValid: true\u003c/code\u003e as completed receipt of funds\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eCheck the \u003ccode\u003e/settle\u003c/code\u003e result separately\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eTreating the transaction's top-level \u003ccode\u003eto\u003c/code\u003e as the USDC recipient\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eCheck USDC \u003ccode\u003eTransfer.to\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eAssuming the signer is the transaction sender\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eCompare the authorizing payer with \u003ccode\u003eTransfer.from\u003c/code\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eAdding testnet receipts to revenue\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eKeep mainnet and testnet ledgers separate\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eCreating a new payment on every timeout\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eRecover the existing order and authorization state first\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Common mistake\"\u003eTreating the SDK package version as the protocol version\u003c/td\u003e\n\u003ctd data-label=\"Correct approach\"\u003eManage the package version and \u003ccode\u003ex402Version\u003c/code\u003e separately\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eUSDC signature domain information also contains a version string. That value serves a different purpose from the x402 protocol version. See the \u003ca href=\"https://eips.ethereum.org/EIPS/eip-3009\"\u003eERC-3009 specification\u003c/a\u003e for the signing domain structure.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#security-checks-and-trust-boundaries-for-external-services\" class=\"anchor\" id=\"security-checks-and-trust-boundaries-for-external-services\"\u003e\u003c/a\u003eSecurity checks and trust boundaries for external services\u003c/h2\u003e\n\u003cp\u003eControl 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 \u003ca href=\"https://github.com/x402-foundation/x402/blob/main/docs/core-concepts/facilitator.md\"\u003efacilitator documentation\u003c/a\u003e.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDo not release content based on a client's success message.\u003c/li\u003e\n\u003cli\u003eCompare the chain, token, amount, and recipient declared by the server.\u003c/li\u003e\n\u003cli\u003eDo not log private keys or seed phrases.\u003c/li\u003e\n\u003cli\u003eExclude full authorization signatures from ordinary access logs as well.\u003c/li\u003e\n\u003cli\u003eLimit payment header size and request processing time.\u003c/li\u003e\n\u003cli\u003eCheck that paid responses do not remain in shared caches.\u003c/li\u003e\n\u003cli\u003eStore test keys and production keys in separate locations.\u003c/li\u003e\n\u003cli\u003eDo not create an unpaid bypass when the facilitator fails.\u003c/li\u003e\n\u003cli\u003eActually test authorization expiration and duplicate requests.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe pinned dependencies in this exercise still have security warnings. \u003ccode\u003enpm audit\u003c/code\u003e reported a zod-related advisory. See the \u003ca href=\"https://github.com/advisories/GHSA-mrhx-c7xc-m3w5\"\u003eGitHub security advisory\u003c/a\u003e for details.\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u003cspan\u003enpm\u003c/span\u003e\u003cspan\u003e audit\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eBefore 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.\u003c/p\u003e\n\u003ch3\u003e\n\u003ca href=\"#comparing-a-self-hosted-server-and-cloudflare-gateway\" class=\"anchor\" id=\"comparing-a-self-hosted-server-and-cloudflare-gateway\"\u003e\u003c/a\u003eComparing a self-hosted server and Cloudflare Gateway\u003c/h3\u003e\n\u003cp\u003eUsing 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 \u003ca href=\"https://developers.cloudflare.com/monetization-gateway/x402/\"\u003eCloudflare x402 documentation\u003c/a\u003e.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eCategory\u003c/th\u003e\n\u003cth\u003eExpress approach in this article\u003c/th\u003e\n\u003cth\u003eCloudflare Monetization Gateway\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eClient payment processing\u003c/td\u003e\n\u003ctd data-label=\"Express approach in this article\"\u003eExpress middleware\u003c/td\u003e\n\u003ctd data-label=\"Cloudflare Monetization Gateway\"\u003eGateway\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eOrigin server verification point\u003c/td\u003e\n\u003ctd data-label=\"Express approach in this article\"\u003eIntegration with the selected facilitator\u003c/td\u003e\n\u003ctd data-label=\"Cloudflare Monetization Gateway\"\u003e\n\u003ccode\u003ePAYMENT-CONTEXT\u003c/code\u003e JWT verification\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eSettlement result delivery\u003c/td\u003e\n\u003ctd data-label=\"Express approach in this article\"\u003eMiddleware's \u003ccode\u003ePAYMENT-RESPONSE\u003c/code\u003e\n\u003c/td\u003e\n\u003ctd data-label=\"Cloudflare Monetization Gateway\"\u003eGateway generates a settlement receipt\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Category\"\u003eAdditional checks\u003c/td\u003e\n\u003ctd data-label=\"Express approach in this article\"\u003eFacilitator's supported chains and authentication\u003c/td\u003e\n\u003ctd data-label=\"Cloudflare Monetization Gateway\"\u003eJWT signature verification and origin access protection\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003e\u003ccode\u003ePAYMENT-CONTEXT\u003c/code\u003e is a header between the Gateway and the origin server. It is not a signature header for a regular x402 client. Follow the \u003ca href=\"https://developers.cloudflare.com/monetization-gateway/configuration/payment-validation/\"\u003eCloudflare Payment validation documentation\u003c/a\u003e for origin verification.\u003c/p\u003e\n\u003cp\u003eEven 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#distinguishing-actual-usage-from-settlement-records\" class=\"anchor\" id=\"distinguishing-actual-usage-from-settlement-records\"\u003e\u003c/a\u003eDistinguishing actual usage from settlement records\u003c/h2\u003e\n\u003cp\u003eThe 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.\u003c/p\u003e\n\u003cdiv class=\"overflow-x-auto\"\u003e\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eRecord\u003c/th\u003e\n\u003cth\u003eQuestion it can answer\u003c/th\u003e\n\u003cth\u003eAdditional record needed\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Record\"\u003e402 response\u003c/td\u003e\n\u003ctd data-label=\"Question it can answer\"\u003eHow often payment requirements were requested\u003c/td\u003e\n\u003ctd data-label=\"Additional record needed\"\u003eWhether payment actually occurred\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Record\"\u003eSuccessful verification\u003c/td\u003e\n\u003ctd data-label=\"Question it can answer\"\u003eWhether the authorization was valid\u003c/td\u003e\n\u003ctd data-label=\"Additional record needed\"\u003eSettlement result\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Record\"\u003eUSDC transfer\u003c/td\u003e\n\u003ctd data-label=\"Question it can answer\"\u003eWhether assets moved\u003c/td\u003e\n\u003ctd data-label=\"Additional record needed\"\u003eOrder association\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Record\"\u003eSettlement completed per order\u003c/td\u003e\n\u003ctd data-label=\"Question it can answer\"\u003eWhether a paid request was paid for\u003c/td\u003e\n\u003ctd data-label=\"Additional record needed\"\u003eResponse delivery status\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd data-label=\"Record\"\u003eRedelivery record\u003c/td\u003e\n\u003ctd data-label=\"Question it can answer\"\u003eWhether an already-paid request was recovered\u003c/td\u003e\n\u003ctd data-label=\"Additional record needed\"\u003eIdentifier distinguishing it from a new payment\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\u003c/div\u003e\n\u003cp\u003eSeparate 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.\u003c/p\u003e\n\u003ch2\u003e\n\u003ca href=\"#steps-for-switching-to-base-mainnet\" class=\"anchor\" id=\"steps-for-switching-to-base-mainnet\"\u003e\u003c/a\u003eSteps for switching to Base mainnet\u003c/h2\u003e\n\u003cp\u003eReview 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 \u003ca href=\"https://docs.x402.org/getting-started/quickstart-for-sellers#running-on-mainnet\"\u003eofficial seller guide\u003c/a\u003e.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eConfirm that the selected facilitator supports Base mainnet.\u003c/li\u003e\n\u003cli\u003eCheck \u003ccode\u003e/supported\u003c/code\u003e for v2, \u003ccode\u003eexact\u003c/code\u003e, and \u003ccode\u003eeip155:8453\u003c/code\u003e support.\u003c/li\u003e\n\u003cli\u003eConfigure the authentication and terms of use required by the provider.\u003c/li\u003e\n\u003cli\u003eChange the network on both the server and client.\u003c/li\u003e\n\u003cli\u003eChange the client's allowed USDC address to the mainnet address.\u003c/li\u003e\n\u003cli\u003eRecheck the amount in the 402 response against the actual receiving address.\u003c/li\u003e\n\u003cli\u003eConnect the duplicate request store and failure recovery procedures.\u003c/li\u003e\n\u003cli\u003eComplete security checks on production dependencies.\u003c/li\u003e\n\u003cli\u003eVerify the actual transfer and content delivery using the small amount you configured.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eMultichain support does not mean every combination is supported. Check the support offered by both the SDK and the facilitator. Use the \u003ca href=\"https://docs.x402.org/core-concepts/network-and-token-support\"\u003enetwork and token support documentation\u003c/a\u003e as the reference.\u003c/p\u003e\n","tags":["Stablecoin","AI Agents","AI Development","Content monetization","Development Tools"],"faqs":[{"question":"How does x402 differ from a regular USDC deposit confirmation?","answer":"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."},{"question":"What are the payment headers in x402 v2?","answer":"Payment requirements are sent via PAYMENT-REQUIRED, and the payment payload via PAYMENT-SIGNATURE. The settlement result is sent via PAYMENT-RESPONSE."},{"question":"Does v1 have an X-PAYMENT-REQUIRED header?","answer":"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."},{"question":"Is payment complete if /verify succeeds?","answer":"A successful /verify means the payment authorization is valid. In this exercise, you must separately confirm that /settle succeeded."},{"question":"Can Base Sepolia test USDC be considered actual revenue?","answer":"Circle states that testnet USDC has no financial value. Record testnet settlements separately from actual sales."},{"question":"Do I need to put a private key on the receiving server?","answer":"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."},{"question":"What is the amount value for 0.001 USDC?","answer":"With USDC's 6 decimal places, this is 1000 in the smallest unit. Use the string \"1000\" in the v2 payment requirements."},{"question":"If the transaction receipt shows success, does that also confirm receipt of USDC?","answer":"A successful transaction status alone is not enough. Check the payer, recipient, and amount against the Transfer event from the official USDC contract."},{"question":"Is it safe to resend the same payment request?","answer":"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."},{"question":"If PAYMENT-RESPONSE is missing, should I pay again?","answer":"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."},{"question":"Does x402 require you to use only a specific company's facilitator?","answer":"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."},{"question":"Is an AI agent's spending also limited automatically?","answer":"This example only limits payment requirements per request. Manage cumulative spending from repeated calls with a separate budget policy."},{"question":"Is Cloudflare PAYMENT-CONTEXT a standard payment signature header?","answer":"PAYMENT-CONTEXT is a JWT that Gateway passes to the origin server. It serves a different purpose from the client's PAYMENT-SIGNATURE."},{"question":"Can I deploy the example directly to mainnet?","answer":"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":[{"url":"https://github.com/x402-foundation/x402/blob/main/specs/x402-specification-v2.md","title":"x402 Protocol Specification v2","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/specs/transports-v1/http.md","title":"x402 HTTP Transport v1","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md","title":"x402 HTTP Transport v2","type":"source"},{"url":"https://docs.x402.org/guides/migration-v1-to-v2","title":"x402 Migration Guide: V1 to V2","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/docs/core-concepts/facilitator.md","title":"x402 Facilitator Roles and Responsibilities","type":"source"},{"url":"https://docs.x402.org/getting-started/quickstart-for-sellers","title":"x402 Quickstart for Sellers","type":"source"},{"url":"https://docs.x402.org/getting-started/quickstart-for-buyers","title":"x402 Quickstart for Buyers","type":"source"},{"url":"https://github.com/x402-foundation/x402/tree/main/examples/typescript/clients/fetch","title":"x402 Official TypeScript fetch Client Example","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/typescript/packages/http/express/src/index.ts","title":"x402 Express Middleware Implementation","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/typescript/packages/core/src/client/x402Client.ts","title":"x402Client Payment Requirements Selection and Policy Implementation","type":"source"},{"url":"https://github.com/x402-foundation/x402/blob/main/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts","title":"x402 ExactEvmScheme Server Price Conversion Implementation","type":"source"},{"url":"https://developers.circle.com/stablecoins/usdc-contract-addresses","title":"Circle USDC Contract Addresses","type":"data_point"},{"url":"https://docs.base.org/get-started/connect-to-base","title":"Base Mainnet and Base Sepolia Network Information","type":"data_point"},{"url":"https://faucet.circle.com/","title":"Circle Testnet Faucet","type":"source"},{"url":"https://viem.sh/docs/actions/public/getTransactionReceipt","title":"viem getTransactionReceipt","type":"source"},{"url":"https://viem.sh/docs/contract/parseEventLogs","title":"viem parseEventLogs","type":"source"},{"url":"https://eips.ethereum.org/EIPS/eip-20","title":"ERC-20 Token Standard","type":"source"},{"url":"https://eips.ethereum.org/EIPS/eip-3009","title":"ERC-3009 Transfer With Authorization","type":"source"},{"url":"https://docs.x402.org/extensions/payment-identifier","title":"x402 Payment-Identifier Idempotency Extension","type":"source"},{"url":"https://developers.cloudflare.com/monetization-gateway/x402/","title":"Cloudflare Monetization Gateway x402 protocol","type":"source"},{"url":"https://developers.cloudflare.com/monetization-gateway/configuration/payment-validation/","title":"Cloudflare Monetization Gateway Payment validation","type":"source"},{"url":"https://docs.x402.org/core-concepts/network-and-token-support","title":"x402 Networks \u0026 Token Support","type":"source"},{"url":"https://github.com/advisories/GHSA-mrhx-c7xc-m3w5","title":"GitHub Security Advisory: Memory Exhaustion from Zod Validation of Large Collections","type":"source"}],"images":[{"id":2199,"url":"https://injoys.com/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzM4MDcsInB1ciI6ImJsb2JfaWQifX0=--49c502fd8da16158a617c3586dfa81ced4bc95df/ai-f0ae2b5c.webp","is_representative":true,"generation_method":"ai_photo","license":"ai_generated","mime_type":"image/webp","width":1536,"height":1024,"translations":{"ko":{"alt":"짙은 회색 스웨터를 입은 남성이 서버 랙 선반에 놓인 태블릿 화면을 살펴보고 있다.","caption":"메인넷으로 전환하기 전에 테스트 환경에서 x402 결제 연동을 검증하세요.","description":null},"en":{"alt":"A man in a dark gray sweater examines a tablet resting on a server rack shelf.","caption":"Validate the x402 payment integration in a test environment before switching to mainnet.","description":null},"ja":{"alt":"濃いグレーのセーターを着た男性が、サーバーラックの棚に置かれたタブレットを確認している。","caption":"メインネットへ移行する前に、テスト環境でx402の決済連携を検証しましょう。","description":null},"es":{"alt":"Un hombre con un suéter gris oscuro revisa una tableta sobre la bandeja de un rack de servidores.","caption":"Valida la integración de pagos x402 en un entorno de prueba antes de pasar a la red principal.","description":null},"id":{"alt":"Seorang pria bersweater abu-abu gelap memeriksa tablet di atas ambalan rak server.","caption":"Validasi integrasi pembayaran x402 di lingkungan uji sebelum beralih ke mainnet.","description":null},"pt":{"alt":"Um homem de suéter cinza-escuro examina um tablet apoiado na prateleira de um rack de servidores.","caption":"Valide a integração de pagamentos x402 em um ambiente de teste antes de migrar para a rede principal.","description":null},"zh-hant":{"alt":"一名穿著深灰色毛衣的男子正在查看伺服器機架層板上的平板電腦。","caption":"切換至主網前，先在測試環境中驗證 x402 支付整合。","description":null},"de":{"alt":"Ein Mann im dunkelgrauen Pullover prüft ein Tablet auf der Ablage eines Serverracks.","caption":"Prüfen Sie die x402-Zahlungsintegration in einer Testumgebung, bevor Sie ins Mainnet wechseln.","description":null}}},{"id":2200,"url":"https://injoys.com/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzM4MTMsInB1ciI6ImJsb2JfaWQifX0=--4e70727f132519277c68d1c6279d95585a936549/ai-3dbd1c62.webp","is_representative":false,"generation_method":"ai_semi","license":"ai_generated","mime_type":"image/webp","width":1536,"height":1024,"translations":{"ko":{"alt":"창가에서 여성이 평평하게 펼친 노트북으로 입력하고 있으며, 화면에는 목록과 두 경로가 합쳐지는 흐름도가 보인다.","caption":"중복 요청 복구를 시험해 결제 재시도가 이중 정산으로 이어지지 않는지 확인하세요.","description":null},"en":{"alt":"A woman types on a laptop opened flat, with a list and two paths converging on one endpoint on its screen.","caption":"Test duplicate-request recovery to ensure payment retries do not cause a second settlement.","description":null},"ja":{"alt":"窓辺で女性が平らに開いたノートパソコンを操作し、画面には一覧と2本の経路が合流するフロー図が表示されている。","caption":"重複リクエストの復旧をテストし、決済の再試行で二重精算が起きないことを確認しましょう。","description":null},"es":{"alt":"Una mujer teclea en un portátil abierto en plano; la pantalla muestra una lista y dos rutas que convergen.","caption":"Prueba la recuperación de solicitudes duplicadas para evitar una segunda liquidación al reintentar el pago.","description":null},"id":{"alt":"Seorang perempuan mengetik di laptop yang dibuka rata, dengan daftar dan diagram dua jalur yang menyatu di layar.","caption":"Uji pemulihan permintaan duplikat untuk memastikan percobaan ulang pembayaran tidak memicu penyelesaian ganda.","description":null},"pt":{"alt":"Uma mulher digita num notebook aberto na horizontal, com uma lista e duas rotas convergentes na tela.","caption":"Teste a recuperação de solicitações duplicadas para garantir que novas tentativas de pagamento não gerem liquidação dupla.","description":null},"zh-hant":{"alt":"一名女子在窗邊操作完全攤平的筆記型電腦，螢幕顯示清單及兩條路徑匯合的流程圖。","caption":"測試重複請求的復原流程，確認付款重試不會造成重複結算。","description":null},"de":{"alt":"Eine Frau tippt auf einem flach aufgeklappten Laptop; das Display zeigt eine Liste und zwei zusammenlaufende Pfade.","caption":"Testen Sie die Wiederherstellung bei doppelten Anfragen, damit Zahlungswiederholungen keine zweite Abrechnung auslösen.","description":null}}}],"published_at":"2026-10-28T19:20:55+09:00","updated_at":"2026-10-28T19:20:55+09:00","license":"cc_by","translation_status":"reviewed","available_locales":["ko","en","ja","es"],"data_locales":["ko","en","ja","es","id","pt","zh-hant","de"],"url":"https://injoys.com/en/articles/x402-payments-2026-v2-base-usdc-settlement-guide"}