Verrouiller un endpoint d'API avec x402 : accepter les paiements USDC sur votre serveur Node.js
Thomas CosiallsVendre l'accès à une API supposait autrefois de construire des comptes, des clés d'API, un système de facturation et une intégration Stripe avant d'encaisser le moindre centime. Le protocole x402 ramène tout cela à un seul code de statut HTTP : votre serveur répond 402 Payment Required avec un prix, le client paie en USDC, et la même requête aboutit quelques secondes plus tard. Sans inscription, sans clé d'API, sans abonnement et sans frais de protocole.
Dans ce tutoriel, nous construirons la boucle complète, des deux côtés de la transaction :
- Côté vendeur : un serveur Express avec un endpoint,
GET /api/premium, verrouillé derrière un paiement de 0,01 $ en USDC sur Base. - Côté acheteur : un client JavaScript et un client Python qui détiennent une clé privée, détectent le
402, signent le paiement et rejouent la requête automatiquement.
Tout tourne sur Base Sepolia (testnet) avec de l'USDC de test gratuit, et nous terminons par les changements exacts à faire pour passer en mainnet.
Le fonctionnement de x402 en un schéma
x402 est un standard ouvert, créé chez Coinbase et désormais porté par la x402 Foundation sous l'égide de la Linux Foundation. Il met enfin au travail le code 402 Payment Required, réservé de longue date : les serveurs annoncent un prix de façon lisible par une machine, les clients le paient au moyen d'une autorisation de stablecoin signée, et un troisième service, le facilitateur, vérifie et règle le paiement on-chain, de sorte que votre serveur ne dialogue jamais avec un nœud blockchain.

