Los pagos x402 se implementan incluyendo las condiciones de pago en la respuesta 402 de la API. Prepare Node.js y una cartera de prueba. Primero, verifique la firma y la liquidación de v2 en Base Sepolia. Configure la entrega de la respuesta de pago para que solo se envíe después de una liquidación correcta.
La información sobre importes y cadenas se basa en la documentación oficial de x402, Base y Circle enlazada como fuente.
Preparación e instalación para la práctica
Prepare un entorno de desarrollo donde pueda modificar la API y una cartera de prueba. El ejemplo está preparado para Node.js 22 y x402 SDK 2.28.0. La estructura de instalación sigue la guía oficial para vendedores.
- Prepare una dirección de cartera EVM para recibir los pagos.
- Prepare otra cartera de prueba para hacer los pagos.
- Consiga USDC de prueba de Base Sepolia para la cartera de pago.
- Cree el proyecto de práctica con los siguientes comandos.
- Guarde los archivos TypeScript que aparecen más adelante en la misma carpeta.
mkdir x402-demo
cd x402-demo
npm init -y
npm pkg set type=module
npm install --save-exact [email protected] [email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected] @x402/[email protected]
npm install --save-dev --save-exact typescript tsx @types/node @types/express
El servidor receptor no necesita la clave privada de la cartera receptora. Configure la clave privada de pago para que solo se lea desde el proceso del cliente. El método de firma con la cartera se basa en la guía oficial para compradores.
La validación de este ejemplo se limita a la comprobación de tipos y a un flujo de pago simulado. No es un ejemplo en el que se haya completado una transferencia real en la red de prueba. Compruebe los resultados en la cadena durante el procedimiento de ejecución que se indica a continuación.
Prueba la recuperación de solicitudes duplicadas para evitar una segunda liquidación al reintentar el pago.
Comparación entre x402 v1 y v2
Al implementar v2, debe adaptar tanto las cabeceras como los campos del cuerpo. Cambiar solo los nombres de las cabeceras deja una estructura de datos incompatible. La siguiente tabla se basa en la guía oficial de migración.
| Aspecto | v1 | v2 |
|---|---|---|
| Versión del protocolo | x402Version: 1 |
x402Version: 2 |
| Condiciones de pago iniciales | Cuerpo JSON de la respuesta 402 | Cabecera de respuesta PAYMENT-REQUIRED
|
| Datos de pago firmados | Cabecera de solicitud X-PAYMENT
|
Cabecera de solicitud PAYMENT-SIGNATURE
|
| Resultado de la liquidación | Cabecera de respuesta X-PAYMENT-RESPONSE
|
Cabecera de respuesta PAYMENT-RESPONSE
|
| Identificador de la red principal de Base | base |
eip155:8453 |
| Identificador de Base Sepolia | base-sepolia |
eip155:84532 |
| Campo del importe en las condiciones de pago | maxAmountRequired |
amount |
| Descripción del recurso |
resource y otros campos dentro de las condiciones de pago |
Objeto resource de nivel superior |
| Condiciones en los datos de pago |
scheme, network y otros campos de nivel superior |
Objeto accepted que contiene las condiciones seleccionadas |
| Ejemplo de paquete TypeScript | x402-express |
@x402/express |
La ubicación donde se envían las condiciones de pago en v1 está especificada en la especificación HTTP v1. La ubicación estándar en v2 es la cabecera PAYMENT-REQUIRED. Una implementación también puede devolverlas en el cuerpo, pero debe procesarlas tomando la cabecera como referencia.
La especificación HTTP v2 describe el código de estado de la siguiente manera.
El servidor indica que se requiere un pago mediante el código de estado HTTP 402 «Payment Required».
Fuente: especificación HTTP v2 de x402
Las cabeceras de pago de v2 contienen JSON codificado en Base64. Base64 no es un método de cifrado. Use HTTPS en los servicios externos.
Solicitud HTTP 402 y secuencia de pago
Esta práctica utiliza el flujo de autorización predeterminado del esquema EVM exact. exact es un esquema para pagar un importe fijo. La secuencia sigue la especificación principal de x402 v2.
- El cliente solicita la API de pago sin información de pago.
- El servidor devuelve HTTP 402 y
PAYMENT-REQUIRED. - El cliente selecciona unas condiciones permitidas de
accepts. - El SDK firma una autorización de pago que cumple las condiciones seleccionadas.
- El cliente repite la solicitud añadiendo
PAYMENT-SIGNATURE. - El servidor verifica la autorización mediante
/verifydel facilitator. - Si la verificación se supera, el servidor procesa el recurso.
- El servidor solicita la liquidación mediante
/settle. - Tras una liquidación correcta, entrega el cuerpo de pago y
PAYMENT-RESPONSE.
Esta secuencia no es fija ni común a todos los métodos de pago. v2 también define flujos con distintos momentos de liquidación. El siguiente código solo permite el flujo predeterminado authorization.
Funciones del servidor de recursos y del facilitator
El servidor de recursos determina el precio y las condiciones de acceso al contenido. El facilitator se encarga de verificar y liquidar los pagos. Esta división de funciones se basa en la documentación oficial del facilitator.
| Componente | Función | Estado que no debe considerarse un éxito |
|---|---|---|
| Cliente | Seleccionar las condiciones y firmar la autorización de pago | Solo se ha generado la firma |
| Servidor de recursos | Declarar el precio y controlar la respuesta de pago | El cliente afirma que el pago se ha completado |
Facilitator /verify
|
Comprobar la validez de la firma y de las condiciones de pago | Solo se ha recibido isValid: true
|
Facilitator /settle
|
En esta práctica, ejecutar el pago en la cadena y devolver el resultado | Se ha enviado, pero no se ha confirmado el resultado |
/verify es una comprobación que no modifica el estado de la cadena. Por tanto, superar la verificación no significa que se hayan recibido los fondos. El saldo o el estado de la autorización pueden cambiar incluso después de la verificación.
Bloquee el cuerpo de pago si falla la verificación o la liquidación. Bloquear cuando no se ha podido confirmar el éxito se denomina fail-closed. Este comportamiento también puede comprobarse en la implementación oficial de Express.
Comparación entre la red principal y la red de prueba de Base
En la primera ejecución, use solo Base Sepolia. Circle especifica que el USDC de la red de prueba no tiene valor financiero. Las direcciones se basan en la lista de contratos de USDC de Circle.
| Aspecto | Red principal de Base | Red de prueba Base Sepolia |
|---|---|---|
| Uso | Pagos con activos reales | Pruebas de implementación y gestión de fallos |
| Chain ID | 8453 |
84532 |
| Red x402 v2 | eip155:8453 |
eip155:84532 |
| Contrato de USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| RPC público | https://mainnet.base.org |
https://sepolia.base.org |
| Valor financiero del token | Activo con valor financiero real | Sin valor financiero |
Consulte el Chain ID y el RPC en la documentación de la red Base. Aunque la dirección de la cartera sea la misma, los saldos son independientes si las cadenas son distintas. No registre un pago correcto en la red de prueba como una venta real.
Puede solicitar USDC de prueba en Circle Testnet Faucet. Seleccione Base Sepolia como red. En la dirección de recepción, introduzca la dirección de la cartera de prueba que usará para pagar.
Implementación de una API de pago con Node.js
Conecte paymentMiddleware antes de la ruta de pago. El siguiente ejemplo exige 0.001 USDC por llamada. El importe corresponde al precio de ejemplo de la guía oficial para vendedores. (Valor comprobado: 0.001 USDC por llamada · Fuente: docs.x402.org · Comprobado el 2026-10-28)
Archivo del servidor
Guarde el siguiente código como server.ts. El servidor solo acepta solicitudes en la interfaz local. La dirección receptora se lee de la variable de entorno PAY_TO.
import express from "express";
import { getAddress, zeroAddress } from "viem";
import { paymentMiddleware } from "@x402/express";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const resourceServer = new x402ResourceServer(
new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }),
).register(network, new ExactEvmScheme());
const app = express();
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "private, no-store");
next();
});
app.use(paymentMiddleware({
"GET /paid-data": {
accepts: [{ scheme: "exact", price: "$0.001", network, payTo }],
description: "Sample paid JSON",
mimeType: "application/json",
},
}, resourceServer));
app.get("/paid-data", (_req, res) => {
res.json({ content: "Paid sample content" });
});
app.listen(4021, "127.0.0.1");
https://x402.org/facilitator es la dirección de prueba de la guía oficial. En este ejemplo se utiliza para la liquidación en Base Sepolia. Para elegir un facilitator de producción, consulte el procedimiento para la red principal de la guía para vendedores.
Ejemplo de cálculo del precio
El precio del ejemplo equivale a 1000 unidades mínimas de USDC. USDC tiene 6 decimales. La conversión de precios del SDK se basa en la implementación del servidor EVM.
0.001 USDC × 10^6 = 1000 최소 단위
| Valor que debe comprobarse | Valor esperado en este ejemplo |
|---|---|
price en la configuración del servidor |
"$0.001" |
amount en las condiciones de pago del 402 |
"1000" |
asset en las condiciones de pago |
Contrato de USDC de Base Sepolia |
payTo en las condiciones de pago |
Dirección receptora configurada |
network en las condiciones de pago |
eip155:84532 |
price es un valor de configuración del SDK. En las condiciones de pago reales de v2 se envía amount. Compare los importes enteros como cadenas de texto o como bigint.
Ejecución del servidor y comprobación del 402
Ejecute el servidor en el primer terminal Bash. El valor que debe introducir es la dirección pública receptora, no una clave privada. Mantenga este terminal abierto durante la ejecución.
read -r -p '수취 지갑 주소: ' PAY_TO
export PAY_TO
npx tsx server.ts
Envíe una solicitud sin pago desde un segundo terminal. El siguiente comando no firma ningún pago. Muestra el estado HTTP y las condiciones de pago decodificadas.
node --input-type=module <<'JS'
const response = await fetch("http://127.0.0.1:4021/paid-data");
const header = response.headers.get("PAYMENT-REQUIRED");
console.log(response.status);
console.log(header && JSON.parse(
Buffer.from(header, "base64").toString("utf8")
));
JS
La respuesta normal a una solicitud sin pago es HTTP 402. Compare las condiciones mostradas con la tabla anterior. Si el contenido de pago se devuelve directamente, corrija primero la configuración de protección de la ruta.
Selección de condiciones de pago e implementación del cliente
Restrinja el cliente para que solo firme las condiciones permitidas. Este ejemplo solo permite el precio fijo de USDC en Base Sepolia. El filtro de opciones de pago utiliza registerPolicy de la implementación oficial de x402Client.
- Guarde el siguiente código como
client.ts. - Guarde también
check-transfer.ts, que aparece en la siguiente sección. - Una vez preparados ambos archivos, continúe con el procedimiento de ejecución.
import { writeFile } from "node:fs/promises";
import { getAddress, zeroAddress, type Hex, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { decodePaymentResponseHeader } from "@x402/core/http";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { checkTransfer } from "./check-transfer.js";
const key = process.env.EVM_PRIVATE_KEY ?? "";
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error("Invalid test key");
const signer = privateKeyToAccount(key as Hex);
const payTo = getAddress(process.env.PAY_TO ?? "");
if (payTo === zeroAddress) throw new Error("Invalid payTo");
const network = "eip155:84532";
const token = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const client = new x402Client().register(network, new ExactEvmScheme(signer));
client.registerPolicy((version, options) => version === 2
? options.filter(r =>
r.scheme === "exact" && r.network === network &&
r.asset.toLowerCase() === token.toLowerCase() &&
r.payTo.toLowerCase() === payTo.toLowerCase() && r.amount === "1000" &&
(!r.extra?.assetTransferMethod || r.extra.assetTransferMethod === "eip3009") &&
(!r.extra?.paymentFlow || r.extra.paymentFlow === "authorization"))
: []);
const paidFetch = wrapFetchWithPayment(fetch, client);
const response = await paidFetch("http://127.0.0.1:4021/paid-data", {
redirect: "error",
});
const raw = response.headers.get("PAYMENT-RESPONSE");
if (!raw) throw new Error(`No settlement receipt: HTTP ${response.status}`);
const settlement = decodePaymentResponseHeader(raw);
await writeFile("settlement.json", JSON.stringify(settlement, null, 2));
if (!response.ok || !settlement.success || settlement.network !== network) {
throw new Error("Payment unresolved; inspect settlement.json before retrying");
}
const evidence = await checkTransfer(
network, settlement.transaction as Hash, signer.address, payTo, 1000n,
);
await writeFile("transfer.json", JSON.stringify(evidence, null, 2));
console.log(await response.text());
wrapFetchWithPayment se encarga de procesar el 402 y de repetir la solicitud con la firma. Su uso sigue el ejemplo oficial de fetch. Si no hay condiciones de pago permitidas, se detiene la creación del pago.
Este cliente también puede usarse en agentes de IA. Sin embargo, un límite de precio por llamada no es un límite de gasto acumulado. Gestione por separado el presupuesto para las llamadas repetidas del agente.
Verificación del evento Transfer de USDC
Compruebe los registros de transferencia de USDC mediante el hash de la transacción del resultado de liquidación. El estado correcto de la transacción, por sí solo, no confirma el destinatario. Los campos de Transfer se basan en el estándar ERC-20.
La verificación requiere los siguientes elementos.
| Elemento de verificación | Valor con el que se compara |
|---|---|
| Chain ID del RPC | Cadena permitida por el servidor |
status del recibo de la transacción |
Estado correcto |
| Dirección que emitió el evento | Contrato oficial de USDC de esa cadena |
Transfer.from |
Pagador confirmado |
Transfer.to |
Dirección receptora definida por el servidor |
Transfer.value |
Importe exigido por el servidor en unidades mínimas |
Guarde el siguiente código como check-transfer.ts. La consulta utiliza getTransactionReceipt de viem. La interpretación de los eventos se basa en parseEventLogs.
import {
createPublicClient, http, getAddress, parseEventLogs,
erc20Abi, type Address, type Hash,
} from "viem";
import { base, baseSepolia } from "viem/chains";
const configs = {
"eip155:84532": {
chain: baseSepolia,
rpc: "https://sepolia.base.org",
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
},
"eip155:8453": {
chain: base,
rpc: "https://mainnet.base.org",
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
},
} as const;
export async function checkTransfer(
network: keyof typeof configs, hash: Hash,
payer: Address, payTo: Address, amount: bigint,
) {
const cfg = configs[network];
const rpc = createPublicClient({ chain: cfg.chain, transport: http(cfg.rpc) });
if (await rpc.getChainId() !== cfg.chain.id) throw new Error("Wrong chain");
const receipt = await rpc.getTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Transaction failed");
const logs = parseEventLogs({
abi: erc20Abi,
eventName: "Transfer",
logs: receipt.logs.filter(
log => getAddress(log.address) === getAddress(cfg.token),
),
strict: true,
});
const transfer = logs.find(log =>
getAddress(log.args.from) === getAddress(payer) &&
getAddress(log.args.to) === getAddress(payTo) &&
log.args.value === amount,
);
if (!transfer) throw new Error("Matching USDC Transfer not found");
return {
network, transaction: hash, logIndex: transfer.logIndex,
blockHash: receipt.blockHash, blockNumber: receipt.blockNumber.toString(),
};
}
Esta función confirma que hubo una transferencia de USDC en esa transacción. Debe comprobarse por separado si corresponde a un pedido concreto de la API. En un servidor de producción, use las condiciones guardadas del pedido como valores de comparación.
No confíe en el archivo transfer.json enviado por el cliente como prueba para conceder acceso. El servidor de este ejemplo utiliza los resultados de verificación y liquidación del facilitator. Si quiere usar una consulta independiente como condición para conceder acceso, debe ejecutarla en el servidor.
Un fallo en la consulta del recibo no significa necesariamente que el pago haya fallado. Si se trata de un error del RPC, vuelva a consultar el hash de transacción guardado. Defina en la política operativa el número de confirmaciones adicionales y la respuesta a las reorganizaciones de la cadena.