Gate an API Endpoint with x402: Accept USDC Payments on Your Node.js Server

Photo of Thomas CosiallsThomas Cosialls

Selling access to an API used to mean building accounts, API keys, a billing system, and a Stripe integration before you could charge a single cent. The x402 protocol collapses all of that into one HTTP status code: your server answers 402 Payment Required with a price, the client pays in USDC, and the same request succeeds seconds later. No sign-up, no API keys, no subscriptions, and no protocol fees.

In this tutorial we will build the complete loop, on both sides of the transaction:

  • Seller side: an Express server with one endpoint, GET /api/premium, gated behind a $0.01 USDC payment on Base.
  • Buyer side: a JavaScript client and a Python client that hold a private key, detect the 402, sign the payment, and retry automatically.

Everything runs on Base Sepolia (testnet) with free test USDC, and we finish with the exact changes needed to go to mainnet.

How x402 works in one diagram

x402 is an open standard, created at Coinbase and now stewarded by the x402 Foundation under the Linux Foundation. It puts the long-reserved 402 Payment Required status code to work: servers advertise a price in a machine-readable way, clients pay it with a signed stablecoin authorization, and a third service called a facilitator verifies and settles the payment onchain so your server never talks to a blockchain node.

x402 payment sequence: request, 402, signed retry, verify, settle, response
The full x402 request lifecycle. Solid arrows are requests, dashed arrows are responses.

Three details make this flow remarkable:

  • The buyer never spends gas. Step 3 is an off-chain signature (EIP-3009 transferWithAuthorization, natively supported by USDC). The facilitator submits the transaction and pays the gas.
  • Funds only move if the server delivers. The signature authorizes exactly the advertised amount, and settlement happens after your handler runs (step 7). There is nothing to refund and nothing to charge back.
  • The facilitator is non-custodial. It executes signed payloads; it can neither change the amount nor redirect the funds.

What we are building

x402 network architecture: buyer client and wallet, Express server with x402 middleware, facilitator, and the USDC contract on Base
The four moving pieces: a buyer script with a hot wallet, your Express server, a facilitator, and USDC on Base.

The seller side is your existing API plus one middleware. The buyer side is any HTTP client wrapped with an x402 interceptor and a wallet. The facilitator (the public x402.org one on testnet, Coinbase CDP or another provider on mainnet) sits between your server and the chain.

Prerequisites

  • Node.js 20+ and npm (or yarn)
  • Python 3.10+ for the Python buyer
  • Two EVM addresses: one to receive funds (seller, address only, no private key needed on the server) and one to pay (buyer, private key required)
  • Test USDC on Base Sepolia for the buyer, free from Circle's faucet

If you need fresh keys, generate a throwaway wallet locally:

generate-wallet.mjs
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'

const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)

console.log('Address:     ', account.address)
console.log('Private key: ', privateKey)

Run it with node generate-wallet.mjs after npm install viem. Keep the private key out of your shell history and your git repository; we will load it from an environment variable later. Fund the buyer address with test USDC from the faucet (select "Base Sepolia"). The buyer needs no ETH at all: payments are gasless for both sides.

Part 1: gate the endpoint (seller side)

Create the server project and install the x402 packages:

mkdir x402-server && cd x402-server npm init -y && npm pkg set type=module npm install express @x402/express @x402/core @x402/evm

The seller configuration is a single environment variable, the address that receives the USDC:

.env
PAY_TO_ADDRESS=0xYourSellerAddress

Now the whole server:

server.js
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'))

Walk through the pieces:

  • HTTPFacilitatorClient points at the facilitator that will verify and settle payments. https://x402.org/facilitator is free and supports Base Sepolia, which is perfect for development.
  • x402ResourceServer.register(...) binds a payment scheme to a network. The exact scheme means fixed-price: the buyer signs for exactly the advertised amount. Networks use CAIP-2 identifiers, so eip155:84532 is Base Sepolia and eip155:8453 is Base mainnet.
  • paymentMiddleware maps route patterns to their payment requirements. price: '$0.01' is a dollar amount; the middleware translates it into atomic USDC units (10000, since USDC has 6 decimals). Only the routes you list are gated.
  • The route handler itself is completely unchanged. By the time it runs, the payment has already been verified.

Start it and probe the endpoint without paying:

node --env-file=.env server.js curl -i http://localhost:4021/api/premium

You get a 402 and a machine-readable offer (trimmed here):

402 response body
{
  "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" }
    }
  ]
}

