El paquete @emdash-cms/x402 añade compatibilidad con el protocolo de pago x402 a cualquier sitio Astro en Cloudflare. Funciona de forma independiente — sin depender del núcleo de EmDash — y encaja bien con los campos del CMS de EmDash para precios por página.
x402 es un protocolo de pago nativo de HTTP. Si un cliente solicita un recurso de pago sin pagar, el servidor responde con 402 Payment Required e instrucciones de pago legibles por máquina. Agentes y navegadores que entienden x402 pueden completar el pago y reintentar la petición.
Cuándo usarlo
El caso más habitual es el modo solo bots: cobrar a agentes de IA y scrapers el acceso al contenido mientras los visitantes humanos leen gratis. Se usa Cloudflare Bot Management para distinguir bots de humanos.
También puedes exigir pago a todos los visitantes, o solo comprobar cabeceras de pago sin exigir (renderizado condicional).
Instalación
pnpm
pnpm add @emdash-cms/x402 npm
npm install @emdash-cms/x402 yarn
yarn add @emdash-cms/x402 Configuración
Añade la integración a la configuración de Astro:
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",
botOnly: true,
botScoreThreshold: 30,
}),
],
});
Añade la referencia de tipos para que TypeScript conozca Astro.locals.x402:
/// <reference types="@emdash-cms/x402/locals" />
Uso básico
La integración coloca un aplicador en Astro.locals.x402. Llama a enforce() en el frontmatter de la página para proteger el contenido tras el pago:
---
const { x402 } = Astro.locals;
const result = await x402.enforce(Astro.request, {
price: "$0.05",
description: "Artículo premium",
});
// Sin pago válido, enforce() devuelve una Response 402.
// Devuélvela directamente para enviar instrucciones de pago al cliente.
if (result instanceof Response) return result;
// Pago verificado (o omitido en modo botOnly). Aplica cabeceras de respuesta
// para la prueba de liquidación.
x402.applyHeaders(result, Astro.response);
---
<article>
<h1>Contenido premium</h1>
</article>
enforce() devuelve:
- una
Response(402) — el cliente debe pagar; devuélvela tal cual. - un
EnforceResult— la petición debe continuar. El contenido estaba pagado o la aplicación se omitió (humano en modo botOnly).
Modo solo bots
Con botOnly en true, la integración lee request.cf.botManagement.score:
- Puntuación por debajo del umbral (por defecto 30) → se trata como bot, se exige pago
- Puntuación en o por encima del umbral → humano, se omite la aplicación
- Sin datos de bot management (desarrollo local, sin CF) → humano
EnforceResult incluye skipped para distinguir «no tuvo que pagar» de «pagó»:
---
const result = await x402.enforce(Astro.request, { price: "$0.01" });
if (result instanceof Response) return result;
x402.applyHeaders(result, Astro.response);
// result.paid — true si el pago se verificó
// result.skipped — true si se omitió (humano en botOnly)
// result.payer — dirección del pagador (si pagó)
---
Precio por página con EmDash
Con EmDash puedes añadir un campo number en la colección para el precio por página. No hace falta esquema especial — un campo CMS normal:
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry } = await getEmDashEntry("posts", slug);
if (!entry) return Astro.redirect("/404");
const { x402 } = Astro.locals;
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>
Comprobar pago sin exigir
Usa hasPayment() para ver si la petición incluye cabeceras de pago sin verificar ni exigir. Útil para renderizado condicional — distinto contenido para quien pagó o no:
---
const { x402 } = Astro.locals;
const hasPaid = x402.hasPayment(Astro.request);
---
{hasPaid ? (
<p>Aquí el contenido premium completo.</p>
) : (
<p>Suscríbete para leer el artículo completo.</p>
)}
Referencia de configuración
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
payTo | string | obligatorio | Dirección de cartera de destino |
network | string | obligatorio | Identificador CAIP-2 (p. ej. eip155:8453) |
defaultPrice | Price | — | Precio por defecto, sobrescribible por página |
facilitatorUrl | string | https://x402.org/facilitator | URL del facilitador |
scheme | string | "exact" | Esquema de pago |
maxTimeoutSeconds | number | 60 | Tiempo máximo para firmas de pago |
evm | boolean | true | Habilitar cadenas EVM |
svm | boolean | false | Habilitar Solana (requiere @x402/svm) |
botOnly | boolean | false | Exigir pago solo a bots |
botScoreThreshold | number | 30 | Umbral de score de bot (1–99, menor = más bot) |
Formato de precio
Los precios pueden indicarse así:
- Cadena en dólares —
"$0.10"(se quita el prefijo$, el valor se pasa tal cual) - Cadena numérica —
"0.10" - Número —
0.10 - Objeto —
{ amount: "100000", asset: "0x...", extra: {} }para activo/cantidad explícitos
Identificadores de red
Las redes usan el formato CAIP-2:
| Red | Identificador |
|---|---|
| Base mainnet | eip155:8453 |
| Base Sepolia | eip155:84532 |
| Ethereum | eip155:1 |
| Solana | solana:mainnet |
Opciones de enforce
Sobrescribe los valores por defecto para una página concreta:
await x402.enforce(Astro.request, {
price: "$0.25",
payTo: "0xDifferentWallet",
network: "eip155:1",
description: "Artículo: Cómo funciona x402",
mimeType: "text/html",
});
Soporte Solana
Solana es opcional. Instala @x402/svm y actívalo en la config:
pnpm add @x402/svm
x402({
payTo: "YourSolanaAddress",
network: "solana:mainnet",
svm: true,
evm: false,
});
Cómo funciona
- La integración
x402()registra middleware que crea el aplicador y lo coloca enAstro.locals.x402. - La configuración llega al middleware mediante un módulo virtual de Vite (
virtual:x402/config). - Al llamar
enforce(), se comprueba la cabecerapayment-signatureen la petición. - Si no hay cabecera de pago, se responde
402 Payment Requiredcon instrucciones enPAYMENT-REQUIRED. - Si hay cabecera, se verifica y liquida mediante el facilitador.
- Tras la liquidación,
applyHeaders()establecePAYMENT-RESPONSEen la respuesta.
El servidor de recursos se inicializa de forma diferida en la primera petición y se cachea durante la vida del worker.