|
| 1 | +--- |
| 2 | +description: Build an x402-compatible Node.js server that accepts ERC-7710 delegation payments. |
| 3 | +sidebar_label: Seller |
| 4 | +keywords: |
| 5 | + [x402, ERC-7710, HTTP 402, Advanced Permissions, facilitator, delegation, Express, Node.js] |
| 6 | +--- |
| 7 | + |
| 8 | +import Tabs from "@theme/Tabs"; |
| 9 | +import TabItem from "@theme/TabItem"; |
| 10 | +import GlossaryTerm from '@theme/GlossaryTerm'; |
| 11 | + |
| 12 | +# Create an x402 server with ERC-7710 |
| 13 | + |
| 14 | +In this guide, you build a Node.js server that charges for HTTP API access using [x402](https://www.x402.org/) and verifies payments through the MetaMask facilitator. |
| 15 | + |
| 16 | +The official `@x402/express` middleware doesn't support ERC-7710 delegation payloads, so you implement the x402 HTTP contract manually. |
| 17 | + |
| 18 | +## Prerequisites |
| 19 | + |
| 20 | +- [Node.js 18](https://nodejs.org/en) or later. |
| 21 | +- [Minimal Node.js Express server](https://expressjs.com/en/starter/installing.html). |
| 22 | +- A seller payout address to recieve funds (e.g. [MetaMask Wallet](https://metamask.io/download)). |
| 23 | + |
| 24 | +## Steps |
| 25 | + |
| 26 | +### 1. Install the dependencies |
| 27 | + |
| 28 | +### 1. Create payment requirements |
| 29 | + |
| 30 | +Define the payment requirements that every protected route returns to buyers. Follow the [x402 payment requirements schema](https://docs.x402.org/core-concepts/http-402#payment-headers-in-v2), and set `scheme` to `erc7710`. |
| 31 | + |
| 32 | +#### Parameters |
| 33 | + |
| 34 | +| Name | Description | |
| 35 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 36 | +| `scheme` | Payment 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. | |
| 37 | +| `network` | [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) chain identifier for the payment. The example uses `eip155:8453`, the identifier for Base. | |
| 38 | +| `maxAmountRequired` | Maximum amount the buyer must authorize, expressed in the wei format. The example charges 0.01 USDC. | |
| 39 | +| `resource` | Protected route this requirement applies to. | |
| 40 | +| `description` | Short, human-readable label that buyers see in the `x402` response and your `/info` discovery payload. | |
| 41 | +| `mimeType` | Content type of the response the buyer receives after a successful payment. | |
| 42 | +| `payTo` | Seller wallet address that receives the settled funds. | |
| 43 | +| `maxTimeoutSeconds` | Maximum time, in seconds, the buyer's payment authorization stays valid before settlement must complete. | |
| 44 | +| `asset` | ERC-20 token contract address used for payment. The example uses USDC on Base. | |
| 45 | + |
| 46 | +#### Implementation |
| 47 | + |
| 48 | +<Tabs> |
| 49 | +<TabItem value = "src/payment.ts"> |
| 50 | + |
| 51 | +```ts |
| 52 | +import { PaymentRequirements } from './type' |
| 53 | +import { Hex } from 'viem' |
| 54 | + |
| 55 | +export const paymentRequirements: PaymentRequirements = { |
| 56 | + scheme: 'erc7710', |
| 57 | + // CAIP-2 identifier for Base. |
| 58 | + network: 'eip155:8453', |
| 59 | + // 0.1 USDC in wei format. |
| 60 | + maxAmountRequired: '10000', |
| 61 | + resource: '/api/premium', |
| 62 | + description: 'Premium API access', |
| 63 | + mimeType: 'application/json', |
| 64 | + payTo: 'YOUR_PAYTO_ADDRESS', |
| 65 | + maxTimeoutSeconds: 60, |
| 66 | + // USDC contract address on Base. |
| 67 | + asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +</TabItem> |
| 72 | +<TabItem value = "src/type.ts"> |
| 73 | + |
| 74 | +```ts |
| 75 | +import { Address } from 'viem' |
| 76 | + |
| 77 | +export type PaymentRequirements = { |
| 78 | + scheme: string |
| 79 | + network: string |
| 80 | + maxAmountRequired: string |
| 81 | + resource: string |
| 82 | + description: string |
| 83 | + mimeType: string |
| 84 | + payTo: Address |
| 85 | + maxTimeoutSeconds: number |
| 86 | + asset: Address |
| 87 | +} |
| 88 | +``` |
| 89 | +
|
| 90 | +</TabItem> |
| 91 | +</Tabs> |
| 92 | +
|
| 93 | +### 2. Add a discovery method |
| 94 | +
|
| 95 | +Add a public `GET /info` route that publishes your protected paths and their payment terms. Buyers call this route before they prepare a ERC-7710 delegation payment. |
| 96 | +
|
| 97 | +```ts |
| 98 | +// src/index.ts |
| 99 | +import express from 'express' |
| 100 | +import { paymentRequirements } from './payment' |
| 101 | + |
| 102 | +const app = express() |
| 103 | +app.use(express.json()) |
| 104 | + |
| 105 | +app.get('/info', (_req, res) => { |
| 106 | + res.json({ |
| 107 | + routes: ['/api/premium'], |
| 108 | + accepts: [paymentRequirements], |
| 109 | + }) |
| 110 | +}) |
| 111 | +``` |
| 112 | + |
| 113 | +### 3. Add payment middleware |
| 114 | + |
| 115 | +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. |
| 116 | + |
| 117 | +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. |
| 118 | + |
| 119 | +```ts |
| 120 | +// src/middleware.ts |
| 121 | +import type { Request, Response, NextFunction } from 'express' |
| 122 | +import { paymentRequirements } from './payment' |
| 123 | + |
| 124 | +export function requirePayment(req: Request, res: Response, next: NextFunction) { |
| 125 | + const header = req.header('X-PAYMENT') |
| 126 | + |
| 127 | + if (!header) { |
| 128 | + return res.status(402).json({ |
| 129 | + x402Version: 1, |
| 130 | + accepts: [paymentRequirements], |
| 131 | + error: 'X-PAYMENT header missing', |
| 132 | + }) |
| 133 | + } |
| 134 | + |
| 135 | + try { |
| 136 | + const decoded = JSON.parse(Buffer.from(header, 'base64').toString('utf8')) |
| 137 | + res.locals.paymentPayload = decoded |
| 138 | + return next() |
| 139 | + } catch (error) { |
| 140 | + return res.status(402).json({ |
| 141 | + x402Version: 1, |
| 142 | + accepts: [paymentRequirements], |
| 143 | + error: 'X-PAYMENT header is not valid base64-encoded JSON', |
| 144 | + }) |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +### 4. Verify the payment |
| 150 | + |
| 151 | +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. |
| 152 | + |
| 153 | +<Tabs> |
| 154 | +<TabItem value = 'src/middleware.ts'> |
| 155 | + |
| 156 | +```ts |
| 157 | +// src/middleware.ts |
| 158 | +import { FACILITATOR_URL } from './config' |
| 159 | + |
| 160 | +export async function verifyPayment(req: Request, res: Response, next: NextFunction) { |
| 161 | + const response = await fetch(`${FACILITATOR_URL}/platform/v2/x402/verify`, { |
| 162 | + method: 'POST', |
| 163 | + headers: { 'Content-Type': 'application/json' }, |
| 164 | + body: JSON.stringify({ |
| 165 | + x402Version: 1, |
| 166 | + paymentPayload: res.locals.paymentPayload, |
| 167 | + paymentRequirements, |
| 168 | + }), |
| 169 | + }) |
| 170 | + |
| 171 | + const result = await response.json() |
| 172 | + |
| 173 | + if (!response.ok || result.isValid !== true) { |
| 174 | + return res.status(402).json({ |
| 175 | + x402Version: 1, |
| 176 | + accepts: [paymentRequirements], |
| 177 | + error: result.invalidReason ?? 'Verification failed', |
| 178 | + }) |
| 179 | + } |
| 180 | + |
| 181 | + return next() |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +</TabItem> |
| 186 | +<TabItem value = 'src/config.ts'> |
| 187 | + |
| 188 | +```ts |
| 189 | +const FACILITATOR_URL = 'https://tx-sentinel-base-mainnet.dev-api.cx.metamask.io' |
| 190 | +``` |
| 191 | + |
| 192 | +</TabItem> |
| 193 | +</Tabs> |
| 194 | + |
| 195 | +### 5. Settle the payment |
| 196 | + |
| 197 | +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. |
| 198 | + |
| 199 | +The example returns `{ message: 'Premium content unlocked' }` as a placeholder. Replace it with whatever JSON payload your protected route serves. |
| 200 | + |
| 201 | +```ts |
| 202 | +// src/index.ts |
| 203 | +import { requirePayment, verifyPayment } from './middleware' |
| 204 | +import { paymentRequirements } from './payment' |
| 205 | +import { FACILITATOR_URL } from './config' |
| 206 | + |
| 207 | +app.get('/api/premium', requirePayment, verifyPayment, async (_req, res) => { |
| 208 | + const settlement = await fetch(`${FACILITATOR_URL}/platform/v2/x402/settle`, { |
| 209 | + method: 'POST', |
| 210 | + headers: { 'Content-Type': 'application/json' }, |
| 211 | + body: JSON.stringify({ |
| 212 | + x402Version: 1, |
| 213 | + paymentPayload: res.locals.paymentPayload, |
| 214 | + paymentRequirements, |
| 215 | + }), |
| 216 | + }).then(r => r.json()) |
| 217 | + |
| 218 | + const encoded = Buffer.from(JSON.stringify(settlement)).toString('base64') |
| 219 | + res.setHeader('X-PAYMENT-RESPONSE', encoded) |
| 220 | + // Run your business logic and send as a response. |
| 221 | + res.json({ message: 'Premium content unlocked' }) |
| 222 | +}) |
| 223 | + |
| 224 | +;(app.listen(4402), |
| 225 | + () => { |
| 226 | + console.log('Seller listening on port 4402') |
| 227 | + }) |
| 228 | +``` |
0 commit comments