That accepts array is the heart of the protocol: everything a buyer needs to construct a valid payment, including the USDC contract address (asset) and your receiving address (payTo). Your endpoint is now monetized. Nobody can call it without paying, and you did not write a line of billing code.

Part 2: pay for the endpoint from JavaScript (buyer side)

The buyer wraps a standard fetch with an interceptor that handles the whole 402 dance transparently. In a separate project:

mkdir x402-buyer && cd x402-buyer npm init -y && npm pkg set type=module npm install @x402/fetch @x402/core @x402/evm viem

Give the client the buyer's private key through the environment, never in code:

.env
EVM_PRIVATE_KEY=0xYourBuyerPrivateKey
client.mjs
import { 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)

Run it:

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

Under the hood, wrapFetchWithPayment made the first request, received the 402, picked a payment option it could satisfy, signed the EIP-3009 authorization with your key, retried with the PAYMENT-SIGNATURE header, and handed you the final 200. The transaction hash in the receipt is a real transfer you can inspect on Sepolia Basescan.

There is an equivalent @x402/axios package if your codebase uses axios interceptors instead of fetch.

Part 3: the same buyer in Python

AI agents are the natural buyers of x402-gated APIs, and much of that ecosystem lives in Python. The x402 package mirrors the JavaScript flow with an httpx client:

pip install "x402[httpx]" eth-account python-dotenv
buyer.py
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())

Run python buyer.py and you get the same paid response and settlement receipt. The structure is identical to the JavaScript client: build a signer from the private key, register the exact EVM scheme, and let the wrapped HTTP client absorb the 402 handshake.

Going to mainnet

Three changes take this to production:

  1. Network: replace eip155:84532 with eip155:8453 (Base mainnet) in both the middleware config and the resource server registration. Mainnet USDC on Base lives at 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; the SDK resolves it for you.
  2. Facilitator: the x402.org facilitator is testnet-only. Point HTTPFacilitatorClient at a production facilitator such as Coinbase CDP (https://api.cdp.coinbase.com/platform/v2/x402) or another provider from the facilitators directory.
  3. Funds: the buyer wallet now needs real USDC on Base, and your payTo address receives real revenue, instantly and finally.

You can also register several networks at once (Base plus Solana, for instance) and let each buyer pay on whichever chain they prefer: the accepts array simply lists more options.

Security notes you should not skip

  • The buyer's private key is the only secret in the whole system. Load it from an environment variable or a secret manager, never commit it, and never log it. Use a dedicated hot wallet holding only a spending float (a few dollars of USDC), not your treasury; the key sits on a machine that makes automatic payments, so assume it can leak and cap the blast radius.
  • The seller holds no secrets at all. The server only knows your public receiving address. There is no key to rotate and nothing for an attacker to steal; consider a hardware or multisig wallet as payTo for real revenue.
  • Amounts are bounded by construction. An EIP-3009 authorization is valid for one specific amount, recipient, and time window, with a unique nonce, so it cannot be replayed or inflated. Clients can additionally cap what they are willing to pay per request in the SDK configuration.
  • Keep the gate in front of everything paid. The middleware only protects the routes you list; a refactor that renames a route but not the middleware config silently makes it free. Add a test that asserts unpaid requests get a 402.

Where this gets interesting

A price on an HTTP endpoint is a small primitive with large consequences. It means an AI agent can buy a dataset mid-task without a human filling in a card form; it means your internal microservice can be opened to the world as a product in an afternoon; it means content can be sold per article to anonymous readers in any country. We covered the concepts in more depth on our x402 service page, including the use cases we see working today.

At Etherwave Labs we integrate x402 end to end: gating and pricing your API, wiring production facilitators, and equipping your agents with safe spending wallets. If you want a payment-native API in production, talk to us.

More articles

Cover image for Building Autonomous Crypto Trading Bots with ElizaOS: A Complete Technical Guide

Building Autonomous Crypto Trading Bots with ElizaOS: A Complete Technical Guide

Learn how to build autonomous AI trading bots using ElizaOS framework. Complete guide covering market analysis, decision algorithms, secure deployment with TEE, and real-world implementation. Built by blockchain experts at Etherwave Labs.

Read more
Cover image for The Guide for Collecting Fees and Rewards from Orca Whirlpool Positions

The Guide for Collecting Fees and Rewards from Orca Whirlpool Positions

Learn how to collect trading fees and rewards from Orca Whirlpool positions using Anchor and the whirlpool_cpi crate. Includes the critical update step most developers miss.

Read more

Ready to take your project to the next level?

Contact us today to discuss how we can help you achieve your goals in the blockchain space.