Skip to content

Commit e11684f

Browse files
committed
Implement x402 payments with TRAC token on NeuroWeb
1 parent ec7f478 commit e11684f

2 files changed

Lines changed: 243 additions & 32 deletions

File tree

src/lib/x402.ts

Lines changed: 228 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,42 @@
11
/**
22
* @file src/lib/x402.ts
3-
* @description X402 payment middleware for MCP server using Corbits and Faremeter.
3+
* @description X402 payment middleware for MCP server on NeuroWeb testnet.
4+
* Implements the x402 payment standard: https://x402.gitbook.io/x402
45
*/
56

6-
import { evm } from '@faremeter/info';
7-
import { express as faremeter } from '@faremeter/middleware';
87
import { type NextFunction, type Request, type Response } from 'express';
98
import { readConfig } from '../shared/config';
109

1110
export interface X402Config {
1211
host: string;
1312
port: number;
14-
amount?: number;
15-
asset?: 'USDC';
16-
network?: 'base';
13+
amount?: string; // Amount in TRAC tokens (with 18 decimals)
14+
asset?: string; // Token contract address
15+
networkId?: string; // Network chain identifier (e.g., otp:20430)
16+
networkName?: string; // Network display name (e.g., neuroweb-testnet)
1717
facilitatorURL?: string;
1818
description?: string;
1919
}
2020

