Skip to main content

Create an x402 server with ERC-7710

In this guide, you build a Node.js server that charges for HTTP API access using x402 and accepts ERC-7710 delegation payments verified through the MetaMask facilitator.

The official @x402/express middleware doesn't support ERC-7710 delegation payloads. This guide shows you how to implement the x402 HTTP contract manually with Express.

Prerequisites

Steps

1. Install the dependencies

npm install viem

2. Create payment requirements

Define the payment requirements that every protected route returns to buyers. Follow the x402 payment requirements schema, and set scheme to erc7710.

Parameters

NameDescription
schemePayment scheme the facilitator uses to interpret the payload. Set this to erc7710 so the facilitator routes the payment to its delegation verifier instead of the default exact scheme.
networkCAIP-2 chain identifier for the payment. The example uses eip155:8453, the identifier for Base.
maxAmountRequiredMaximum amount the buyer must authorize, expressed in the wei format. The example charges 0.01 USDC.
resourceProtected route this requirement applies to.
descriptionShort, human-readable label that buyers see in the x402 response and your /info discovery payload.
mimeTypeContent type of the response the buyer receives after a successful payment.
payToSeller wallet address that receives the settled funds.
maxTimeoutSecondsMaximum time, in seconds, the buyer's payment authorization stays valid before settlement must complete.
assetERC-20 token contract address used for payment. The example uses USDC on Base.

Implementation

import { PaymentRequirements } from './type'
import { Hex } from 'viem'

export const paymentRequirements: PaymentRequirements = {
scheme: 'erc7710',
// CAIP-2 identifier for Base.
network: 'eip155:8453',
// 0.1 USDC in wei format.
maxAmountRequired: '10000',
resource: '/api/premium',
description: 'Premium API access',
mimeType: 'application/json',
payTo: '<YOUR_PAYTO_ADDRESS>',
maxTimeoutSeconds: 60,
// USDC contract address on Base.
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}

3. Add a discovery method

Add a public GET /info route that publishes your protected paths and their payment terms. Buyers call this route before they prepare an ERC-7710 delegation payment.

// src/index.ts
import express from 'express'
import { paymentRequirements } from './payment'

const app = express()
app.use(express.json())

app.get('/info', (_req, res) => {
res.json({
routes: ['/api/premium'],
accepts: [paymentRequirements],
})
})

4. Add payment middleware

Add payment middleware that runs before each protected handler. Parse the base64 X-PAYMENT header to extract the encoded delegation chain (permissionContext) the buyer prepared.

When the X-PAYMENT header is missing or malformed, respond with 402 Payment Required and a JSON body that tells the buyer how to pay.

// src/middleware.ts
import type { Request, Response, NextFunction } from 'express'
import { paymentRequirements } from './payment'

export function requirePayment(req: Request, res: Response, next: NextFunction) {
const header = req.header('X-PAYMENT')

if (!header) {
return res.status(402).json({
x402Version: 1,
accepts: [paymentRequirements],
error: 'X-PAYMENT header missing',
})
}

try {
const decoded = JSON.parse(Buffer.from(header, 'base64').toString('utf8'))
res.locals.paymentPayload = decoded
return next()
} catch (error) {
return res.status(402).json({
x402Version: 1,
accepts: [paymentRequirements],
error: 'X-PAYMENT header is not valid base64-encoded JSON',
})
}
}

5. Verify the payment

Call the MetaMask facilitator's verify endpoint to confirm the encoded delegation chain authorizes the requested resource. If verification fails, return 402 with the failure reason from the facilitator so the buyer can correct the payment and retry.

// src/middleware.ts
import { FACILITATOR_URL } from './config'

export async function verifyPayment(req: Request, res: Response, next: NextFunction) {
const response = await fetch(`${FACILITATOR_URL}/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
x402Version: 1,
paymentPayload: res.locals.paymentPayload,
paymentRequirements,
}),
})

const result = await response.json()

if (!response.ok || result.isValid !== true) {
return res.status(402).json({
x402Version: 1,
accepts: [paymentRequirements],
error: result.invalidReason ?? 'Verification failed',
})
}

return next()
}

6. Settle the payment

After verification succeeds, run your business logic and call the MetaMask facilitator's settle endpoint. Attach the settlement result to the response as the X-PAYMENT-RESPONSE header so the buyer can correlate it with the original request.

The example returns { message: 'Premium content unlocked' } as a placeholder. Replace it with whatever JSON payload your protected route serves.

// src/index.ts
import { requirePayment, verifyPayment } from './middleware'
import { paymentRequirements } from './payment'
import { FACILITATOR_URL } from './config'

app.get('/api/premium', requirePayment, verifyPayment, async (_req, res) => {
const settlement = await fetch(`${FACILITATOR_URL}/settle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
x402Version: 1,
paymentPayload: res.locals.paymentPayload,
paymentRequirements,
}),
}).then(r => r.json())

const encoded = Buffer.from(JSON.stringify(settlement)).toString('base64')
res.setHeader('X-PAYMENT-RESPONSE', encoded)
// Run your business logic and send as a response.
res.json({ message: 'Premium content unlocked' })
})

app.listen(4402, () => {
console.log('Seller listening on port 4402')
})

Next steps