x402決済
このページで分かること
@emdash-cms/x402パッケージの役割(EmDashがなくても使えるAstroのインテグレーション)と、x402の仕組み(402 Payment Requiredを返す)- 支払いを求める方法(すべてのリクエスト、ボットのみ、
hasPayment()による確認のみ)の違いと、ルートでのenforce()の使い方 - EmDashのフィールドから価格を読む方法、設定項目、価格とネットワークの指定方法、Solanaへの対応、リクエストの流れ
このページの目次
@emdash-cms/x402 パッケージは、サーバーで描画するAstroのサイトに、x402決済プロトコルへの対応を追加します。独立したAstroのインテグレーションなので、支払いを必須にするのにEmDashは必要ありません。サイトでEmDashも使っている場合は、コンテンツのフィールドでエントリーごとの価格を指定できます。
x402は、HTTPにもともと組み込まれた仕組みを使う決済プロトコルです。クライアントが支払いなしで有料のリソースを要求すると、サーバーは 402 Payment Required と、機械が読める支払いの指示を返します。x402を理解するエージェントやブラウザーは、自動で支払いを済ませ、リクエストをやり直せます。
支払いを求める方法の選択
ルートへのすべてのリクエストに有効な支払いを求める場合は、通常の方法を使います。この方法は、Cloudflare固有のリクエストのメタデータに依存しません。
サイトがBot Managementを有効にしたCloudflare Workersで動いていて、スコアの低いリクエストにだけ支払いを求める場合は、ボットのみのモードを使います。パッケージは request.cf.botManagement.score を読み取ります。この値がない場合は、リクエストを人間として扱い、支払いを求めません。ボットのデータがないときに安全側に倒して拒否する必要がある場合は、ボットのみのモードを使わないでください。
hasPayment() は、表示の切り替えだけに使う3つ目の動作です。リクエストに支払いのヘッダーがあるかどうかを返しますが、支払いの検証や決済はしません。
やさしい解説
x402は、Webページを見るときに少額の支払いを求める仕組みです。支払いなしでアクセスすると、サーバーはページの代わりに「402 Payment Required(支払いが必要)」と支払い方法を返し、x402に対応したAIエージェントなどは自動で支払ってページを受け取ります。支払いを求める相手は、全員にするか、Cloudflareがボットと判定したアクセスだけにするかを選べます。hasPayment() は表示を変えるための確認だけで、支払いが本物かどうかは確かめないため、有料の内容を見せる判断には使えません。
パッケージのインストール
使っているパッケージマネージャーでパッケージをインストールします。
pnpm
pnpm add @emdash-cms/x402
npm
npm install @emdash-cms/x402
yarn
yarn add @emdash-cms/x402
インテグレーションの設定
組み合わせて使えるウォレット、ネットワーク、ファシリテーターを選びます。インテグレーションは初期状態でEthereum Virtual Machine(EVM)のネットワークに対応していますが、ファシリテーターも、設定したネットワークと資産に対応している必要があります。次の例はBaseのメインネットを使い、すべてのリクエストに支払いを求めます。
import { defineConfig } from "astro/config";
import { x402 } from "@emdash-cms/x402";
export default defineConfig({
integrations: [
x402({
payTo: "0xYourWalletAddress",
network: "eip155:8453", // Base mainnet
defaultPrice: "$0.01",
}),
],
});
TypeScriptが Astro.locals.x402 を認識できるように、型の参照を追加します。
/// <reference types="@emdash-cms/x402/locals" />
ルートでの支払いの要求
インテグレーションは、支払いを求める処理(エンフォーサー)を Astro.locals.x402 に置きます。ページのフロントマターで enforce() を呼び出し、コンテンツを支払いの後ろに置きます。
---
const { x402 } = Astro.locals;
const result = await x402.enforce(Astro.request, {
price: "$0.05",
description: "Premium article",
});
// If the request has no valid payment, enforce() returns a 402 Response.
// Return it directly to send payment instructions to the client.
if (result instanceof Response) return result;
// Payment verified (or skipped in botOnly mode). Apply response headers
// so the client gets settlement proof.
x402.applyHeaders(result, Astro.response);
---
<article>
<h1>Premium content</h1>
</article>
enforce() メソッドは、次のどちらかを返します。
Response(402):クライアントが支払う必要があります。そのまま返します。EnforceResult:リクエストを先に進めてかまいません。コンテンツの支払いが済んでいるか、支払いの要求が省かれています(botOnlyモードで人間と判定された場合)。
ボットのみのモードの有効化
インテグレーションの設定で botOnly を有効にします。
x402({
payTo: "0xYourWalletAddress",
network: "eip155:8453",
defaultPrice: "$0.01",
botOnly: true,
botScoreThreshold: 30,
});
インテグレーションは、Cloudflareの request.cf.botManagement.score を読み取ってリクエストを分類します。
- スコアがしきい値(初期値は30)より低い:ボットとして扱い、支払いを求めます
- スコアがしきい値以上:人間として扱い、支払いを求めません
- ボット管理のデータがない(ローカル開発、Cloudflare以外へのデプロイ):人間として扱います
EnforceResult には skipped フラグが含まれるため、「支払う必要がなかった」と「支払った」を区別できます。
---
const result = await x402.enforce(Astro.request, { price: "$0.01" });
if (result instanceof Response) return result;
x402.applyHeaders(result, Astro.response);
// result.paid — true if payment was verified
// result.skipped — true if enforcement was skipped (human in botOnly mode)
// result.payer — wallet address of payer (if paid)
---
EmDashからの価格の読み込み
EmDashを使う場合は、ページごとの価格用に、コレクションに通常の number フィールドを追加し、リクエストのたびにその値を読み込みます。
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry } = await getEmDashEntry("posts", slug);
if (!entry) return Astro.redirect("/404");
const { x402 } = Astro.locals;
// Use the price from the CMS, falling back only when it is absent.
const result = await x402.enforce(Astro.request, {
price: entry.data.price ?? "$0.01",
description: entry.data.title,
});
if (result instanceof Response) return result;
x402.applyHeaders(result, Astro.response);
---
<article>
<h1>{entry.data.title}</h1>
</article>
やさしい解説
記事ごとに値段を変えたい場合は、管理画面でコレクションに数値(number)のフィールドを1つ追加し、編集者が記事ごとに価格を入力します。ページのコードは、そのフィールドの値を enforce() の price に渡すだけです。価格が未入力の記事では、?? "$0.01" の部分で指定した価格が使われます。
支払いを求めずに支払いのヘッダーを確認する
リクエストに支払いのヘッダーが含まれているかどうかを、検証や支払いの要求をせずに確認するには、hasPayment() を使います。案内やログインのメッセージを変えるのには使えますが、保護されたコンテンツを見せるために使ってはいけません。
---
const { x402 } = Astro.locals;
const hasPaymentHeader = x402.hasPayment(Astro.request);
---
{hasPaymentHeader ? (
<p>Payment supplied. Verification is still required.</p>
) : (
<p>This page requires payment.</p>
)}
設定リファレンス
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
payTo |
string |
必須 | 送金先のウォレットアドレス |
network |
string |
必須 | CAIP-2のネットワーク識別子(例:eip155:8453) |
defaultPrice |
Price |
— | 既定の価格。ページごとに上書きできます |
facilitatorUrl |
string |
https://x402.org/facilitator |
決済ファシリテーターのURL |
scheme |
string |
"exact" |
支払いの方式 |
maxTimeoutSeconds |
number |
60 |
支払いの署名のタイムアウトの上限 |
evm |
boolean |
true |
Ethereum Virtual Machineのネットワークへの対応を有効にする |
svm |
boolean |
false |
Solana Virtual Machineへの対応を有効にする(@x402/svm が必要) |
botOnly |
boolean |
false |
ボットにだけ支払いを求める |
botScoreThreshold |
number |
30 |
ボットのスコアのしきい値(1〜99。低いほどボットの可能性が高い) |
価格の形式
価格は、次のいくつかの形式で指定できます。
- ドルの文字列:
"$0.10"(接頭辞の$は取り除かれ、値はそのまま渡されます) - 数値の文字列:
"0.10" - 数値:
0.10 - オブジェクト:資産と金額を明示する場合は
{ amount: "100000", asset: "0x...", extra: {} }
ネットワーク識別子
CAIP-2は、Baseのメインネットを表す eip155:8453 のように、ブロックチェーンのネットワークに紛れのない識別子を与えます。ファシリテーターが示している識別子をそのまま使います。パッケージは、EVMへの対応が有効な場合は eip155:* の系統を、Solana Virtual Machine(SVM)への対応が有効な場合は solana:* の系統を受け付けます。ただし、ファシリテーターがその系統のすべてのネットワークを受け付けるとは限りません。
1つのリクエストでの上書き
特定のページで、設定の既定値を上書きします。
await x402.enforce(Astro.request, {
price: "$0.25", // Override price
payTo: "0xDifferentWallet", // Override wallet
network: "eip155:1", // Override network
description: "Article: How x402 Works", // Resource description
mimeType: "text/html", // MIME type hint
});
Solanaへの対応の有効化
Solanaへの対応は、明示的に有効にした場合だけ使えます。@x402/svm をインストールし、設定で有効にします。
pnpm add @x402/svm
svm: true を設定し、ファシリテーターが対応しているCAIP-2のSolanaの識別子を使います。サイトでSolanaの支払いだけを受け付ける場合は、EVMを無効にします。たとえば、@x402/svm 2.8が使うSolanaのメインネットの識別子は solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp です。
x402({
payTo: "YourSolanaAddress",
network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
svm: true,
evm: false, // Disable EVM if only using Solana
});
リクエストの流れ
- ページが実行される前に、インテグレーションが支払いを求める処理を
Astro.locals.x402に置きます。 enforce()がpayment-signatureヘッダーを確認します。- 支払いのヘッダーがない場合、
enforce()は402 Payment Requiredのレスポンスを返します。そのレスポンスの本文とPAYMENT-REQUIREDヘッダーが、受け付ける支払いを示します。 - 条件に合う支払いがある場合は、設定したファシリテーターがそれを検証し、決済します。
enforce()は支払った人と決済の結果を返します。ページのレスポンスにファシリテーターのPAYMENT-RESPONSEの証明が含まれるように、applyHeaders()を呼び出します。