x402 Payments
The @emdash-cms/x402 package adds x402 payment protocol support to server-rendered Astro sites. It is a standalone Astro integration, so payment enforcement does not require EmDash. When the site also uses EmDash, a content field can supply the price for each entry.
x402 is an HTTP-native payment protocol. When a client requests a paid resource without payment, the server responds with 402 Payment Required and machine-readable payment instructions. Agents and browsers that understand x402 can complete payment automatically and retry the request.
Choose an enforcement mode
Section titled “Choose an enforcement mode”Use normal enforcement when every request to the route must present a valid payment. This mode does not depend on Cloudflare-specific request metadata.
Use bot-only mode when the site runs on Cloudflare Workers with Bot Management enabled and only low-score requests should pay. The package reads request.cf.botManagement.score; when that value is absent, it treats the request as human and skips enforcement. Do not use bot-only mode when missing bot data must fail closed.
hasPayment() provides a third behavior for presentation only. It reports whether the request has a payment header but does not verify or settle the payment.
Install the package
Section titled “Install the package”Install the package with your package manager:
pnpm add @emdash-cms/x402npm install @emdash-cms/x402yarn add @emdash-cms/x402Configure the integration
Section titled “Configure the integration”Choose a wallet, network, and facilitator that work together. The integration supports Ethereum Virtual Machine (EVM) networks by default, but the facilitator must also support the configured network and asset. The following example uses Base mainnet and enforces payment for every request:
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", }), ],});Add the type reference so TypeScript knows about Astro.locals.x402:
/// <reference types="@emdash-cms/x402/locals" />Enforce payment on a route
Section titled “Enforce payment on a route”The integration puts an enforcer on Astro.locals.x402. Call enforce() in your page frontmatter to gate content behind payment:
---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>The enforce() method returns either:
- A
Response(402) — the client needs to pay. Return it directly. - An
EnforceResult— the request should proceed. The content was paid for, or enforcement was skipped (human in botOnly mode).
Enable bot-only mode
Section titled “Enable bot-only mode”Enable botOnly in the integration configuration:
x402({ payTo: "0xYourWalletAddress", network: "eip155:8453", defaultPrice: "$0.01", botOnly: true, botScoreThreshold: 30,});The integration reads Cloudflare’s request.cf.botManagement.score to classify requests:
- Score below threshold (default 30) -> treated as bot, payment enforced
- Score at or above threshold -> treated as human, enforcement skipped
- No bot management data (local dev, non-CF deployment) -> treated as human
The EnforceResult includes a skipped flag so you can distinguish “didn’t need to pay” from “paid”:
---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)---Read a price from EmDash
Section titled “Read a price from EmDash”When using EmDash, add a regular number field to your collection for per-page pricing and read it at request time:
---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>Check for a payment header without enforcing
Section titled “Check for a payment header without enforcing”Use hasPayment() to check whether a request includes a payment header without verifying or enforcing it. This can change a prompt or sign-in message, but it must not unlock protected content:
---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>)}Configuration reference
Section titled “Configuration reference”| Option | Type | Default | Description |
|---|---|---|---|
payTo | string | required | Destination wallet address |
network | string | required | CAIP-2 network identifier (e.g., eip155:8453) |
defaultPrice | Price | — | Default price, overridable per-page |
facilitatorUrl | string | https://x402.org/facilitator | Payment facilitator URL |
scheme | string | "exact" | Payment scheme |
maxTimeoutSeconds | number | 60 | Maximum timeout for payment signatures |
evm | boolean | true | Enable Ethereum Virtual Machine network support |
svm | boolean | false | Enable Solana Virtual Machine support (requires @x402/svm) |
botOnly | boolean | false | Only enforce payment for bots |
botScoreThreshold | number | 30 | Bot score threshold (1-99, lower = more likely bot) |
Price format
Section titled “Price format”Prices can be specified in several formats:
- Dollar string —
"$0.10"(the$prefix is stripped, value passed as-is) - Numeric string —
"0.10" - Number —
0.10 - Object —
{ amount: "100000", asset: "0x...", extra: {} }for explicit asset/amount
Network identifiers
Section titled “Network identifiers”CAIP-2 gives blockchain networks unambiguous identifiers such as eip155:8453 for Base mainnet. Use the exact identifier advertised by the facilitator. The package accepts the eip155:* family when EVM support is enabled and the solana:* family when Solana Virtual Machine (SVM) support is enabled; this does not guarantee that a facilitator accepts every network in that family.
Override one request
Section titled “Override one request”Override config defaults for a specific page:
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});Enable Solana support
Section titled “Enable Solana support”Solana is opt-in. Install @x402/svm and enable it in config:
pnpm add @x402/svmSet svm: true, use the CAIP-2 Solana identifier supported by the facilitator, and disable EVM if the site only accepts Solana payments. For example, the Solana mainnet identifier used by @x402/svm 2.8 is solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:
x402({ payTo: "YourSolanaAddress", network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", svm: true, evm: false, // Disable EVM if only using Solana});Request flow
Section titled “Request flow”- The integration puts the payment enforcer on
Astro.locals.x402before the page runs. enforce()checks for apayment-signatureheader.- Without a payment header,
enforce()returns a402 Payment Requiredresponse whose body andPAYMENT-REQUIREDheader describe the accepted payment. - With a matching payment, the configured facilitator verifies and settles it.
enforce()returns the payer and settlement result. CallapplyHeaders()so the page response includes the facilitator’sPAYMENT-RESPONSEproof.