このページで分かること

  • @emdash-cms/x402 パッケージの役割(EmDashがなくても使えるAstroのインテグレーション)と、x402の仕組み(402 Payment Required を返す)
  • 支払いを求める方法(すべてのリクエスト、ボットのみ、hasPayment() による確認のみ)の違いと、ルートでの enforce() の使い方
  • EmDashのフィールドから価格を読む方法、設定項目、価格とネットワークの指定方法、Solanaへの対応、リクエストの流れ
難易度
実践
読む時間
4分
このページの目次

@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のメインネットを使い、すべてのリクエストに支払いを求めます。

astro.config.mjs
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 を認識できるように、型の参照を追加します。

src/env.d.ts
/// <reference types="@emdash-cms/x402/locals" />

ルートでの支払いの要求

インテグレーションは、支払いを求める処理(エンフォーサー)を Astro.locals.x402 に置きます。ページのフロントマターで enforce() を呼び出し、コンテンツを支払いの後ろに置きます。

src/pages/posts/[...slug].astro
---
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 を有効にします。

astro.config.mjs
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 フィールドを追加し、リクエストのたびにその値を読み込みます。

src/pages/posts/[...slug].astro
---
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 です。

astro.config.mjs
x402({
	payTo: "YourSolanaAddress",
	network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
	svm: true,
	evm: false, // Disable EVM if only using Solana
});

リクエストの流れ

  1. ページが実行される前に、インテグレーションが支払いを求める処理を Astro.locals.x402 に置きます。
  2. enforce()payment-signature ヘッダーを確認します。
  3. 支払いのヘッダーがない場合、enforce()402 Payment Required のレスポンスを返します。そのレスポンスの本文と PAYMENT-REQUIRED ヘッダーが、受け付ける支払いを示します。
  4. 条件に合う支払いがある場合は、設定したファシリテーターがそれを検証し、決済します。
  5. enforce() は支払った人と決済の結果を返します。ページのレスポンスにファシリテーターの PAYMENT-RESPONSE の証明が含まれるように、applyHeaders() を呼び出します。