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' ;
87import { type NextFunction , type Request , type Response } from 'express' ;
98import { readConfig } from '../shared/config' ;
109
1110export 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+
2140export const BYPASS_PAYMENT_METHODS = [
2241 'initialize' ,
2342 'initialized' ,
@@ -29,6 +48,137 @@ export const BYPASS_PAYMENT_METHODS = [
2948
3049export 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} ;
0 commit comments