21+
export interface X402PaymentInfo {
22+
maxAmountRequired: string;
23+
resource: string;
24+
description: string;
25+
payTo: string;
26+
asset: string;
27+
network: string;
28+
}
29+
30+
export interface X402PaymentHeader {
31+
scheme: string;
32+
amount: string;
33+
asset: string;
34+
network: string;
35+
nonce: string;
36+
signature: string;
37+
from: string;
38+
}
39+
2140
export const BYPASS_PAYMENT_METHODS = [
2241
'initialize',
2342
'initialized',
@@ -29,6 +48,137 @@ export const BYPASS_PAYMENT_METHODS = [
2948

3049
export const PAYWALLED_TOOLS = ['query', 'publish', 'lookup'] as const;
3150

51+
export const NEUROWEB_TESTNET_ID = 'otp:20430';
52+
export const NEUROWEB_TESTNET_NAME = 'neuroweb-testnet';
53+
export const TRAC_TOKEN_ADDRESS = '0xFfFFFFff00000000000000000000000000000001';
54+
export const TRAC_AMOUNT = '1000000000000000000'; // 1 TRAC with 18 decimals
55+
56+
/**
57+
* Parse the X-Payment header from the request
58+
*/
59+
function parsePaymentHeader(header: string): X402PaymentHeader | null {
60+
try {
61+
// Expected format: scheme amount=<amount> asset=<asset> network=<network> nonce=<nonce> signature=<signature> from=<from>
62+
const parts = header.split(' ');
63+
const scheme = parts[0];
64+
const params: Record<string, string> = {};
65+
66+
for (let i = 1; i < parts.length; i++) {
67+
const [key, value] = parts[i].split('=');
68+
if (key && value) {
69+
params[key] = value;
70+
}
71+
}
72+
73+
if (
74+
!params.amount ||
75+
!params.asset ||
76+
!params.network ||
77+
!params.nonce ||
78+
!params.signature ||
79+
!params.from
80+
) {
81+
return null;
82+
}
83+
84+
return {
85+
scheme,
86+
amount: params.amount,
87+
asset: params.asset,
88+
network: params.network,
89+
nonce: params.nonce,
90+
signature: params.signature,
91+
from: params.from,
92+
};
93+
} catch {
94+
return null;
95+
}
96+
}
97+
98+
/**
99+
* Verify payment with the facilitator
100+
*/
101+
async function verifyPayment(
102+
paymentHeader: X402PaymentHeader,
103+
facilitatorURL: string,
104+
expectedAmount: string,
105+
expectedAsset: string,
106+
expectedNetwork: string,
107+
payTo: string,
108+
): Promise<boolean> {
109+
try {
110+
const verifyResponse = await fetch(`${facilitatorURL}/verify`, {
111+
method: 'POST',
112+
headers: {
113+
'Content-Type': 'application/json',
114+
},
115+
body: JSON.stringify({
116+
scheme: paymentHeader.scheme,
117+
amount: paymentHeader.amount,
118+
asset: paymentHeader.asset,
119+
network: paymentHeader.network,
120+
nonce: paymentHeader.nonce,
121+
signature: paymentHeader.signature,
122+
from: paymentHeader.from,
123+
payTo,
124+
expectedAmount,
125+
expectedAsset,
126+
expectedNetwork,
127+
}),
128+
});
129+
130+
if (!verifyResponse.ok) {
131+
console.error('Payment verification failed:', await verifyResponse.text());
132+
return false;
133+
}
134+
135+
const verifyResult = await verifyResponse.json();
136+
return verifyResult.valid === true;
137+
} catch (error) {
138+
console.error('Error verifying payment:', error);
139+
return false;
140+
}
141+
}
142+
143+
/**
144+
* Settle payment with the facilitator
145+
*/
146+
async function settlePayment(
147+
paymentHeader: X402PaymentHeader,
148+
facilitatorURL: string,
149+
payTo: string,
150+
): Promise<boolean> {
151+
try {
152+
const settleResponse = await fetch(`${facilitatorURL}/settle`, {
153+
method: 'POST',
154+
headers: {
155+
'Content-Type': 'application/json',
156+
},
157+
body: JSON.stringify({
158+
scheme: paymentHeader.scheme,
159+
amount: paymentHeader.amount,
160+
asset: paymentHeader.asset,
161+
network: paymentHeader.network,
162+
nonce: paymentHeader.nonce,
163+
signature: paymentHeader.signature,
164+
from: paymentHeader.from,
165+
payTo,
166+
}),
167+
});
168+
169+
if (!settleResponse.ok) {
170+
console.error('Payment settlement failed:', await settleResponse.text());
171+
return false;
172+
}
173+
174+
const settleResult = await settleResponse.json();
175+
return settleResult.settled === true;
176+
} catch (error) {
177+
console.error('Error settling payment:', error);
178+
return false;
179+
}
180+
}
181+
32182
/**
33183
* Create MCP x402 payment middleware that only requires payment for specific tools.
34184
* @returns Express middleware function
@@ -46,37 +196,90 @@ export const initializePaymentMiddleware = async (
46196
}
47197

48198
const baseUrl = `http://${config.host}:${config.port}/mcp`;
199+
const facilitatorURL = config.facilitatorURL ?? 'https://api.cdp.coinbase.com/platform/v2/x402';
200+
const amount = config.amount ?? TRAC_AMOUNT;
201+
const asset = config.asset ?? TRAC_TOKEN_ADDRESS;
202+
const networkId = config.networkId ?? NEUROWEB_TESTNET_ID;
49203

50-
const paywalledMiddleware = await faremeter.createMiddleware({
51-
facilitatorURL: config.facilitatorURL ?? 'https://facilitator.corbits.dev',
52-
accepts: [
53-
{
54-
...evm.x402Exact({
55-
network: config.network ?? 'base',
56-
asset: config.asset ?? 'USDC',
57-
amount: config.amount ?? 10000, // $0.01 per request
58-
payTo: walletAddress,
59-
}),
60-
resource: baseUrl,
61-
description: config.description ?? 'GWALN MCP tools',
62-
},
63-
],
64-
});
65-
66-
return (req: Request, res: Response, next: NextFunction): void => {
204+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
67205
if (req.body?.method && BYPASS_PAYMENT_METHODS.includes(req.body.method)) {
68206
next();
69207
return;
70208
}
71209

210+
let isPaywalled = false;
211+
let toolDescription = config.description ?? 'GWALN MCP tool access';
212+
72213
if (req.body?.method === 'tools/call') {
73214
const toolName = req.body?.params?.name;
74215
if (toolName && PAYWALLED_TOOLS.includes(toolName)) {
75-
paywalledMiddleware!(req, res, next);
76-
return;
216+
isPaywalled = true;
217+
toolDescription = `Access to ${toolName} tool requires payment`;
77218
}
78219
}
79220

221+
if (!isPaywalled) {
222+
next();
223+
return;
224+
}
225+
226+
const paymentHeaderValue = req.headers['x-payment'] as string | undefined;
227+
228+
if (!paymentHeaderValue) {
229+
const paymentInfo: X402PaymentInfo = {
230+
maxAmountRequired: amount,
231+
resource: baseUrl,
232+
description: toolDescription,
233+
payTo: walletAddress,
234+
asset,
235+
network: networkId,
236+
};
237+
238+
res.status(402).json(paymentInfo);
239+
return;
240+
}
241+
242+
const paymentHeader = parsePaymentHeader(paymentHeaderValue);
243+
if (!paymentHeader) {
244+
res.status(400).json({
245+
error: 'Invalid payment header format',
246+
});
247+
return;
248+
}
249+
250+
const isVerified = await verifyPayment(
251+
paymentHeader,
252+
facilitatorURL,
253+
amount,
254+
asset,
255+
networkId,
256+
walletAddress,
257+
);
258+
259+
if (!isVerified) {
260+
res.status(402).json({
261+
error: 'Payment verification failed',
262+
paymentInfo: {
263+
maxAmountRequired: amount,
264+
resource: baseUrl,
265+
description: toolDescription,
266+
payTo: walletAddress,
267+
asset,
268+
network: networkId,
269+
},
270+
});
271+
return;
272+
}
273+
274+
const isSettled = await settlePayment(paymentHeader, facilitatorURL, walletAddress);
275+
276+
if (!isSettled) {
277+
res.status(500).json({
278+
error: 'Payment settlement failed',
279+
});
280+
return;
281+
}
282+
80283
next();
81284
};
82285
};

src/mcp-server.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
88
import express, { type Request, type Response } from 'express';
99
import { randomUUID } from 'node:crypto';
1010
import pkg from '../package.json';
11-
import { initializePaymentMiddleware, PAYWALLED_TOOLS } from './lib/x402';
11+
import {
12+
initializePaymentMiddleware,
13+
NEUROWEB_TESTNET_ID,
14+
NEUROWEB_TESTNET_NAME,
15+
PAYWALLED_TOOLS,
16+
TRAC_AMOUNT,
17+
TRAC_TOKEN_ADDRESS,
18+
} from './lib/x402';
1219
import { analyzeHandler, analyzeTool } from './tools/analyze';
1320
import { fetchHandler, fetchTool } from './tools/fetch';
1421
import { lookupHandler, lookupTool } from './tools/lookup';
@@ -135,14 +142,13 @@ const handleMcpRequest = async (req: Request, res: Response): Promise<void> => {
135142
}
136143
};
137144

138-
const network = 'base';
139-
140145
initializePaymentMiddleware({
141146
host: HOST,
142147
port: PORT,
143-
amount: 10000, // $0.01 per request
144-
asset: 'USDC',
145-
network,
148+
amount: TRAC_AMOUNT,
149+
asset: TRAC_TOKEN_ADDRESS,
150+
networkId: NEUROWEB_TESTNET_ID,
151+
networkName: NEUROWEB_TESTNET_NAME,
146152
description: 'GWALN MCP tools',
147153
})
148154
.then((paymentMiddleware) => {
@@ -155,8 +161,10 @@ initializePaymentMiddleware({
155161
console.log(`[mcp] Listening on ${url}`);
156162
console.log('[mcp] Use this URL in your MCP client configuration.');
157163
console.log(
158-
`[mcp] X402 MCP paywall enabled. Tools that require payment on network ${network}: ${PAYWALLED_TOOLS.join(', ')}.`,
164+
`[mcp] X402 MCP paywall enabled on ${NEUROWEB_TESTNET_NAME} (${NEUROWEB_TESTNET_ID}).`,
159165
);
166+
console.log(`[mcp] Payment required: 1 TRAC token per request.`);
167+
console.log(`[mcp] Paywalled tools: ${PAYWALLED_TOOLS.join(', ')}.`);
160168
})
161169
.on('error', (error) => {
162170
console.error('[mcp] Failed to start server:', error);

0 commit comments

Comments
 (0)