x402決済は、APIの402レスポンスに決済条件を含めて実装します。Node.jsとテスト用ウォレットを用意してください。まず、Base Sepoliaでv2の署名と精算を検証します。有料レスポンスは、精算が成功した後に返すように設定してください。
金額・チェーン情報は、出典としてリンクしたx402・Base・Circleの公式ドキュメントに基づいています。
実習の準備とインストール
APIを変更できる開発環境とテスト用ウォレットを用意してください。サンプルはNode.js 22とx402 SDK 2.28.0を基準に構成しています。インストール構成は公式販売者ガイドに従います。
- 決済を受け取るEVMウォレットのアドレスを用意してください。
- 決済用のテストウォレットを別途用意してください。
- 決済用ウォレットにBase Sepoliaのテスト用USDCを用意してください。
- 以下のコマンドで実習用プロジェクトを作成してください。
- 以降のTypeScriptファイルを同じフォルダーに保存してください。
mkdir x402-demo
cd x402-demo
npm init -y
npm pkg set type=module
npm install --save-exact [email protected] [email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected]
npm install --save-dev --save-exact typescript tsx @types/node @types/express
受取側のサーバーに、受取ウォレットの秘密鍵は必要ありません。決済用の秘密鍵は、クライアントプロセスでのみ読み込む構成にしてください。ウォレットの署名方式は公式購入者ガイドに基づいています。
このサンプルの検証範囲は、型チェックと模擬的な決済フローです。実際にテストネットでの送金まで完了したサンプルではありません。以下の実行手順で、オンチェーンの結果を直接確認してください。
重複リクエストの復旧をテストし、決済の再試行で二重精算が起きないことを確認しましょう。
x402 v1とv2の比較
v2を実装する際は、ヘッダーと本文のフィールドを両方そろえる必要があります。ヘッダー名だけを変更しても、データ構造が一致しません。次の表は公式移行ガイドに基づいています。
| 区分 | v1 | v2 |
|---|---|---|
| プロトコルバージョン | x402Version: 1 |
x402Version: 2 |
| 初回の決済条件 | 402レスポンスのJSON本文 |
PAYMENT-REQUIREDレスポンスヘッダー |
| 署名済み決済データ |
X-PAYMENTリクエストヘッダー |
PAYMENT-SIGNATUREリクエストヘッダー |
| 精算結果 |
X-PAYMENT-RESPONSEレスポンスヘッダー |
PAYMENT-RESPONSEレスポンスヘッダー |
| Baseメインネット識別子 | base |
eip155:8453 |
| Base Sepolia識別子 | base-sepolia |
eip155:84532 |
| 決済条件の金額フィールド | maxAmountRequired |
amount |
| リソースの説明 | 決済条件内のresourceなど |
最上位のresourceオブジェクト |
| 決済ペイロードの条件 | 最上位のscheme、networkなど |
選択した条件を格納するacceptedオブジェクト |
| TypeScriptパッケージの例 | x402-express |
@x402/express |
v1で決済条件を渡す場所は、HTTP v1仕様に明記されています。v2で標準となる受け渡し場所は、PAYMENT-REQUIREDヘッダーです。実装によっては本文も返す場合がありますが、ヘッダーを基準に処理してください。
HTTP v2仕様では、ステータスコードを次のように説明しています。
サーバーは、HTTP 402「Payment Required」ステータスコードを使って、決済が必要であることを示します。
出典:x402 HTTP v2仕様
v2の決済ヘッダーは、JSONをBase64でエンコードした値です。Base64は暗号化方式ではありません。外部向けサービスではHTTPSを使用してください。
HTTP 402リクエストと決済の順序
この実習では、EVMのexact方式の基本的な承認フローを使用します。exactは、指定された金額を決済する方式です。順序はx402 v2の基本仕様に従います。
- クライアントが決済情報なしで有料APIをリクエストします。
- サーバーがHTTP 402と
PAYMENT-REQUIREDを返します。 - クライアントが
acceptsから許可された条件を選択します。 - SDKが、選択した条件に合う決済承認に署名します。
- クライアントが
PAYMENT-SIGNATUREを付けて再リクエストします。 - サーバーがfacilitatorの
/verifyで承認を検証します。 - 検証を通過すると、サーバーがリソースの処理を実行します。
- サーバーが
/settleで精算をリクエストします。 - 精算が成功した後、有料の本文と
PAYMENT-RESPONSEを返します。
この順序は、すべての決済方式に共通する固定の順序ではありません。v2には、精算のタイミングが異なるフローも定義されています。以下のコードでは、基本的なauthorizationフローのみを許可します。
Resource serverとfacilitatorの役割
Resource serverは、価格とコンテンツへのアクセス条件を決定します。Facilitatorは、決済の検証と精算を担当します。役割の区分は公式facilitatorドキュメントに基づいています。
| 構成要素 | 担当する処理 | 成功と見なしてはいけない状態 |
|---|---|---|
| クライアント | 条件の選択と決済承認への署名 | 署名の生成だけが完了した状態 |
| Resource server | 価格の宣言と有料レスポンスの制御 | クライアントが成功したと主張している状態 |
Facilitator /verify
|
署名と決済条件の有効性確認 |
isValid: trueを受け取っただけの状態 |
Facilitator /settle
|
この実習ではオンチェーン決済の実行と結果の返却 | 送信したものの結果を確認できていない状態 |
/verifyは、オンチェーンの状態を変更しない検査です。そのため、検証の成功は入金の完了を意味しません。検証後も、残高や承認状態が変わる可能性があります。
検証や精算に失敗した場合は、有料の本文をブロックしてください。成功を確認できなかったときにブロックする処理を、fail-closedと呼びます。この動作は公式Express実装でも確認できます。
Baseメインネットとテストネットの比較
初回の実行では、Base Sepoliaだけを使用してください。Circleは、テストネットのUSDCに金銭的価値がないと明記しています。アドレスはCircleのUSDCコントラクト一覧に基づいています。
| 項目 | Baseメインネット | Base Sepoliaテストネット |
|---|---|---|
| 用途 | 実際の資産による決済 | 実装と障害処理のテスト |
| Chain ID | 8453 |
84532 |
| x402 v2ネットワーク | eip155:8453 |
eip155:84532 |
| USDCコントラクト | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| 公開RPC | https://mainnet.base.org |
https://sepolia.base.org |
| トークンの金銭的価値 | 実際に金銭的価値のある資産 | 金銭的価値なし |
Chain IDとRPCは、Baseネットワークドキュメントで確認してください。同じウォレットアドレスでも、チェーンが異なれば残高は別です。テストネットでの成功を、実際の売上として記録してはいけません。
テスト用USDCは、Circle Testnet Faucetでリクエストできます。ネットワークにはBase Sepoliaを選択してください。受取アドレスには、決済用テストウォレットのアドレスを入力します。
Node.jsによる有料APIの実装
有料ルートの前にpaymentMiddlewareを組み込んでください。以下のサンプルは、呼び出し1回あたり0.001 USDCを要求します。金額は、公式販売者ガイドの価格例を使用しています。(確認値:呼び出し1回あたり0.001 USDC・出典:docs.x402.org・2026-10-28確認)
サーバーファイル
次のコードをserver.tsとして保存してください。サーバーは、ローカルインターフェースでのみリクエストを受け付けます。受取アドレスは、環境変数PAY_TOから読み込みます。
import express from "express";
import { getAddress, zeroAddress } from "viem";
import { paymentMiddleware } from "@x402/express";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const resourceServer = new x402ResourceServer(
new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }),
).register(network, new ExactEvmScheme());
const app = express();
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "private, no-store");
next();
});
app.use(paymentMiddleware({
"GET /paid-data": {
accepts: [{ scheme: "exact", price: "$0.001", network, payTo }],
description: "Sample paid JSON",
mimeType: "application/json",
},
}, resourceServer));
app.get("/paid-data", (_req, res) => {
res.json({ content: "Paid sample content" });
});
app.listen(4021, "127.0.0.1");
https://x402.org/facilitatorは、公式ガイドに掲載されているテスト用のアドレスです。このサンプルでは、Base Sepoliaでの精算に使用します。本番用facilitatorの選択については、販売者ガイドのメインネット手順を確認してください。
価格計算の例
サンプルの価格は、USDCの最小単位で1000です。USDCの小数点以下の桁数は6桁です。SDKによる価格変換は、EVMサーバー実装に基づいています。
0.001 USDC × 10^6 = 1000 최소 단위
| 確認する値 | このサンプルでの期待値 |
|---|---|
サーバー設定のprice
|
"$0.001" |
402決済条件のamount
|
"1000" |
決済条件のasset
|
Base SepoliaのUSDCコントラクト |
決済条件のpayTo
|
設定した受取アドレス |
決済条件のnetwork
|
eip155:84532 |
priceはSDKの設定値です。実際のv2決済条件では、amountが渡されます。整数の金額は、文字列またはbigintとして比較してください。
サーバーの起動と402の確認
1つ目のBashターミナルでサーバーを起動してください。入力する値は、秘密鍵ではなく公開の受取アドレスです。実行中は、このターミナルを開いたままにします。
read -r -p '수취 지갑 주소: ' PAY_TO
export PAY_TO
npx tsx server.ts
2つ目のターミナルから、決済なしのリクエストを送信してください。以下のコマンドは、決済に署名しません。HTTPステータスと、デコードした決済条件を出力します。
node --input-type=module <<'JS'
const response = await fetch("http://127.0.0.1:4021/paid-data");
const header = response.headers.get("PAYMENT-REQUIRED");
console.log(response.status);
console.log(header && JSON.parse(
Buffer.from(header, "base64").toString("utf8")
));
JS
正常な決済なしのレスポンスは、HTTP 402です。出力された条件を、上の表と照合してください。有料コンテンツがそのまま返される場合は、ルートの保護設定から修正します。
決済条件の選択とクライアントの実装
許可した条件にのみ署名するように、クライアントを制限してください。このサンプルでは、Base SepoliaのUSDCによる指定価格の決済だけを許可します。決済オプションのフィルターには、公式x402Client実装のregisterPolicyを使用します。
- 以下のコードを
client.tsとして保存してください。 - 次の節の
check-transfer.tsも保存してください。 - 2つのファイルを用意してから、実行手順に進んでください。
import { writeFile } from "node:fs/promises";
import { getAddress, zeroAddress, type Hex, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { decodePaymentResponseHeader } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { checkTransfer } from "./check-transfer.js";
const key = process.env.EVM_PRIVATE_KEY ?? "";
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error("Invalid test key");
const signer = privateKeyToAccount(key as Hex);
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const token = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const client = new x402Client().register(network, new ExactEvmScheme(signer));
client.registerPolicy((version, options) => version === 2
? options.filter(r =>
r.scheme === "exact" && r.network === network &&
r.asset.toLowerCase() === token.toLowerCase() &&
r.payTo.toLowerCase() === payTo.toLowerCase() && r.amount === "1000" &&
(!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === "eip3009") &&
(!r.extra?.paymentFlow || r.extra.paymentFlow === "authorization"))
: []);
const paidFetch = wrapFetchWithPayment(fetch, client);
const response = await paidFetch("http://127.0.0.1:4021/paid-data", {
redirect: "error",
});
const raw = response.headers.get("PAYMENT-RESPONSE");
if (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);
const settlement = decodePaymentResponseHeader(raw);
await writeFile("settlement.json", JSON.stringify(settlement, null, 2));
if (!response.ok || !settlement.success || settlement.network !== network) {
throw new Error("Payment unresolved; inspect settlement.json before retrying");
}
const evidence = await checkTransfer(
network, settlement.transaction as Hash, signer.address, payTo, 1000n,
);
await writeFile("transfer.json", JSON.stringify(evidence, null, 2));
console.log(await response.text());
wrapFetchWithPaymentが、402の処理と署名付きの再リクエストを担当します。使い方は公式fetchサンプルに従います。許可された決済条件がなければ、決済の生成を中止します。
AIエージェントでも、このクライアントを使用できます。ただし、呼び出し1回あたりの価格制限は、累積支出の制限ではありません。エージェントによる繰り返し呼び出しの予算は、別途管理してください。
USDC Transferイベントの検証
精算結果のトランザクションハッシュを使って、USDCの送金ログを確認してください。トランザクションの成功状態だけでは、受取人まで確認できません。Transferのフィールドは、ERC-20標準に基づいています。
検証には、次の項目が必要です。
| 検証項目 | 比較対象 |
|---|---|
| RPCのChain ID | サーバーが許可したチェーン |
トランザクションレシートのstatus
|
成功状態 |
| イベントを発生させたアドレス | そのチェーンの公式USDCコントラクト |
Transfer.from |
確認済みの支払者 |
Transfer.to |
サーバーが指定した受取アドレス |
Transfer.value |
サーバーが要求した最小単位での金額 |
以下のコードをcheck-transfer.tsとして保存してください。照会には、viemのgetTransactionReceiptを使用します。イベントの解釈は、parseEventLogsに基づいています。
import {
createPublicClient, http, getAddress, parseEventLogs,
erc20Abi, type Address, type Hash,
} from "viem";
import { base, baseSepolia } from "viem/chains";
const configs = {
"eip155:84532": {
chain: baseSepolia,
rpc: "https://sepolia.base.org",
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
},
"eip155:8453": {
chain: base,
rpc: "https://mainnet.base.org",
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
},
} as const;
export async function checkTransfer(
network: keyof typeof configs, hash: Hash,
payer: Address, payTo: Address, amount: bigint,
) {
const cfg = configs[network];
const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });
if (await rpc.getChainId() !== cfg.chain.id) throw new Error("Wrong chain");
const receipt = await rpc.getTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Transaction failed");
const logs = parseEventLogs({
abi: erc20Abi,
eventName: "Transfer",
logs: receipt.logs.filter(
log => getAddress(log.address) === getAddress(cfg.token),
),
strict: true,
});
const transfer = logs.find(log =>
getAddress(log.args.from) === getAddress(payer) &&
getAddress(log.args.to) === getAddress(payTo) &&
log.args.value === amount,
);
if (!transfer) throw new Error("Matching USDC Transfer not found");
return {
network, transaction: hash, logIndex: transfer.logIndex,
blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),
};
}
この関数は、そのトランザクションでUSDCが送金された事実を確認します。特定のAPI注文に対応するものかどうかは、別途確認する必要があります。本番サーバーでは、保存済みの注文条件を比較値として使用してください。
クライアントがアップロードしたtransfer.jsonを、アクセス許可の証拠として信用してはいけません。このサンプルのサーバーは、facilitatorの検証・精算結果を使用します。独立した照会をアクセス許可の条件にする場合は、サーバー側で実行する必要があります。
レシートの照会に失敗しても、それだけで決済の失敗を意味するわけではありません。RPCエラーであれば、保存済みのトランザクションハッシュを再照会してください。追加の承認数とチェーン再編成への対応は、運用ポリシーで定めます。