Skip to main content

Blog

Purlと Cloudflareのテンプレートで x402 決済をローカルにて試してみる

この記事をシェア:

HTTP 402 Payment Required という、決済されていないリクエストを拒絶する際に利用するHTTPステータスコードに注目が集まっています。Coinbase が設計し、Cloudflare が共同推進する x402 は、このステータスコードを利用して機械同士が HTTP の上で直接お金を払い合えるプロトコルを定義しました。AIエージェントがデータフィードに自律的に課金されるような世界の土台となる仕様です。

この記事では、そのプロトコルをローカルで体験します。purl というコマンドラインツールと、Cloudflare が公開しているサンプルの組み合わせで、402 → 署名 → 決済 → コンテンツ取得という一連のフローを手元で動かします。

x402 のフローをざっくり理解する

x402 の流れは 4 ステップです。

  • まずクライアント、今回はpurlがリソースにアクセスを試みます
  • サーバーが HTTP 402 を返します。この際「いくら・どのアドレスに・どのネットワークで払え」という指示をレスポンスボディに入れます
  • クライアントはウォレットで署名した支払いペイロードをヘッダに付けて再リクエストします
  • 支払いペイロードを検証・決済処理し、サーバーがコンテンツを返します。

purl を使うことで、このステップをcurlやbashスクリプトで構成する必要なく、クライアント側の処理を済ませることができます。

事前セットアップ

事前にいくつか準備が必要です。

まず wranglerpurlの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_URLORIGIN_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.jsoncassets ブロック

Worker を必ず先に実行し(run_worker_first)、ランタイムから env.ASSETSpublic/ を読めるようにします。

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: proxyToOriginenv.ASSETS 分岐を追加する

async function proxyToOrigin の本文内で、ORIGIN_URLif ブロックが閉じた直後、最終行の 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.tsASSETS: 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.varsJWT_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

ウォレット作成時に設定したパスワードを聞かれたら入力します。その後、次のことが順番に起きます。

  1. /premium/1 に GET リクエストを送る
  2. サーバーが 402 を返す(支払い先アドレス・金額・ネットワークが入っています)
  3. purl がウォレットで署名したペイロードを生成し、X-PAYMENT ヘッダに付けて再リクエスト
  4. https://x402.org/facilitator が支払いを検証・決済(API キー不要のテスト用ファシリテータです)
  5. サーバーが 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.varsJWT_SECRET が書かれていないか、Wrangler が読み込んでいません。npm run dev を一度止めて .dev.vars を確認し、再起動します。

起動ログに env.ASSETS が出ない

修正1の wrangler.jsonc が正しく書き換えられていません。"binding": "ASSETS""run_worker_first": true の両方があるか確認してください。

TypeScript エラーが出る

修正3の env.tsCloudflare.Env を継承する形になっているか確認し、npx wrangler types を再実行してください。

接続できない(Connection refused)

npm run dev が動いているか確認します。デフォルトのポートは 8787 です。

HTTP の外に出なくていい決済

curlpurl に変えただけで、支払いが完了します。アカウント登録も、カード情報の入力も、リダイレクトもありません。HTTP のリクエスト・レスポンスサイクルの中で、価値の移動が完結します。

これが「機械同士の取引」を変える部分です。AIエージェントが外部の決済フローに依存せず、リクエストのたびに自律的に対価を払えます。今回試したのはそのプロトコルの最小単位ですが、次のステップとして Cloudflare の MCP tool への課金 を見ると、エージェント時代の応用例が具体的にイメージできます。

Tools to Support Stripe Development

We provide helpful tools to extend the Stripe Dashboard and streamline development and testing.

View All Tools

Support This Project

If you find this content helpful, consider supporting the project through GitHub Sponsors. Your support helps maintain and improve these tools.