@emdash-cms/x402 パッケージは、サーバーレンダリングされる Astro サイトに x402 決済プロトコルのサポートを追加します。これは単独で動作する Astro インテグレーションなので、決済の強制に EmDash は必要ありません。サイトが EmDash も使っている場合は、コンテンツフィールドでエントリごとの価格を指定できます。
x402 は HTTP ネイティブな決済プロトコルです。クライアントが支払いなしで有料リソースをリクエストすると、サーバーは 402 Payment Required と、機械可読な支払い手順を返します。x402 に対応したエージェントやブラウザーは、自動的に支払いを完了し、リクエストを再試行できます。
強制モードを選ぶ
ルートへのすべてのリクエストで有効な支払いを提示させる必要がある場合は、通常の強制を使用します。このモードは、Cloudflare 固有のリクエストメタデータに依存しません。
サイトが Bot Management を有効にした Cloudflare Workers で動作していて、スコアの低いリクエストだけに支払いを求めたい場合は、ボット専用モードを使用します。パッケージは request.cf.botManagement.score を読み取ります。この値が存在しない場合、リクエストは人間によるものとして扱われ、強制はスキップされます。ボットのデータが欠けているときにフェイルクローズ(fail closed)にする必要がある場合は、ボット専用モードを使用しないでください。
hasPayment() は、表示専用の 3 つ目の動作を提供します。リクエストに支払いヘッダーがあるかどうかを報告しますが、支払いの検証や決済は行いません。
パッケージをインストールする
お使いのパッケージマネージャーでパッケージをインストールします。
pnpm
pnpm add @emdash-cms/x402 npm
npm install @emdash-cms/x402 yarn
yarn add @emdash-cms/x402 インテグレーションを設定する
連携して動作するウォレット、ネットワーク、facilitator を選んでください。このインテグレーションはデフォルトで Ethereum Virtual Machine(EVM)ネットワークをサポートしますが、facilitator も、設定したネットワークとアセットをサポートしている必要があります。次の例では 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 を有効にします。
x402({
payTo: "0xYourWalletAddress",
network: "eip155:8453",
defaultPrice: "$0.01",
botOnly: true,
botScoreThreshold: 30,
});
インテグレーションは、Cloudflare の request.cf.botManagement.score を読み取ってリクエストを分類します。
- スコアがしきい値未満(デフォルトは 30)-> ボットとして扱い、支払いを強制
- スコアがしきい値以上 -> 人間として扱い、強制をスキップ
- Bot Management のデータなし(ローカル開発、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.rewrite("/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>
強制せずに支払いヘッダーを確認する
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 | 決済 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 は、eip155:8453(Base メインネット)のように、ブロックチェーンネットワークに曖昧さのない識別子を与えます。facilitator が公開している識別子を、そのまま使用してください。パッケージは、EVM サポートが有効な場合は eip155:* ファミリー、Solana Virtual Machine(SVM)サポートが有効な場合は solana:* ファミリーを受け付けます。ただし、facilitator がそのファミリー内のすべてのネットワークを受け付けることは保証されません。
単一のリクエストを上書きする
特定のページに対して、設定のデフォルトを上書きします。
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 を設定し、facilitator がサポートする 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ヘッダーに、受け付ける支払い方法が記載されます。 - 一致する支払いがある場合は、設定された facilitator が検証と決済を行います。
enforce()は、支払者と決済結果を返します。ページのレスポンスに facilitator のPAYMENT-RESPONSEによる証明が含まれるように、applyHeaders()を呼び出してください。