Trois détails rendent ce flux remarquable :
- L'acheteur ne dépense jamais de gas. L'étape 3 est une signature hors chaîne (EIP-3009
transferWithAuthorization, nativement pris en charge par l'USDC). C'est le facilitateur qui soumet la transaction et paie le gas. - Les fonds ne bougent que si le serveur livre. La signature autorise exactement le montant annoncé, et le règlement intervient après l'exécution de votre handler (étape 7). Il n'y a rien à rembourser et rien à contester.
- Le facilitateur n'a pas la garde des fonds. Il exécute des payloads signés ; il ne peut ni modifier le montant ni rediriger les fonds.
Ce que nous allons construire

Côté vendeur, c'est votre API existante plus un middleware. Côté acheteur, c'est n'importe quel client HTTP enveloppé d'un intercepteur x402 et d'un wallet. Le facilitateur (celui, public, de x402.org en testnet ; Coinbase CDP ou un autre fournisseur en mainnet) se place entre votre serveur et la chaîne.
Prérequis
- Node.js 20+ et npm (ou yarn)
- Python 3.10+ pour l'acheteur Python
- Deux adresses EVM : une pour recevoir les fonds (le vendeur, l'adresse suffit, aucune clé privée n'est nécessaire côté serveur) et une pour payer (l'acheteur, clé privée requise)
- De l'USDC de test sur Base Sepolia pour l'acheteur, gratuit via le faucet de Circle
S'il vous faut de nouvelles clés, générez un wallet jetable en local :
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
console.log('Address: ', account.address)
console.log('Private key: ', privateKey)Lancez-le avec node generate-wallet.mjs après un npm install viem. Gardez la clé privée hors de l'historique de votre shell et de votre dépôt git ; nous la chargerons plus tard depuis une variable d'environnement. Approvisionnez l'adresse acheteuse en USDC de test via le faucet (choisissez « Base Sepolia »). L'acheteur n'a besoin d'aucun ETH : les paiements sont sans gas des deux côtés.
Partie 1 : verrouiller l'endpoint (côté vendeur)
Créez le projet serveur et installez les paquets x402 :
mkdir x402-server && cd x402-server npm init -y && npm pkg set type=module npm install express @x402/express @x402/core @x402/evm
La configuration du vendeur tient en une seule variable d'environnement : l'adresse qui reçoit les USDC.
PAY_TO_ADDRESS=0xYourSellerAddressVoici le serveur complet :
import express from 'express'
import { paymentMiddleware, x402ResourceServer } from '@x402/express'
import { ExactEvmScheme } from '@x402/evm/exact/server'
import { HTTPFacilitatorClient } from '@x402/core/server'
const payTo = process.env.PAY_TO_ADDRESS
if (!payTo) throw new Error('PAY_TO_ADDRESS not configured')
// Public facilitator, testnet only. Swap for a mainnet facilitator in production.
const facilitatorClient = new HTTPFacilitatorClient({
url: 'https://x402.org/facilitator',
})
// Registers which payment scheme we accept on which network.
const resourceServer = new x402ResourceServer(facilitatorClient).register(
'eip155:84532', // Base Sepolia
new ExactEvmScheme(),
)
const app = express()
app.use(
paymentMiddleware(
{
'GET /api/premium': {
accepts: [
{
scheme: 'exact',
price: '$0.01',
network: 'eip155:84532',
payTo,
},
],
description: 'Premium market report, paid per request',
mimeType: 'application/json',
},
},
resourceServer,
),
)
app.get('/api/premium', (req, res) => {
res.json({
report: 'Institutional flows turned net positive this week.',
generatedAt: new Date().toISOString(),
})
})
app.get('/api/free', (req, res) => {
res.json({ status: 'ok' }) // Routes not listed in the middleware stay free.
})
app.listen(4021, () => console.log('Listening on http://localhost:4021'))Reprenons les éléments un à un :
HTTPFacilitatorClientpointe vers le facilitateur qui vérifiera et réglera les paiements.https://x402.org/facilitatorest gratuit et prend en charge Base Sepolia, ce qui convient parfaitement au développement.x402ResourceServer.register(...)associe un schéma de paiement à un réseau. Le schémaexactsignifie prix fixe : l'acheteur signe pour exactement le montant annoncé. Les réseaux utilisent les identifiants CAIP-2 :eip155:84532est Base Sepolia,eip155:8453est le mainnet Base.paymentMiddlewareassocie des motifs de route à leurs conditions de paiement.price: '$0.01'est un montant en dollars ; le middleware le traduit en unités atomiques d'USDC (10000, l'USDC ayant 6 décimales). Seules les routes que vous listez sont verrouillées.- Le handler de route, lui, ne change pas d'une ligne. Au moment où il s'exécute, le paiement a déjà été vérifié.
Démarrez-le et interrogez l'endpoint sans payer :
node --env-file=.env server.js curl -i http://localhost:4021/api/premium
Vous obtenez un 402 et une offre lisible par une machine (tronquée ici) :
{
"x402Version": 2,
"error": "Payment required",
"accepts": [
{
"scheme": "exact",
"network": "eip155:84532",
"maxAmountRequired": "10000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0xYourSellerAddress",
"resource": "http://localhost:4021/api/premium",
"description": "Premium market report, paid per request",
"mimeType": "application/json",
"maxTimeoutSeconds": 60,
"extra": { "name": "USDC", "version": "2" }
}
]
}Ce tableau accepts est le cœur du protocole : tout ce dont un acheteur a besoin pour construire un paiement valide, y compris l'adresse du contrat USDC (asset) et votre adresse de réception (payTo). Votre endpoint est désormais monétisé. Personne ne peut l'appeler sans payer, et vous n'avez pas écrit une ligne de code de facturation.
Partie 2 : payer l'endpoint depuis JavaScript (côté acheteur)
L'acheteur enveloppe un fetch standard dans un intercepteur qui gère toute la chorégraphie du 402 de façon transparente. Dans un projet séparé :
mkdir x402-buyer && cd x402-buyer npm init -y && npm pkg set type=module npm install @x402/fetch @x402/core @x402/evm viem
Fournissez au client la clé privée de l'acheteur via l'environnement, jamais dans le code :
EVM_PRIVATE_KEY=0xYourBuyerPrivateKeyimport { wrapFetchWithPayment, x402HTTPClient } from '@x402/fetch'
import { x402Client } from '@x402/core/client'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'
const privateKey = process.env.EVM_PRIVATE_KEY
if (!privateKey) throw new Error('EVM_PRIVATE_KEY not configured')
// The signer that will authorize USDC transfers.
const signer = privateKeyToAccount(privateKey)
// Register the exact scheme for every EVM network (eip155:*).
const client = new x402Client()
client.register('eip155:*', new ExactEvmScheme(signer))
// A drop-in fetch that pays 402s automatically.
const fetchWithPayment = wrapFetchWithPayment(fetch, client)
const response = await fetchWithPayment('http://localhost:4021/api/premium')
const data = await response.json()
console.log('Body:', data)
// The settlement receipt travels back in the PAYMENT-RESPONSE header.
const receipt = new x402HTTPClient(client).getPaymentSettleResponse((name) =>
response.headers.get(name),
)
console.log('Paid on', receipt.network, '- tx:', receipt.transaction)Lancez-le :
node --env-file=.env client.mjs
Body: { report: 'Institutional flows turned net positive this week.', generatedAt: '2026-07-10T09:14:52.113Z' } Paid on eip155:84532 - tx: 0x6e1f...c40b
Sous le capot, wrapFetchWithPayment a émis la première requête, reçu le 402, choisi une option de paiement qu'il pouvait honorer, signé l'autorisation EIP-3009 avec votre clé, rejoué la requête avec l'en-tête PAYMENT-SIGNATURE et vous a remis le 200 final. Le hash de transaction figurant dans le reçu correspond à un vrai transfert, consultable sur Sepolia Basescan.
Il existe un paquet @x402/axios équivalent si votre base de code utilise les intercepteurs axios plutôt que fetch.
Partie 3 : le même acheteur en Python
Les agents IA sont les acheteurs naturels des API verrouillées par x402, et une grande partie de cet écosystème vit en Python. Le paquet x402 reproduit le flux JavaScript avec un client httpx :
pip install "x402[httpx]" eth-account python-dotenv
import asyncio
import os
from dotenv import load_dotenv
from eth_account import Account
from x402 import x402Client
from x402.http import x402HTTPClient
from x402.http.clients import x402HttpxClient
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact.register import register_exact_evm_client
load_dotenv()
private_key = os.getenv("EVM_PRIVATE_KEY")
if not private_key:
raise RuntimeError("EVM_PRIVATE_KEY not configured")
account = Account.from_key(private_key)
client = x402Client()
register_exact_evm_client(client, EthAccountSigner(account))
async def main() -> None:
async with x402HttpxClient(client) as http:
response = await http.get("http://localhost:4021/api/premium")
print("Body:", response.json())
receipt = x402HTTPClient(client).get_payment_settle_response(
lambda name: response.headers.get(name)
)
print("Paid on", receipt.network, "- tx:", receipt.transaction)
asyncio.run(main())Lancez python buyer.py : vous obtenez la même réponse payante et le même reçu de règlement. La structure est identique à celle du client JavaScript : construire un signataire à partir de la clé privée, enregistrer le schéma EVM exact, et laisser le client HTTP enveloppé absorber la poignée de main 402.
Passer en mainnet
Trois changements suffisent pour la production :
- Réseau : remplacez
eip155:84532pareip155:8453(mainnet Base), dans la configuration du middleware comme dans l'enregistrement du resource server. L'USDC mainnet sur Base se trouve à l'adresse0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; le SDK la résout pour vous. - Facilitateur : celui de
x402.orgest réservé au testnet. PointezHTTPFacilitatorClientvers un facilitateur de production, comme Coinbase CDP (https://api.cdp.coinbase.com/platform/v2/x402) ou un autre fournisseur listé dans l'annuaire des facilitateurs. - Fonds : le wallet acheteur a désormais besoin de vrais USDC sur Base, et votre adresse
payToreçoit de vrais revenus, immédiatement et définitivement.
Vous pouvez aussi enregistrer plusieurs réseaux à la fois (Base et Solana, par exemple) et laisser chaque acheteur payer sur la chaîne de son choix : le tableau accepts liste simplement davantage d'options.
Points de sécurité à ne pas négliger
- La clé privée de l'acheteur est le seul secret de tout le système. Chargez-la depuis une variable d'environnement ou un gestionnaire de secrets, ne la committez jamais et ne la journalisez jamais. Utilisez un hot wallet dédié ne contenant qu'un flottant de dépense (quelques dollars d'USDC), pas votre trésorerie : la clé réside sur une machine qui effectue des paiements automatiques, alors partez du principe qu'elle peut fuiter et limitez les dégâts.
- Le vendeur, lui, ne détient aucun secret. Le serveur ne connaît que votre adresse publique de réception. Il n'y a aucune clé à faire tourner et rien à voler ; envisagez un wallet matériel ou multisig comme
payTopour de vrais revenus. - Les montants sont bornés par construction. Une autorisation EIP-3009 vaut pour un montant, un destinataire et une fenêtre temporelle précis, avec un nonce unique : elle ne peut être ni rejouée ni gonflée. Les clients peuvent en outre plafonner dans le SDK ce qu'ils acceptent de payer par requête.
- Gardez la barrière devant tout ce qui est payant. Le middleware ne protège que les routes que vous listez ; un refactoring qui renomme une route sans toucher à la configuration du middleware la rend silencieusement gratuite. Ajoutez un test qui vérifie qu'une requête non payée reçoit bien un
402.
Là où cela devient intéressant
Un prix sur un endpoint HTTP est une primitive modeste aux conséquences considérables. Elle permet à un agent IA d'acheter un jeu de données en pleine tâche sans qu'un humain saisisse une carte ; elle permet d'ouvrir votre microservice interne au monde comme un produit en une après-midi ; elle permet de vendre un contenu à l'article à des lecteurs anonymes, dans n'importe quel pays. Nous détaillons ces concepts sur notre page de service x402, avec les cas d'usage qui fonctionnent aujourd'hui.
Chez Etherwave Labs, nous intégrons x402 de bout en bout : verrouillage et tarification de votre API, branchement de facilitateurs de production, et équipement de vos agents avec des wallets de dépense sûrs. Si vous voulez une API nativement payante en production, parlons-en.

