@aibtc/mcp-server 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,16 @@ const TOKEN_DECIMALS = {
6
6
  sBTC: 8,
7
7
  USDCx: 6,
8
8
  };
9
+ // Pricing tier amounts (matches x402-api and stx402 patterns)
10
+ const TIER_AMOUNTS = {
11
+ free: { STX: "0", sBTC: "0", USDCx: "0" },
12
+ simple: { STX: "0.001", sBTC: "0.000001", USDCx: "0.001" },
13
+ standard: { STX: "0.001", sBTC: "0.000001", USDCx: "0.001" },
14
+ ai: { STX: "0.003", sBTC: "0.000003", USDCx: "0.003" },
15
+ heavy_ai: { STX: "0.01", sBTC: "0.00001", USDCx: "0.01" },
16
+ storage_read: { STX: "0.0005", sBTC: "0.0000005", USDCx: "0.0005" },
17
+ storage_write: { STX: "0.001", sBTC: "0.000001", USDCx: "0.001" },
18
+ };
9
19
  /**
10
20
  * Convert human-readable amount to smallest unit (microSTX, sats, etc.)
11
21
  */
@@ -15,27 +25,35 @@ function toSmallestUnit(amount, tokenType) {
15
25
  const paddedFraction = fraction.padEnd(decimals, "0").slice(0, decimals);
16
26
  return BigInt(whole + paddedFraction).toString();
17
27
  }
28
+ /**
29
+ * Get amount for endpoint based on tier or explicit amount
30
+ */
31
+ function getEndpointAmount(ep) {
32
+ if (ep.tier) {
33
+ return TIER_AMOUNTS[ep.tier][ep.tokenType];
34
+ }
35
+ return ep.amount || TIER_AMOUNTS.standard[ep.tokenType];
36
+ }
18
37
  /**
19
38
  * Generate Hono route code for each endpoint
20
39
  */
