# x402 Payments

> Require x402 payments for server-rendered Astro routes.

Source: https://emdashcmseverything.com/official-docs/guides/x402-payments/

The `@emdash-cms/x402` package adds [x402 payment protocol](https://www.x402.org/) 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

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

Install the package with your package manager:

#### pnpm

```bash
pnpm add @emdash-cms/x402
```

#### npm

```bash
npm install @emdash-cms/x402
```

#### yarn

```bash
yarn add @emdash-cms/x402
```

## 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:

```js title="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",
		}),
	],
});
```

Add the type reference so TypeScript knows about `Astro.locals.x402`:

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

## 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:

```astro title="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>
```

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

Enable `botOnly` in the integration configuration:

```js title="astro.config.mjs"
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":

```astro
---
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)
---
```

> **Caution**
>
> Bot-only mode requires a Cloudflare Workers deployment with Bot Management enabled. Local,
> non-Cloudflare, and Cloudflare requests without Bot Management data all skip enforcement.

## 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:

```astro title="src/pages/posts/[...slug].astro"
---
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>
```

## 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:

```astro
---
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>
)}
```

> **Caution**
>
> `hasPayment()` only checks for the presence of a payment header. An attacker can send an invalid
> header. Use `enforce()` before returning paid content.

## 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

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

[CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) 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

Override config defaults for a specific page:

```ts
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

Solana is opt-in. Install `@x402/svm` and enable it in config:

```bash
pnpm add @x402/svm
```

Set `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`:

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

## Request flow

1. The integration puts the payment enforcer on `Astro.locals.x402` before the page runs.
2. `enforce()` checks for a `payment-signature` header.
3. Without a payment header, `enforce()` returns a `402 Payment Required` response whose body and `PAYMENT-REQUIRED` header describe the accepted payment.
4. With a matching payment, the configured facilitator verifies and settles it.
5. `enforce()` returns the payer and settlement result. Call `applyHeaders()` so the page response includes the facilitator's `PAYMENT-RESPONSE` proof.
