Skip to content

Commit 238e6b7

Browse files
add x402 seller guide
1 parent cd7d65d commit 238e6b7

2 files changed

Lines changed: 235 additions & 0 deletions

File tree

gator-sidebar.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ const sidebar = {
113113
'guides/advanced-permissions/create-redelegation',
114114
],
115115
},
116+
{
117+
type: 'category',
118+
label: 'x402',
119+
collapsed: true,
120+
key: 'x402-guides',
121+
items: ['guides/x402/seller'],
122+
},
116123
],
117124
},
118125
{
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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

Comments
 (0)