HTTP 402 Payment Required という、決済されていないリクエストを拒絶する際に利用するHTTPステータスコードに注目が集まっています。Coinbase が設計し、Cloudflare が共同推進する x402 は、このステータスコードを利用して機械同士が HTTP の上で直接お金を払い合えるプロトコルを定義しました。AIエージェントがデータフィードに自律的に課金されるような世界の土台となる仕様です。
この記事では、そのプロトコルをローカルで体験します。purl というコマンドラインツールと、Cloudflare が公開しているサンプルの組み合わせで、402 → 署名 → 決済 → コンテンツ取得という一連のフローを手元で動かします。
x402 のフローをざっくり理解する
x402 の流れは 4 ステップです。
- まずクライアント、今回はpurlがリソースにアクセスを試みます
- サーバーが HTTP 402 を返します。この際「いくら・どのアドレスに・どのネットワークで払え」という指示をレスポンスボディに入れます
- クライアントはウォレットで署名した支払いペイロードをヘッダに付けて再リクエストします
- 支払いペイロードを検証・決済処理し、サーバーがコンテンツを返します。
purl を使うことで、このステップをcurlやbashスクリプトで構成する必要なく、クライアント側の処理を済ませることができます。
事前セットアップ
事前にいくつか準備が必要です。
まず wranglerと purlの2つはHomebrewやnpmなどでインストールしてください。また、テスト用の決済で利用する暗号通貨のウォレットが必要です。これはpurl wallet addコマンドで生成できます。作成方法は先日公開した記事「purlコマンドを使って、暗号通貨のウォレットなしに x402 の支払いを体験する」をチェックしてください。
Cloudflareのサンプルでx402対応のアプリをローカル実行させてみる
ウォレット側の準備ができたら、早速アプリをセットアップしてみましょう。なお、ローカルで起動させるには、テンプレートに4カ所の修正を加える必要があります。もし手早く済ませたい場合・コードを変更したくない場合は、ローカルで起動する代わりにwrangler deployなどでCloudflareへデプロイしてからPurlを実行してください。
テンプレートの取得
Cloudflare が公開している x402-proxy-template を clone します。
git clone https://github.com/cloudflare/templates.git
cd templates/x402-proxy-template
npm install
次のコマンドで必要なファイルが揃っていることを確認します。
test -f package.json && test -f wrangler.jsonc && test -f src/index.ts && echo OK
OK が出れば次に進みます。
ローカルで purl を通すための4カ所の修正
ローカルで動作するための変更を加えましょう。
x402-hono のミドルウェアは、支払いの検証・決済(settle)が成功した後、アプリがオリジンサーバーへリクエストをプロキシする設計になっています。このとき ORIGIN_URL も ORIGIN_SERVICE も設定されていない場合、テンプレートの実装は fetch(request) にフォールバックします。これは「ゾーンの DNS オリジン」に送ることを意味しますが、wrangler dev のローカル環境にはその実オリジンが存在しません。結果として、決済自体は通ったのに最終レスポンスが失敗し、purl からは Payment was not accepted に見えます。
もうひとつ、purl はデフォルトでリダイレクトを追いません。Workers Static Assets はディレクトリ型の URL に対して 307 で末尾スラッシュ付きの URL へ誘導することがあります。/premium/1 へのリクエストが 307 で止まると、purl には「成功」と映りません。
これら2つの問題を解消するために、以下の4カ所を修正します。
修正1: wrangler.jsonc の assets ブロック
Worker を必ず先に実行し(run_worker_first)、ランタイムから env.ASSETS で public/ を読めるようにします。
wrangler.jsonc を開き、トップレベルの "assets" ブロックを次の内容に置き換えます。
// Worker を先に実行し、認証後は ASSETS で静的ファイルを返す(ローカル dev で DNS オリジンがない場合のフォールバック)
"assets": {
"directory": "public",
"binding": "ASSETS",
"run_worker_first": true,
},
修正後に検証します。
grep -q '"binding": "ASSETS"' wrangler.jsonc && grep -q 'run_worker_first' wrangler.jsonc && echo "wrangler.jsonc OK"
修正2: src/index.ts に2つの変更を加える
2-A: requestForAssetFetch 関数を追加する
307 リダイレクトを回避するため、ディレクトリ型のパスに末尾スラッシュを付けてから ASSETS.fetch するヘルパー関数を追加します。
src/index.ts を開き、BUILT_IN_PUBLIC_PATHS の定数定義の直後(proxyToOrigin より前)に次の関数を追加します。
/**
* Workers Static Assets often 307-redirect directory URLs to a trailing slash.
* Normalize so non-browser clients (e.g. purl) get 200 without following redirects.
*/
function requestForAssetFetch(request: Request): Request {
const url = new URL(request.url);
const path = url.pathname;
if (path.endsWith("/")) {
return request;
}
const lastSegment = path.split("/").pop() ?? "";
if (lastSegment.includes(".")) {
return request;
}
url.pathname = `${path}/`;
return new Request(url.toString(), request);
}
2-B: proxyToOrigin に env.ASSETS 分岐を追加する
async function proxyToOrigin の本文内で、ORIGIN_URL の if ブロックが閉じた直後、最終行の return fetch(request) の直前に次を挿入します。
// Standalone / local dev: no DNS origin — serve Workers Static Assets (see wrangler "ASSETS" binding)
if (env.ASSETS) {
return env.ASSETS.fetch(requestForAssetFetch(request));
}
修正後に検証します。
grep -q "function requestForAssetFetch" src/index.ts && grep -q "env.ASSETS.fetch(requestForAssetFetch" src/index.ts && echo "index.ts OK"
修正3: src/env.ts を書き換える
wrangler types が生成する Cloudflare.Env を継承する形に変更し、ASSETS が型に反映されるようにします。
src/env.ts を次の内容に書き換えます(既存の CloudflareBindings 継承や重複フィールドはすべて削除します)。
/**
* Environment bindings type definition
*
* Base shape comes from `wrangler types` (Cloudflare.Env). Optional bindings are
* listed here when they may be absent from wrangler.jsonc.
*/
import type { JWTPayload } from "./jwt";
export interface Env extends Cloudflare.Env {
/**
* Optional origin URL for External Origin mode.
* When set, requests are rewritten to this URL instead of using DNS-based routing.
*/
ORIGIN_URL?: string;
/** Optional: Service Binding to origin Worker */
ORIGIN_SERVICE?: Fetcher;
}
/** Full app context type for Hono */
export interface AppContext {
Bindings: Env;
Variables: {
auth?: JWTPayload;
};
}
修正後に検証します。
grep -q "extends Cloudflare.Env" src/env.ts && grep -q "ORIGIN_URL" src/env.ts && echo "env.ts OK"
修正4: public/premium/1/index.html を作成する
保護パターン /premium/* に一致する /premium/1 へのレスポンス本文です。このファイルがないと、決済後に 404 が返ります。
mkdir -p public/premium/1
public/premium/1/index.html に次の内容を書き込みます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Premium (demo)</title>
</head>
<body>
<h1>Premium content</h1>
<p>Demo asset for <code>/premium/1</code> after x402 payment.</p>
</body>
</html>
修正後に検証します。
test -f public/premium/1/index.html && grep -q "Premium content" public/premium/1/index.html && echo "premium asset OK"
型を生成する
4カ所の修正が終わったら、wrangler types を実行して ASSETS を TypeScript に反映させます。
npx wrangler types
worker-configuration.d.ts に ASSETS: Fetcher が含まれることを確認します。
grep -q "ASSETS: Fetcher" worker-configuration.d.ts && echo "types OK"
4カ所の総合確認
次の5行をすべて実行し、すべて OK が返ることを確認してから次に進みます。
grep -q '"binding": "ASSETS"' wrangler.jsonc && echo "1 OK"
grep -q "env.ASSETS.fetch(requestForAssetFetch" src/index.ts && echo "2 OK"
grep -q "extends Cloudflare.Env" src/env.ts && echo "3 OK"
test -f public/premium/1/index.html && echo "4 OK"
grep -q "ASSETS: Fetcher" worker-configuration.d.ts && echo "5 OK"
1つでも失敗した場合は、対応する修正に戻ってください。
.dev.vars の設定
Worker は JWT でセッションを管理するため、JWT_SECRET が必須です。
cp .dev.vars.example .dev.vars
次のコマンドで 32 バイトのランダムな hex 文字列を生成します。
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
出力された 64 文字の文字列を .dev.vars の JWT_SECRET= に貼り付けます。.dev.vars.example にあるプレースホルダ your-secret-key-here のままでは動きません。必ず置き換えてください。
.dev.vars の例:
JWT_SECRET=(ここに64文字hexを貼る)
このファイルは .gitignore 済みなのでコミットされません。
開発サーバーを起動する
npm run dev
http://localhost:8787 で待ち受けが始まります。起動ログに env.ASSETS が含まれていることを目視で確認します。含まれていない場合は修正1の wrangler.jsonc に戻ってください。
別のターミナルを開いて、以降の確認コマンドを実行します。
まず Worker が応答しているかを確認します。
curl -s http://localhost:8787/__x402/health
JSON に "status":"ok" が返れば動作しています。
次に、保護ルートが未払いで 402 を返すことを確認します。
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8787/premium/1
402 が返れば準備完了です。
purl で決済する
保護されたルートに purl でアクセスします。
purl http://localhost:8787/premium/1 --network base-sepolia
ウォレット作成時に設定したパスワードを聞かれたら入力します。その後、次のことが順番に起きます。
/premium/1に GET リクエストを送る- サーバーが 402 を返す(支払い先アドレス・金額・ネットワークが入っています)
purlがウォレットで署名したペイロードを生成し、X-PAYMENTヘッダに付けて再リクエストhttps://x402.org/facilitatorが支払いを検証・決済(API キー不要のテスト用ファシリテータです)- サーバーが 200 とコンテンツを返す
標準出力に Premium content が表示され、エラー行が出なければ成功です。
ステータスコードまで確認したい場合は -i を付けます。
purl http://localhost:8787/premium/1 --network base-sepolia -i
先頭に HTTP/1.1 200 が見えれば、x402 による支払い付き HTTP アクセスが完了しています。
うまくいかない場合
Error: Payment was not accepted by the server が出る
まず -vvv で詳細を出して、JSON の error フィールドを読みます。
purl http://localhost:8787/premium/1 --network base-sepolia -vvv
よくある原因は3つです。「Base Sepolia の ETH が足りない(ガス不足)」「USDC が足りない」「--network base-sepolia を付け忘れている」。purl balance で残高を確認し、ファウセットで補充します。
決済は通ったが本文がおかしい・200 にならない
修正2の requestForAssetFetch が適用されていないか、修正4の public/premium/1/index.html が存在しない可能性があります。「4カ所の総合確認」の5行を再実行してください。
Worker が 500 を返す
.dev.vars に JWT_SECRET が書かれていないか、Wrangler が読み込んでいません。npm run dev を一度止めて .dev.vars を確認し、再起動します。
起動ログに env.ASSETS が出ない
修正1の wrangler.jsonc が正しく書き換えられていません。"binding": "ASSETS" と "run_worker_first": true の両方があるか確認してください。
TypeScript エラーが出る
修正3の env.ts が Cloudflare.Env を継承する形になっているか確認し、npx wrangler types を再実行してください。
接続できない(Connection refused)
npm run dev が動いているか確認します。デフォルトのポートは 8787 です。
HTTP の外に出なくていい決済
curl を purl に変えただけで、支払いが完了します。アカウント登録も、カード情報の入力も、リダイレクトもありません。HTTP のリクエスト・レスポンスサイクルの中で、価値の移動が完結します。
これが「機械同士の取引」を変える部分です。AIエージェントが外部の決済フローに依存せず、リクエストのたびに自律的に対価を払えます。今回試したのはそのプロトコルの最小単位ですが、次のステップとして Cloudflare の MCP tool への課金 を見ると、エージェント時代の応用例が具体的にイメージできます。
