Pagos x402

En esta página

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ónTipoPredeterminadoDescripción
payTostringobligatorioDirección de cartera de destino
networkstringobligatorioIdentificador CAIP-2 (p. ej. eip155:8453)
defaultPricePricePrecio por defecto, sobrescribible por página
facilitatorUrlstringhttps://x402.org/facilitatorURL del facilitador
schemestring"exact"Esquema de pago
maxTimeoutSecondsnumber60Tiempo máximo para firmas de pago
evmbooleantrueHabilitar cadenas EVM
svmbooleanfalseHabilitar Solana (requiere @x402/svm)
botOnlybooleanfalseExigir pago solo a bots
botScoreThresholdnumber30Umbral 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úmero0.10
  • Objeto{ amount: "100000", asset: "0x...", extra: {} } para activo/cantidad explícitos

Identificadores de red

Las redes usan el formato CAIP-2:

RedIdentificador
Base mainneteip155:8453
Base Sepoliaeip155:84532
Ethereumeip155:1
Solanasolana: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

  1. La integración x402() registra middleware que crea el aplicador y lo coloca en Astro.locals.x402.
  2. La configuración llega al middleware mediante un módulo virtual de Vite (virtual:x402/config).
  3. Al llamar enforce(), se comprueba la cabecera payment-signature en la petición.
  4. Si no hay cabecera de pago, se responde 402 Payment Required con instrucciones en PAYMENT-REQUIRED.
  5. Si hay cabecera, se verifica y liquida mediante el facilitador.
  6. Tras la liquidación, applyHeaders() establece PAYMENT-RESPONSE en la respuesta.

El servidor de recursos se inicializa de forma diferida en la primera petición y se cachea durante la vida del worker.