21
40
  function generateEndpointCode(endpoints) {
22
41
  return endpoints
23
42
  .map((ep) => {
24
- const amountSmallest = toSmallestUnit(ep.amount, ep.tokenType);
43
+ const amount = getEndpointAmount(ep);
44
+ const amountSmallest = toSmallestUnit(amount, ep.tokenType);
45
+ const tierComment = ep.tier ? ` (tier: ${ep.tier})` : "";
25
46
  // Generate real example logic based on endpoint characteristics
26
47
  const exampleLogic = generateExampleLogic(ep);
27
48
  return `
28
- // ${ep.description}
49
+ // ${ep.description}${tierComment}
29
50
  app.${ep.method.toLowerCase()}('${ep.path}',
30
51
  x402Middleware({
31
52
  amount: '${amountSmallest}',
32
- address: env.RECIPIENT_ADDRESS,
33
- network: env.NETWORK as 'mainnet' | 'testnet',
34
53
  tokenType: '${ep.tokenType}',
35
- facilitatorUrl: env.FACILITATOR_URL,
36
54
  }),
37
55
  async (c) => {
38
- const payment = c.get('payment');
56
+ const payment = c.get('x402');
39
57
  ${exampleLogic}
40
58
  }
41
59
  );`;
@@ -62,9 +80,8 @@ function generateExampleLogic(ep) {
62
80
  success: true,
63
81
  data: result,
64
82
  payment: {
65
- txId: payment?.txId,
66
- sender: payment?.sender,
67
- amount: payment?.amount?.toString(),
83
+ txId: payment?.settleResult?.txId,
84
+ sender: payment?.payerAddress,
68
85
  },
69
86
  });`;
70
87
  }
@@ -81,9 +98,8 @@ function generateExampleLogic(ep) {
81
98
  success: true,
82
99
  data,
83
100
  payment: {
84
- txId: payment?.txId,
85
- sender: payment?.sender,
86
- amount: payment?.amount?.toString(),
101
+ txId: payment?.settleResult?.txId,
102
+ sender: payment?.payerAddress,
87
103
  },
88
104
  });`;
89
105
  }
@@ -93,9 +109,11 @@ function generateExampleLogic(ep) {
93
109
  function generateEndpointDocs(endpoints) {
94
110
  return endpoints
95
111
  .map((ep) => {
112
+ const amount = getEndpointAmount(ep);
113
+ const tierInfo = ep.tier ? ` (tier: ${ep.tier})` : "";
96
114
  return `### ${ep.method} ${ep.path}
97
115
  - **Description:** ${ep.description}
98
- - **Cost:** ${ep.amount} ${ep.tokenType}
116
+ - **Cost:** ${amount} ${ep.tokenType}${tierInfo}
99
117
  - **Payment Required:** Yes`;
100
118
  })
101
119
  .join("\n\n");
@@ -112,247 +130,382 @@ function generateTokenList(endpoints) {
112
130
  // =============================================================================
113
131
  function getIndexTemplate(endpoints) {
114
132
  const endpointCode = generateEndpointCode(endpoints);
115
- return `import { Hono } from 'hono';
133
+ return `// BigInt.toJSON polyfill for JSON.stringify compatibility
134
+ (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () {
135
+ return this.toString();
136
+ };
137
+
138
+ import { Hono } from 'hono';
116
139
  import { cors } from 'hono/cors';
117
140
  import { x402Middleware } from './x402-middleware';
141
+ import type { X402Context } from './x402-middleware';
118
142
 
119
- type Bindings = {
143
+ type Env = {
120
144
  RECIPIENT_ADDRESS: string;
121
145
  NETWORK: string;
122
146
  FACILITATOR_URL: string;
123
147
  };
124
148
 
125
149
  type Variables = {
126
- payment?: {
127
- txId: string;
128
- status: string;
129
- sender: string;
130
- recipient: string;
131
- amount: bigint;
132
- };
150
+ x402?: X402Context;
133
151
  };
134
152
 
135
- const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
153
+ const app = new Hono<{ Bindings: Env; Variables: Variables }>();
136
154
 
137
- app.use('*', cors());
155
+ // CORS middleware with x402 headers
156
+ app.use('*', cors({
157
+ origin: '*',
158
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
159
+ allowHeaders: ['X-PAYMENT', 'X-PAYMENT-TOKEN-TYPE', 'Authorization', 'Content-Type'],
160
+ exposeHeaders: ['X-PAYMENT-RESPONSE', 'X-PAYER-ADDRESS'],
161
+ }));
138
162
 
139
163
  // Startup validation - fail fast if required secrets are missing
140
164
  app.use('*', async (c, next) => {
165
+ // Skip validation for health check
166
+ if (c.req.path === '/health') {
167
+ return next();
168
+ }
169
+
141
170
  const missingSecrets: string[] = [];
142
171
 
143
172
  if (!c.env.RECIPIENT_ADDRESS) {
144
173
  missingSecrets.push('RECIPIENT_ADDRESS');
145
174
  }
146
- if (!c.env.FACILITATOR_URL) {
147
- missingSecrets.push('FACILITATOR_URL');
148
- }
149
175
 
150
176
  if (missingSecrets.length > 0) {
151
177
  return c.json({
152
178
  error: 'Server configuration error',
153
179
  message: \`Missing required secrets: \${missingSecrets.join(', ')}\`,
154
- hint: missingSecrets.map(s => \`Run: npm run wrangler -- secret put \${s}\`).join(' && '),
180
+ hint: missingSecrets.map(s => \`Run: wrangler secret put \${s}\`).join(' && '),
155
181
  }, 503);
156
182
  }
157
183
 
158
184
  await next();
159
185
  });
160
186
 
187
+ // Service info at root (free)
188
+ app.get('/', (c) => {
189
+ return c.json({
190
+ service: '${endpoints.length > 0 ? "x402-api" : "my-x402-api"}',
191
+ version: '1.0.0',
192
+ health: '/health',
193
+ payment: {
194
+ tokens: ['STX', 'sBTC', 'USDCx'],
195
+ header: 'X-PAYMENT',
196
+ tokenTypeHeader: 'X-PAYMENT-TOKEN-TYPE',
197
+ },
198
+ });
199
+ });
200
+
161
201
  // Health check (free)
162
202
  app.get('/health', (c) => {
163
203
  return c.json({
164
204
  status: 'ok',
165
205
  timestamp: new Date().toISOString(),
206
+ network: c.env.NETWORK || 'testnet',
166
207
  });
167
208
  });
168
-
169
- // x402-protected endpoints
170
- app.use('*', async (c, next) => {
171
- // Make env available to middleware
172
- const env = c.env;
173
- (globalThis as Record<string, unknown>).__env = env;
174
- await next();
175
- });
176
-
177
- const env = {
178
- get RECIPIENT_ADDRESS() {
179
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.RECIPIENT_ADDRESS || '';
180
- },
181
- get NETWORK() {
182
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.NETWORK || 'testnet';
183
- },
184
- get FACILITATOR_URL() {
185
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.FACILITATOR_URL || '';
186
- },
187
- };
188
209
  ${endpointCode}
189
210
 
190
211
  export default app;
191
212
  `;
192
213
  }
193
214
  function getMiddlewareTemplate() {
194
- return `import type { Context, Next } from 'hono';
215
+ return `/**
216
+ * x402 Payment Middleware for Hono
217
+ *
218
+ * Based on production implementations from:
219
+ * - https://github.com/aibtcdev/x402-api
220
+ * - https://github.com/whoabuddy/stx402
221
+ *
222
+ * Uses the x402-stacks library for payment verification.
223
+ */
224
+
225
+ import type { Context, Next } from 'hono';
226
+ import { X402PaymentVerifier } from 'x402-stacks';
227
+
228
+ // =============================================================================
229
+ // Types
230
+ // =============================================================================
231
+
232
+ export type TokenType = 'STX' | 'sBTC' | 'USDCx';
233
+
234
+ export interface TokenContract {
235
+ address: string;
236
+ name: string;
237
+ }
195
238
 
196
239
  export interface X402Config {
197
240
  amount: string;
198
- address: string;
199
- network: 'mainnet' | 'testnet';
200
- tokenType: 'STX' | 'sBTC' | 'USDCx';
201
- facilitatorUrl?: string;
202
- resource?: string;
241
+ tokenType: TokenType;
203
242
  }
204
243
 
205
- interface TokenContract {
206
- address: string;
207
- name: string;
244
+ export interface SettleResult {
245
+ isValid: boolean;
246
+ txId?: string;
247
+ status?: string;
248
+ sender?: string;
249
+ senderAddress?: string;
250
+ sender_address?: string;
251
+ recipient?: string;
252
+ error?: string;
253
+ reason?: string;
254
+ validationError?: string;
208
255
  }
209
256
 
210
- // Token contract addresses for payment verification
211
- const TOKEN_CONTRACTS: Record<string, Record<string, TokenContract | null>> = {
212
- mainnet: {
213
- STX: null,
214
- sBTC: { address: 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', name: 'sbtc-token' },
215
- USDCx: { address: 'SP2XD7417HGPRTREMKF748VNEQPDRR0RMANB7X1NK', name: 'token-usdcx' },
216
- },
217
- testnet: {
218
- STX: null,
219
- sBTC: { address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM', name: 'sbtc-token' },
220
- USDCx: null,
221
- },
222
- };
257
+ export interface X402Context {
258
+ payerAddress: string;
259
+ settleResult: SettleResult;
260
+ signedTx: string;
261
+ }
223
262
 
224
263
  interface PaymentRequirement {
225
264
  maxAmountRequired: string;
226
265
  resource: string;
227
266
  payTo: string;
228
- network: string;
267
+ network: 'mainnet' | 'testnet';
229
268
  nonce: string;
230
269
  expiresAt: string;
231
- tokenType: string;
270
+ tokenType: TokenType;
232
271
  tokenContract?: TokenContract;
233
272
  }
234
273
 
235
- interface SettleRequest {
236
- signed_transaction: string;
237
- expected_recipient: string;
238
- min_amount: string;
239
- network: string;
240
- token_type: string;
274
+ type PaymentErrorCode =
275
+ | 'FACILITATOR_UNAVAILABLE'
276
+ | 'FACILITATOR_ERROR'
277
+ | 'PAYMENT_INVALID'
278
+ | 'INSUFFICIENT_FUNDS'
279
+ | 'PAYMENT_EXPIRED'
280
+ | 'AMOUNT_TOO_LOW'
281
+ | 'NETWORK_ERROR'
282
+ | 'UNKNOWN_ERROR';
283
+
284
+ interface PaymentErrorResponse {
285
+ error: string;
286
+ code: PaymentErrorCode;
287
+ retryAfter?: number;
288
+ tokenType: TokenType;
241
289
  resource: string;
242
- method: string;
290
+ details?: Record<string, string | undefined>;
243
291
  }
244
292
 
245
- interface SettleResponse {
246
- success: boolean;
247
- tx_id?: string;
248
- status?: string;
249
- sender_address?: string;
250
- recipient_address?: string;
251
- amount?: number;
252
- error?: string;
293
+ // =============================================================================
294
+ // Token Contracts (correct mainnet/testnet addresses)
295
+ // =============================================================================
296
+
297
+ const TOKEN_CONTRACTS: Record<'mainnet' | 'testnet', Record<'sBTC' | 'USDCx', TokenContract>> = {
298
+ mainnet: {
299
+ sBTC: { address: 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', name: 'sbtc-token' },
300
+ USDCx: { address: 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE', name: 'usdcx' },
301
+ },
302
+ testnet: {
303
+ sBTC: { address: 'ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT', name: 'sbtc-token' },
304
+ USDCx: { address: 'ST1NXBK3K5YYMD6FD41MVNP3JS1GABZ8TRVX023PT', name: 'usdcx' },
305
+ },
306
+ };
307
+
308
+ // =============================================================================
309
+ // Error Classification
310
+ // =============================================================================
311
+
312
+ function classifyPaymentError(error: unknown, settleResult?: SettleResult): {
313
+ code: PaymentErrorCode;
314
+ message: string;
315
+ httpStatus: number;
316
+ retryAfter?: number;
317
+ } {
318
+ const errorStr = String(error).toLowerCase();
319
+ const resultError = settleResult?.error?.toLowerCase() || '';
320
+ const resultReason = settleResult?.reason?.toLowerCase() || '';
321
+ const validationError = settleResult?.validationError?.toLowerCase() || '';
322
+ const combined = \`\${errorStr} \${resultError} \${resultReason} \${validationError}\`;
323
+
324
+ if (combined.includes('fetch') || combined.includes('network') || combined.includes('timeout')) {
325
+ return { code: 'NETWORK_ERROR', message: 'Network error with payment facilitator', httpStatus: 502, retryAfter: 5 };
326
+ }
327
+
328
+ if (combined.includes('503') || combined.includes('unavailable')) {
329
+ return { code: 'FACILITATOR_UNAVAILABLE', message: 'Payment facilitator temporarily unavailable', httpStatus: 503, retryAfter: 30 };
330
+ }
331
+
332
+ if (combined.includes('insufficient') || combined.includes('balance')) {
333
+ return { code: 'INSUFFICIENT_FUNDS', message: 'Insufficient funds in wallet', httpStatus: 402 };
334
+ }
335
+
336
+ if (combined.includes('expired') || combined.includes('nonce')) {
337
+ return { code: 'PAYMENT_EXPIRED', message: 'Payment expired, please sign a new payment', httpStatus: 402 };
338
+ }
339
+
340
+ if (combined.includes('amount') && (combined.includes('low') || combined.includes('minimum'))) {
341
+ return { code: 'AMOUNT_TOO_LOW', message: 'Payment amount below minimum required', httpStatus: 402 };
342
+ }
343
+
344
+ if (combined.includes('invalid') || combined.includes('signature')) {
345
+ return { code: 'PAYMENT_INVALID', message: 'Invalid payment signature', httpStatus: 400 };
346
+ }
347
+
348
+ return { code: 'UNKNOWN_ERROR', message: 'Payment processing error', httpStatus: 500, retryAfter: 5 };
253
349
  }
254
350
 
351
+ // =============================================================================
352
+ // Middleware
353
+ // =============================================================================
354
+
355
+ type Env = {
356
+ RECIPIENT_ADDRESS: string;
357
+ NETWORK: string;
358
+ FACILITATOR_URL: string;
359
+ };
360
+
255
361
  /**
256
- * x402 Payment Middleware for Hono
362
+ * x402 Payment Middleware
257
363
  *
258
364
  * Handles the x402 payment flow:
259
365
  * 1. If no X-PAYMENT header, return 402 with payment requirements
260
366
  * 2. If X-PAYMENT header present, verify payment via facilitator
261
- * 3. On success, attach payment info and continue to handler
367
+ * 3. On success, attach payment context and continue to handler
262
368
  */
263
369
  export function x402Middleware(config: X402Config) {
264
- const facilitatorUrl = config.facilitatorUrl || 'https://facilitator.x402stacks.xyz';
265
- const tokenContract = TOKEN_CONTRACTS[config.network]?.[config.tokenType] || null;
266
-
267
- return async (c: Context, next: Next) => {
268
- const signedPayment = c.req.header('x-payment');
370
+ return async (c: Context<{ Bindings: Env; Variables: { x402?: X402Context } }>, next: Next) => {
371
+ const env = c.env;
372
+ const network = (env.NETWORK || 'testnet') as 'mainnet' | 'testnet';
373
+ const facilitatorUrl = env.FACILITATOR_URL || 'https://facilitator.x402stacks.xyz';
374
+ const recipientAddress = env.RECIPIENT_ADDRESS;
375
+
376
+ // Get token type from header or use config default
377
+ const headerTokenType = c.req.header('X-PAYMENT-TOKEN-TYPE');
378
+ const tokenType = (headerTokenType?.toUpperCase() === 'SBTC' ? 'sBTC' :
379
+ headerTokenType?.toUpperCase() === 'USDCX' ? 'USDCx' :
380
+ headerTokenType?.toUpperCase() === 'STX' ? 'STX' :
381
+ config.tokenType) as TokenType;
382
+
383
+ const minAmount = BigInt(config.amount);
384
+ const signedTx = c.req.header('X-PAYMENT');
385
+
386
+ if (!signedTx) {
387
+ // Return 402 Payment Required
388
+ let tokenContract: TokenContract | undefined;
389
+ if (tokenType === 'sBTC' || tokenType === 'USDCx') {
390
+ tokenContract = TOKEN_CONTRACTS[network][tokenType];
391
+ }
269
392
 
270
- if (!signedPayment) {
271
- // Return 402 Payment Required with payment details
272
393
  const paymentReq: PaymentRequirement = {
273
394
  maxAmountRequired: config.amount,
274
- resource: config.resource || c.req.path,
275
- payTo: config.address,
276
- network: config.network,
395
+ resource: c.req.path,
396
+ payTo: recipientAddress,
397
+ network,
277
398
  nonce: crypto.randomUUID(),
278
- expiresAt: new Date(Date.now() + 300000).toISOString(),
279
- tokenType: config.tokenType,
399
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
400
+ tokenType,
401
+ ...(tokenContract && { tokenContract }),
280
402
  };
281
403
 
282
- if (tokenContract) {
283
- paymentReq.tokenContract = tokenContract;
284
- }
285
-
286
404
  return c.json(paymentReq, 402);
287
405
  }
288
406
 
289
- // Verify and settle payment via facilitator
407
+ // Verify payment via x402-stacks
408
+ const verifier = new X402PaymentVerifier(facilitatorUrl, network);
409
+
410
+ let settleResult: SettleResult;
290
411
  try {
291
- const settleRequest: SettleRequest = {
292
- signed_transaction: signedPayment,
293
- expected_recipient: config.address,
294
- min_amount: config.amount,
295
- network: config.network,
296
- token_type: config.tokenType.toUpperCase(),
297
- resource: config.resource || c.req.path,
298
- method: c.req.method,
412
+ settleResult = await verifier.settlePayment(signedTx, {
413
+ expectedRecipient: recipientAddress,
414
+ minAmount,
415
+ tokenType,
416
+ }) as SettleResult;
417
+ } catch (error) {
418
+ const classified = classifyPaymentError(error);
419
+
420
+ const errorResponse: PaymentErrorResponse = {
421
+ error: classified.message,
422
+ code: classified.code,
423
+ retryAfter: classified.retryAfter,
424
+ tokenType,
425
+ resource: c.req.path,
426
+ details: { exceptionMessage: String(error) },
299
427
  };
300
428
 
301
- const response = await fetch(\`\${facilitatorUrl}/api/v1/settle\`, {
302
- method: 'POST',
303
- headers: { 'Content-Type': 'application/json' },
304
- body: JSON.stringify(settleRequest),
305
- });
306
-
307
- const result = (await response.json()) as SettleResponse;
308
-
309
- if (!result.success) {
310
- return c.json(
311
- {
312
- error: 'Payment verification failed',
313
- reason: result.error || 'Unknown error',
314
- },
315
- 402
316
- );
429
+ if (classified.retryAfter) {
430
+ c.header('Retry-After', String(classified.retryAfter));
317
431
  }
318
432
 
319
- // Store payment info in context for handler to use
320
- c.set('payment', {
321
- txId: result.tx_id,
322
- status: result.status,
323
- sender: result.sender_address,
324
- recipient: result.recipient_address,
325
- amount: BigInt(result.amount || 0),
326
- });
433
+ return c.json(errorResponse, classified.httpStatus as 400 | 402 | 500 | 502 | 503);
434
+ }
327
435
 
328
- await next();
329
- } catch (error) {
330
- return c.json(
331
- {
332
- error: 'Payment processing error',
333
- message: error instanceof Error ? error.message : 'Unknown error',
334
- },
335
- 500
436
+ if (!settleResult.isValid) {
437
+ const classified = classifyPaymentError(
438
+ settleResult.validationError || settleResult.error || 'invalid',
439
+ settleResult
336
440
  );
441
+
442
+ const errorResponse: PaymentErrorResponse = {
443
+ error: classified.message,
444
+ code: classified.code,
445
+ retryAfter: classified.retryAfter,
446
+ tokenType,
447
+ resource: c.req.path,
448
+ details: {
449
+ settleError: settleResult.error,
450
+ settleReason: settleResult.reason,
451
+ validationError: settleResult.validationError,
452
+ },
453
+ };
454
+
455
+ if (classified.retryAfter) {
456
+ c.header('Retry-After', String(classified.retryAfter));
457
+ }
458
+
459
+ return c.json(errorResponse, classified.httpStatus as 400 | 402 | 500 | 502 | 503);
337
460
  }
461
+
462
+ // Extract payer address
463
+ const payerAddress = settleResult.senderAddress || settleResult.sender_address || settleResult.sender || 'unknown';
464
+
465
+ // Store context for downstream handlers
466
+ c.set('x402', {
467
+ payerAddress,
468
+ settleResult,
469
+ signedTx,
470
+ });
471
+
472
+ // Add response headers
473
+ c.header('X-PAYMENT-RESPONSE', JSON.stringify(settleResult));
474
+ c.header('X-PAYER-ADDRESS', payerAddress);
475
+
476
+ await next();
338
477
  };
339
478
  }
340
479
  `;
341
480
  }
342
481
  function getWranglerTemplate(projectName, network, facilitatorUrl) {
482
+ const mainnetFacilitator = "https://facilitator.x402stacks.xyz";
343
483
  return `{
484
+ "$schema": "node_modules/wrangler/config-schema.json",
344
485
  "name": "${projectName}",
345
486
  "main": "src/index.ts",
346
487
  "compatibility_date": "2026-01-14",
347
- "compatibility_flags": ["nodejs_compat"],
488
+ "compatibility_flags": ["nodejs_compat_v2"],
489
+ "workers_dev": true,
348
490
  "vars": {
349
491
  "NETWORK": "${network}",
350
492
  "FACILITATOR_URL": "${facilitatorUrl}"
351
493
  },
352
494
  "env": {
495
+ "staging": {
496
+ "name": "${projectName}-staging",
497
+ "workers_dev": true,
498
+ "vars": {
499
+ "NETWORK": "testnet",
500
+ "FACILITATOR_URL": "${facilitatorUrl}"
501
+ }
502
+ },
353
503
  "production": {
504
+ "name": "${projectName}",
505
+ "workers_dev": false,
354
506
  "vars": {
355
- "NETWORK": "mainnet"
507
+ "NETWORK": "mainnet",
508
+ "FACILITATOR_URL": "${mainnetFacilitator}"
356
509
  }
357
510
  }
358
511
  }
@@ -366,20 +519,21 @@ function getPackageJsonTemplate(projectName) {
366
519
  "private": true,
367
520
  "type": "module",
368
521
  "scripts": {
369
- "wrangler": "set -a && . ./.env && set +a && wrangler",
370
- "dev": "npm run wrangler -- dev",
371
- "deploy": "npm run wrangler -- deploy",
372
- "deploy:dry": "npm run wrangler -- deploy --dry-run",
373
- "deploy:production": "npm run wrangler -- deploy --env production",
374
- "tail": "npm run wrangler -- tail"
522
+ "dev": "wrangler dev",
523
+ "deploy": "wrangler deploy",
524
+ "deploy:staging": "wrangler deploy --env staging",
525
+ "deploy:production": "wrangler deploy --env production",
526
+ "tail": "wrangler tail",
527
+ "cf-typegen": "wrangler types"
375
528
  },
376
529
  "dependencies": {
377
- "hono": "^4.7.0"
530
+ "hono": "^4.7.0",
531
+ "x402-stacks": "^1.1.1"
378
532
  },
379
533
  "devDependencies": {
380
534
  "@cloudflare/workers-types": "^4.20250109.0",
381
535
  "typescript": "^5.7.0",
382
- "wrangler": "^4.5.0"
536
+ "wrangler": "^4.56.0"
383
537
  }
384
538
  }
385
539
  `;
@@ -405,13 +559,26 @@ function getTsconfigTemplate() {
405
559
  `;
406
560
  }
407
561
  function getEnvExampleTemplate(recipientAddress) {
408
- return `# Cloudflare credentials
562
+ const addressNote = recipientAddress
563
+ ? `# Value: ${recipientAddress}`
564
+ : "# Value: Your Stacks address (SP... for mainnet, ST... for testnet)";
565
+ return `# Cloudflare credentials (only needed for CI/CD deployment)
566
+ # For local dev, wrangler uses browser-based auth
409
567
  CLOUDFLARE_API_TOKEN=your-api-token-here
410
568
  CLOUDFLARE_ACCOUNT_ID=your-account-id-here
411
569
 
412
570
  # x402 recipient address (set via wrangler secret)
413
571
  # wrangler secret put RECIPIENT_ADDRESS
414
- # Value: ${recipientAddress}
572
+ ${addressNote}
573
+ `;
574
+ }
575
+ function getDevVarsTemplate(recipientAddress) {
576
+ const address = recipientAddress || "YOUR_STACKS_ADDRESS_HERE";
577
+ return `# Local development variables
578
+ # These are used by wrangler dev and are NOT deployed to production
579
+ # For production secrets, use: wrangler secret put RECIPIENT_ADDRESS
580
+
581
+ RECIPIENT_ADDRESS=${address}
415
582
  `;
416
583
  }
417
584
  function getGitignoreTemplate() {
@@ -425,66 +592,64 @@ dist/
425
592
  function getReadmeTemplate(projectName, endpoints, recipientAddress) {
426
593
  const tokenList = generateTokenList(endpoints);
427
594
  const endpointDocs = generateEndpointDocs(endpoints);
595
+ const addressDisplay = recipientAddress || "YOUR_STACKS_ADDRESS";
428
596
  return `# ${projectName}
429
597
 
430
598
  x402-enabled API endpoints on Cloudflare Workers.
431
599
 
600
+ Built using patterns from:
601
+ - [x402-api](https://github.com/aibtcdev/x402-api)
602
+ - [stx402](https://github.com/whoabuddy/stx402)
603
+
604
+ ## Quick Start
605
+
606
+ \`\`\`bash
607
+ # Install dependencies
608
+ npm install
609
+
610
+ # Set your recipient address for local dev
611
+ # Edit .dev.vars and replace YOUR_STACKS_ADDRESS_HERE with your address
612
+
613
+ # Start local dev server
614
+ npm run dev
615
+ \`\`\`
616
+
617
+ The server will start at http://localhost:8787
618
+
432
619
  ## Payment Tokens
433
620
 
434
621
  This API accepts payments in:
435
622
  ${tokenList}
436
623
 
437
- ## Recipient Address
438
-
439
- Payments are sent to: \`${recipientAddress}\`
440
-
441
624
  ## Endpoints
442
625
 
626
+ ### GET /
627
+ - **Description:** Service info
628
+ - **Cost:** Free
629
+
443
630
  ### GET /health
444
631
  - **Description:** Health check endpoint
445
632
  - **Cost:** Free
446
- - **Payment Required:** No
447
633
 
448
634
  ${endpointDocs}
449
635
 
450
- ## Setup
451
-
452
- 1. Install dependencies:
453
- \`\`\`bash
454
- npm install
455
- \`\`\`
456
-
457
- 2. Create \`.env\` file from \`.env.example\`:
458
- \`\`\`bash
459
- cp .env.example .env
460
- \`\`\`
636
+ ## Deployment
461
637
 
462
- 3. Add your Cloudflare credentials to \`.env\`
463
-
464
- 4. Set the recipient address as a secret:
465
- \`\`\`bash
466
- npm run wrangler -- secret put RECIPIENT_ADDRESS
467
- # Enter: ${recipientAddress}
468
- \`\`\`
469
-
470
- ## Local Development
638
+ ### Set Production Secrets
471
639
 
472
640
  \`\`\`bash
473
- npm run dev
641
+ # Set your recipient address (where payments will be sent)
642
+ wrangler secret put RECIPIENT_ADDRESS
643
+ # Enter: ${addressDisplay}
474
644
  \`\`\`
475
645
 
476
- The server will start at http://localhost:8787
477
-
478
- ## Deploy
646
+ ### Deploy
479
647
 
480
648
  \`\`\`bash
481
- # Dry run first
482
- npm run deploy:dry
483
-
484
- # Deploy to staging
485
- npm run deploy
649
+ # Deploy to staging (testnet)
650
+ npm run deploy:staging
486
651
 
487
- # Deploy to production
652
+ # Deploy to production (mainnet)
488
653
  npm run deploy:production
489
654
  \`\`\`
490
655
 
@@ -496,9 +661,11 @@ npm run deploy:production
496
661
  {
497
662
  "maxAmountRequired": "1000",
498
663
  "resource": "/api/endpoint",
499
- "payTo": "${recipientAddress}",
664
+ "payTo": "${addressDisplay}",
500
665
  "network": "testnet",
501
- "tokenType": "STX"
666
+ "tokenType": "STX",
667
+ "nonce": "uuid",
668
+ "expiresAt": "2024-01-01T00:05:00Z"
502
669
  }
503
670
  \`\`\`
504
671
  3. Client signs payment transaction (does NOT broadcast)
@@ -509,6 +676,9 @@ npm run deploy:production
509
676
  ## Testing with curl
510
677
 
511
678
  \`\`\`bash
679
+ # Service info (free)
680
+ curl http://localhost:8787/
681
+
512
682
  # Health check (free)
513
683
  curl http://localhost:8787/health
514
684
 
@@ -516,9 +686,33 @@ curl http://localhost:8787/health
516
686
  curl http://localhost:8787${endpoints[0]?.path || "/api/endpoint"}
517
687
  \`\`\`
518
688
 
689
+ ## Token Type Selection
690
+
691
+ Clients can specify which token to pay with using the \`X-PAYMENT-TOKEN-TYPE\` header:
692
+
693
+ \`\`\`bash
694
+ # Pay with sBTC instead of STX
695
+ curl -H "X-PAYMENT-TOKEN-TYPE: sBTC" http://localhost:8787${endpoints[0]?.path || "/api/endpoint"}
696
+ \`\`\`
697
+
698
+ Supported values: \`STX\`, \`sBTC\`, \`USDCx\`
699
+
700
+ ## Error Codes
701
+
702
+ The API returns structured error responses for payment failures:
703
+
704
+ | Code | Description | HTTP Status |
705
+ |------|-------------|-------------|
706
+ | \`INSUFFICIENT_FUNDS\` | Wallet needs funding | 402 |
707
+ | \`PAYMENT_EXPIRED\` | Sign a new payment | 402 |
708
+ | \`AMOUNT_TOO_LOW\` | Payment below minimum | 402 |
709
+ | \`PAYMENT_INVALID\` | Bad signature/params | 400 |
710
+ | \`NETWORK_ERROR\` | Transient error | 502 |
711
+ | \`FACILITATOR_UNAVAILABLE\` | Try again later | 503 |
712
+
519
713
  ---
520
714
 
521
- Generated with @aibtc/mcp-server scaffold tool.
715
+ Generated with [@aibtc/mcp-server](https://www.npmjs.com/package/@aibtc/mcp-server) scaffold tool.
522
716
  `;
523
717
  }
524
718
  // =============================================================================
@@ -553,6 +747,7 @@ export async function scaffoldProject(config) {
553
747
  { name: "package.json", content: getPackageJsonTemplate(projectName) },
554
748
  { name: "tsconfig.json", content: getTsconfigTemplate() },
555
749
  { name: ".env.example", content: getEnvExampleTemplate(recipientAddress) },
750
+ { name: ".dev.vars", content: getDevVarsTemplate(recipientAddress) },
556
751
  { name: ".gitignore", content: getGitignoreTemplate() },
557
752
  { name: "README.md", content: getReadmeTemplate(projectName, endpoints, recipientAddress) },
558
753
  ];
@@ -561,16 +756,22 @@ export async function scaffoldProject(config) {
561
756
  await fs.writeFile(filePath, file.content, "utf-8");
562
757
  filesCreated.push(file.name);
563
758
  }
759
+ const addressInstruction = recipientAddress
760
+ ? `wrangler secret put RECIPIENT_ADDRESS (enter: ${recipientAddress})`
761
+ : "wrangler secret put RECIPIENT_ADDRESS (enter your Stacks address)";
564
762
  return {
565
763
  projectPath,
566
764
  filesCreated,
567
765
  nextSteps: [
568
766
  `cd ${projectPath}`,
569
767
  "npm install",
570
- "cp .env.example .env",
571
- "# Add your Cloudflare credentials to .env",
572
- `npm run wrangler -- secret put RECIPIENT_ADDRESS (enter: ${recipientAddress})`,
768
+ recipientAddress
769
+ ? "# .dev.vars is pre-configured with your address"
770
+ : "# Edit .dev.vars and set your RECIPIENT_ADDRESS",
573
771
  "npm run dev",
772
+ "# For production deployment:",
773
+ addressInstruction,
774
+ "npm run deploy:production",
574
775
  ],
575
776
  };
576
777
  }
@@ -611,13 +812,10 @@ function generateAIEndpointCode(endpoints, defaultModel) {
611
812
  app.post('${ep.path}',
612
813
  x402Middleware({
613
814
  amount: '${amountSmallest}',
614
- address: env.RECIPIENT_ADDRESS,
615
- network: env.NETWORK as 'mainnet' | 'testnet',
616
815
  tokenType: '${ep.tokenType}',
617
- facilitatorUrl: env.FACILITATOR_URL,
618
816
  }),
619
817
  async (c) => {
620
- const payment = c.get('payment');
818
+ const payment = c.get('x402');
621
819
  const body = await c.req.json<{ prompt?: string; message?: string; text?: string; targetLanguage?: string }>();
622
820
  const userInput = body.prompt || body.message || body.text || '';
623
821
 
@@ -626,7 +824,7 @@ app.post('${ep.path}',
626
824
  }
627
825
 
628
826
  const result = await callOpenRouter({
629
- apiKey: env.OPENROUTER_API_KEY,
827
+ apiKey: c.env.OPENROUTER_API_KEY,
630
828
  model: '${model}',
631
829
  systemPrompt: \`${systemPrompt.replace(/`/g, "\\`")}\`,
632
830
  userMessage: ${ep.aiType === "translate" ? "`Translate to ${body.targetLanguage || 'English'}: ${userInput}`" : "userInput"},
@@ -636,7 +834,10 @@ app.post('${ep.path}',
636
834
  result: result.content,
637
835
  model: result.model,
638
836
  usage: result.usage,
639
- txId: payment?.txId,
837
+ payment: {
838
+ txId: payment?.settleResult?.txId,
839
+ sender: payment?.payerAddress,
840
+ },
640
841
  });
641
842
  }
642
843
  );`;
@@ -658,12 +859,18 @@ function generateAIEndpointDocs(endpoints) {
658
859
  }
659
860
  function getAIIndexTemplate(endpoints, defaultModel) {
660
861
  const endpointCode = generateAIEndpointCode(endpoints, defaultModel);
661
- return `import { Hono } from 'hono';
862
+ return `// BigInt.toJSON polyfill for JSON.stringify compatibility
863
+ (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () {
864
+ return this.toString();
865
+ };
866
+
867
+ import { Hono } from 'hono';
662
868
  import { cors } from 'hono/cors';
663
869
  import { x402Middleware } from './x402-middleware';
870
+ import type { X402Context } from './x402-middleware';
664
871
  import { callOpenRouter } from './openrouter';
665
872
 
666
- type Bindings = {
873
+ type Env = {
667
874
  RECIPIENT_ADDRESS: string;
668
875
  NETWORK: string;
669
876
  FACILITATOR_URL: string;
@@ -671,29 +878,31 @@ type Bindings = {
671
878
  };
672
879
 
673
880
  type Variables = {
674
- payment?: {
675
- txId: string;
676
- status: string;
677
- sender: string;
678
- recipient: string;
679
- amount: bigint;
680
- };
881
+ x402?: X402Context;
681
882
  };
682
883
 
683
- const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
884
+ const app = new Hono<{ Bindings: Env; Variables: Variables }>();
684
885
 
685
- app.use('*', cors());
886
+ // CORS middleware with x402 headers
887
+ app.use('*', cors({
888
+ origin: '*',
889
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
890
+ allowHeaders: ['X-PAYMENT', 'X-PAYMENT-TOKEN-TYPE', 'Authorization', 'Content-Type'],
891
+ exposeHeaders: ['X-PAYMENT-RESPONSE', 'X-PAYER-ADDRESS'],
892
+ }));
686
893
 
687
894
  // Startup validation - fail fast if required secrets are missing
688
895
  app.use('*', async (c, next) => {
896
+ // Skip validation for health check and root
897
+ if (c.req.path === '/health' || c.req.path === '/') {
898
+ return next();
899
+ }
900
+
689
901
  const missingSecrets: string[] = [];
690
902
 
691
903
  if (!c.env.RECIPIENT_ADDRESS) {
692
904
  missingSecrets.push('RECIPIENT_ADDRESS');
693
905
  }
694
- if (!c.env.FACILITATOR_URL) {
695
- missingSecrets.push('FACILITATOR_URL');
696
- }
697
906
  if (!c.env.OPENROUTER_API_KEY) {
698
907
  missingSecrets.push('OPENROUTER_API_KEY');
699
908
  }
@@ -702,43 +911,36 @@ app.use('*', async (c, next) => {
702
911
  return c.json({
703
912
  error: 'Server configuration error',
704
913
  message: \`Missing required secrets: \${missingSecrets.join(', ')}\`,
705
- hint: missingSecrets.map(s => \`Run: npm run wrangler -- secret put \${s}\`).join(' && '),
914
+ hint: missingSecrets.map(s => \`Run: wrangler secret put \${s}\`).join(' && '),
706
915
  }, 503);
707
916
  }
708
917
 
709
918
  await next();
710
919
  });
711
920
 
921
+ // Service info at root (free)
922
+ app.get('/', (c) => {
923
+ return c.json({
924
+ service: 'x402-ai-api',
925
+ version: '1.0.0',
926
+ defaultModel: '${defaultModel}',
927
+ health: '/health',
928
+ payment: {
929
+ tokens: ['STX', 'sBTC', 'USDCx'],
930
+ header: 'X-PAYMENT',
931
+ tokenTypeHeader: 'X-PAYMENT-TOKEN-TYPE',
932
+ },
933
+ });
934
+ });
935
+
712
936
  // Health check (free)
713
937
  app.get('/health', (c) => {
714
938
  return c.json({
715
939
  status: 'ok',
716
940
  timestamp: new Date().toISOString(),
941
+ network: c.env.NETWORK || 'testnet',
717
942
  });
718
943
  });
719
-
720
- // x402-protected AI endpoints
721
- app.use('*', async (c, next) => {
722
- // Make env available to middleware
723
- const env = c.env;
724
- (globalThis as Record<string, unknown>).__env = env;
725
- await next();
726
- });
727
-
728
- const env = {
729
- get RECIPIENT_ADDRESS() {
730
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.RECIPIENT_ADDRESS || '';
731
- },
732
- get NETWORK() {
733
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.NETWORK || 'testnet';
734
- },
735
- get FACILITATOR_URL() {
736
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.FACILITATOR_URL || '';
737
- },
738
- get OPENROUTER_API_KEY() {
739
- return ((globalThis as Record<string, unknown>).__env as Bindings)?.OPENROUTER_API_KEY || '';
740
- },
741
- };
742
944
  ${endpointCode}
743
945
 
744
946
  export default app;
@@ -841,123 +1043,134 @@ function getAIPackageJsonTemplate(projectName) {
841
1043
  "private": true,
842
1044
  "type": "module",
843
1045
  "scripts": {
844
- "wrangler": "set -a && . ./.env && set +a && wrangler",
845
- "dev": "npm run wrangler -- dev",
846
- "deploy": "npm run wrangler -- deploy",
847
- "deploy:dry": "npm run wrangler -- deploy --dry-run",
848
- "deploy:production": "npm run wrangler -- deploy --env production",
849
- "tail": "npm run wrangler -- tail"
1046
+ "dev": "wrangler dev",
1047
+ "deploy": "wrangler deploy",
1048
+ "deploy:staging": "wrangler deploy --env staging",
1049
+ "deploy:production": "wrangler deploy --env production",
1050
+ "tail": "wrangler tail",
1051
+ "cf-typegen": "wrangler types"
850
1052
  },
851
1053
  "dependencies": {
852
- "hono": "^4.7.0"
1054
+ "hono": "^4.7.0",
1055
+ "x402-stacks": "^1.1.1"
853
1056
  },
854
1057
  "devDependencies": {
855
1058
  "@cloudflare/workers-types": "^4.20250109.0",
856
1059
  "typescript": "^5.7.0",
857
- "wrangler": "^4.5.0"
1060
+ "wrangler": "^4.56.0"
858
1061
  }
859
1062
  }
860
1063
  `;
861
1064
  }
862
1065
  function getAIEnvExampleTemplate(recipientAddress) {
863
- return `# Cloudflare credentials
1066
+ const addressNote = recipientAddress
1067
+ ? `# Value: ${recipientAddress}`
1068
+ : "# Value: Your Stacks address (SP... for mainnet, ST... for testnet)";
1069
+ return `# Cloudflare credentials (only needed for CI/CD deployment)
1070
+ # For local dev, wrangler uses browser-based auth
864
1071
  CLOUDFLARE_API_TOKEN=your-api-token-here
865
1072
  CLOUDFLARE_ACCOUNT_ID=your-account-id-here
866
1073
 
867
1074
  # x402 recipient address (set via wrangler secret)
868
1075
  # wrangler secret put RECIPIENT_ADDRESS
869
- # Value: ${recipientAddress}
1076
+ ${addressNote}
870
1077
 
871
1078
  # OpenRouter API key (set via wrangler secret)
872
1079
  # Get your key at https://openrouter.ai/keys
873
1080
  # wrangler secret put OPENROUTER_API_KEY
874
1081
  `;
875
1082
  }
1083
+ function getAIDevVarsTemplate(recipientAddress) {
1084
+ const address = recipientAddress || "YOUR_STACKS_ADDRESS_HERE";
1085
+ return `# Local development variables
1086
+ # These are used by wrangler dev and are NOT deployed to production
1087
+ # For production secrets, use: wrangler secret put <SECRET_NAME>
1088
+
1089
+ RECIPIENT_ADDRESS=${address}
1090
+ OPENROUTER_API_KEY=your-openrouter-key-here
1091
+ `;
1092
+ }
876
1093
  function getAIReadmeTemplate(projectName, endpoints, recipientAddress, defaultModel) {
877
1094
  const tokenList = [...new Set(endpoints.map((ep) => `- ${ep.tokenType}`))].join("\n");
878
1095
  const endpointDocs = generateAIEndpointDocs(endpoints);
1096
+ const addressDisplay = recipientAddress || "YOUR_STACKS_ADDRESS";
1097
+ const modelDisplay = defaultModel || "anthropic/claude-3-haiku";
879
1098
  return `# ${projectName}
880
1099
 
881
1100
  x402-enabled AI API endpoints on Cloudflare Workers, powered by OpenRouter.
882
1101
 
1102
+ Built using patterns from:
1103
+ - [x402-api](https://github.com/aibtcdev/x402-api)
1104
+ - [stx402](https://github.com/whoabuddy/stx402)
1105
+
1106
+ ## Quick Start
1107
+
1108
+ \`\`\`bash
1109
+ # Install dependencies
1110
+ npm install
1111
+
1112
+ # Edit .dev.vars with your settings:
1113
+ # - RECIPIENT_ADDRESS: Your Stacks address
1114
+ # - OPENROUTER_API_KEY: Get from https://openrouter.ai/keys
1115
+
1116
+ # Start local dev server
1117
+ npm run dev
1118
+ \`\`\`
1119
+
1120
+ The server will start at http://localhost:8787
1121
+
883
1122
  ## AI Provider
884
1123
 
885
1124
  This API uses [OpenRouter](https://openrouter.ai) to access AI models.
886
- Default model: \`${defaultModel}\`
1125
+ Default model: \`${modelDisplay}\`
887
1126
 
888
1127
  ## Payment Tokens
889
1128
 
890
1129
  This API accepts payments in:
891
1130
  ${tokenList}
892
1131
 
893
- ## Recipient Address
894
-
895
- Payments are sent to: \`${recipientAddress}\`
896
-
897
1132
  ## Endpoints
898
1133
 
1134
+ ### GET /
1135
+ - **Description:** Service info
1136
+ - **Cost:** Free
1137
+
899
1138
  ### GET /health
900
1139
  - **Description:** Health check endpoint
901
1140
  - **Cost:** Free
902
- - **Payment Required:** No
903
1141
 
904
1142
  ${endpointDocs}
905
1143
 
906
- ## Setup
907
-
908
- 1. Install dependencies:
909
- \`\`\`bash
910
- npm install
911
- \`\`\`
912
-
913
- 2. Create \`.env\` file from \`.env.example\`:
914
- \`\`\`bash
915
- cp .env.example .env
916
- \`\`\`
917
-
918
- 3. Add your Cloudflare credentials to \`.env\`
919
-
920
- 4. Set secrets:
921
- \`\`\`bash
922
- # Recipient address for payments
923
- npm run wrangler -- secret put RECIPIENT_ADDRESS
924
- # Enter: ${recipientAddress}
1144
+ ## Deployment
925
1145
 
926
- # OpenRouter API key (get from https://openrouter.ai/keys)
927
- npm run wrangler -- secret put OPENROUTER_API_KEY
928
- \`\`\`
929
-
930
- ## Local Development
1146
+ ### Set Production Secrets
931
1147
 
932
- For local development, create a \`.dev.vars\` file:
933
- \`\`\`
934
- RECIPIENT_ADDRESS=${recipientAddress}
935
- OPENROUTER_API_KEY=your-openrouter-key
936
- \`\`\`
937
-
938
- Then run:
939
1148
  \`\`\`bash
940
- npm run dev
941
- \`\`\`
1149
+ # Set your recipient address (where payments will be sent)
1150
+ wrangler secret put RECIPIENT_ADDRESS
1151
+ # Enter: ${addressDisplay}
942
1152
 
943
- The server will start at http://localhost:8787
1153
+ # Set your OpenRouter API key
1154
+ wrangler secret put OPENROUTER_API_KEY
1155
+ # Enter: your-api-key-from-openrouter.ai/keys
1156
+ \`\`\`
944
1157
 
945
- ## Deploy
1158
+ ### Deploy
946
1159
 
947
1160
  \`\`\`bash
948
- # Dry run first
949
- npm run deploy:dry
1161
+ # Deploy to staging (testnet)
1162
+ npm run deploy:staging
950
1163
 
951
- # Deploy to staging
952
- npm run deploy
953
-
954
- # Deploy to production
1164
+ # Deploy to production (mainnet)
955
1165
  npm run deploy:production
956
1166
  \`\`\`
957
1167
 
958
1168
  ## Example Usage
959
1169
 
960
1170
  \`\`\`bash
1171
+ # Service info (free)
1172
+ curl http://localhost:8787/
1173
+
961
1174
  # Health check (free)
962
1175
  curl http://localhost:8787/health
963
1176
 
@@ -976,21 +1189,46 @@ curl -X POST http://localhost:8787${endpoints[0]?.path || "/api/chat"} \\
976
1189
  5. Server verifies and settles payment via facilitator
977
1190
  6. Server calls OpenRouter API and returns AI response
978
1191
 
1192
+ ## Token Type Selection
1193
+
1194
+ Clients can specify which token to pay with using the \`X-PAYMENT-TOKEN-TYPE\` header:
1195
+
1196
+ \`\`\`bash
1197
+ # Pay with sBTC instead of STX
1198
+ curl -H "X-PAYMENT-TOKEN-TYPE: sBTC" -X POST http://localhost:8787${endpoints[0]?.path || "/api/chat"} \\
1199
+ -H "Content-Type: application/json" \\
1200
+ -d '{"prompt": "Hello"}'
1201
+ \`\`\`
1202
+
1203
+ Supported values: \`STX\`, \`sBTC\`, \`USDCx\`
1204
+
979
1205
  ## OpenRouter Models
980
1206
 
981
- You can use any model available on OpenRouter. Popular options:
982
- - \`anthropic/claude-3.5-sonnet\` - Best for complex tasks
983
- - \`anthropic/claude-3-haiku\` - Fast and affordable
1207
+ Popular options:
1208
+ - \`anthropic/claude-sonnet-4.5\` - Best overall, 1M context
1209
+ - \`anthropic/claude-3.5-haiku\` - Fast and affordable
984
1210
  - \`openai/gpt-4o\` - OpenAI's latest
985
1211
  - \`openai/gpt-4o-mini\` - Fast and cheap
986
- - \`meta-llama/llama-3.1-70b-instruct\` - Open source
987
- - \`google/gemini-pro-1.5\` - Google's model
1212
+ - \`google/gemini-2.5-flash\` - 1M context, fast
1213
+ - \`deepseek/deepseek-r1\` - Excellent reasoning
1214
+ - \`meta-llama/llama-3.3-70b-instruct\` - Best open source
988
1215
 
989
1216
  See all models: https://openrouter.ai/models
990
1217
 
1218
+ ## Error Codes
1219
+
1220
+ | Code | Description | HTTP Status |
1221
+ |------|-------------|-------------|
1222
+ | \`INSUFFICIENT_FUNDS\` | Wallet needs funding | 402 |
1223
+ | \`PAYMENT_EXPIRED\` | Sign a new payment | 402 |
1224
+ | \`AMOUNT_TOO_LOW\` | Payment below minimum | 402 |
1225
+ | \`PAYMENT_INVALID\` | Bad signature/params | 400 |
1226
+ | \`NETWORK_ERROR\` | Transient error | 502 |
1227
+ | \`FACILITATOR_UNAVAILABLE\` | Try again later | 503 |
1228
+
991
1229
  ---
992
1230
 
993
- Generated with @aibtc/mcp-server scaffold tool.
1231
+ Generated with [@aibtc/mcp-server](https://www.npmjs.com/package/@aibtc/mcp-server) scaffold tool.
994
1232
  `;
995
1233
  }
996
1234
  // =============================================================================
@@ -1026,6 +1264,7 @@ export async function scaffoldAIProject(config) {
1026
1264
  { name: "package.json", content: getAIPackageJsonTemplate(projectName) },
1027
1265
  { name: "tsconfig.json", content: getTsconfigTemplate() },
1028
1266
  { name: ".env.example", content: getAIEnvExampleTemplate(recipientAddress) },
1267
+ { name: ".dev.vars", content: getAIDevVarsTemplate(recipientAddress) },
1029
1268
  { name: ".gitignore", content: getGitignoreTemplate() },
1030
1269
  {
1031
1270
  name: "README.md",
@@ -1037,18 +1276,21 @@ export async function scaffoldAIProject(config) {
1037
1276
  await fs.writeFile(filePath, file.content, "utf-8");
1038
1277
  filesCreated.push(file.name);
1039
1278
  }
1279
+ const addressInstruction = recipientAddress
1280
+ ? `wrangler secret put RECIPIENT_ADDRESS (enter: ${recipientAddress})`
1281
+ : "wrangler secret put RECIPIENT_ADDRESS (enter your Stacks address)";
1040
1282
  return {
1041
1283
  projectPath,
1042
1284
  filesCreated,
1043
1285
  nextSteps: [
1044
1286
  `cd ${projectPath}`,
1045
1287
  "npm install",
1046
- "cp .env.example .env",
1047
- "# Add your Cloudflare credentials to .env",
1048
- `npm run wrangler -- secret put RECIPIENT_ADDRESS (enter: ${recipientAddress})`,
1049
- "npm run wrangler -- secret put OPENROUTER_API_KEY (get from https://openrouter.ai/keys)",
1050
- "# For local dev, create .dev.vars with RECIPIENT_ADDRESS and OPENROUTER_API_KEY",
1288
+ "# Edit .dev.vars with your RECIPIENT_ADDRESS and OPENROUTER_API_KEY",
1051
1289
  "npm run dev",
1290
+ "# For production deployment:",
1291
+ addressInstruction,
1292
+ "wrangler secret put OPENROUTER_API_KEY (get from https://openrouter.ai/keys)",
1293
+ "npm run deploy:production",
1052
1294
  ],
1053
1295
  };
1054
1296
  }