@aibtc/mcp-server 1.0.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.
- package/README.md +340 -0
- package/dist/api.d.ts +9 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +79 -0
- package/dist/api.js.map +1 -0
- package/dist/config/contracts.d.ts +169 -0
- package/dist/config/contracts.d.ts.map +1 -0
- package/dist/config/contracts.js +250 -0
- package/dist/config/contracts.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +3 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/networks.d.ts +11 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +21 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/endpoints/index.d.ts +2 -0
- package/dist/endpoints/index.d.ts.map +1 -0
- package/dist/endpoints/index.js +2 -0
- package/dist/endpoints/index.js.map +1 -0
- package/dist/endpoints/registry.d.ts +38 -0
- package/dist/endpoints/registry.d.ts.map +1 -0
- package/dist/endpoints/registry.js +935 -0
- package/dist/endpoints/registry.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/services/bitflow.service.d.ts +159 -0
- package/dist/services/bitflow.service.d.ts.map +1 -0
- package/dist/services/bitflow.service.js +325 -0
- package/dist/services/bitflow.service.js.map +1 -0
- package/dist/services/bns.service.d.ts +87 -0
- package/dist/services/bns.service.d.ts.map +1 -0
- package/dist/services/bns.service.js +312 -0
- package/dist/services/bns.service.js.map +1 -0
- package/dist/services/defi.service.d.ts +146 -0
- package/dist/services/defi.service.d.ts.map +1 -0
- package/dist/services/defi.service.js +461 -0
- package/dist/services/defi.service.js.map +1 -0
- package/dist/services/hiro-api.d.ts +438 -0
- package/dist/services/hiro-api.d.ts.map +1 -0
- package/dist/services/hiro-api.js +376 -0
- package/dist/services/hiro-api.js.map +1 -0
- package/dist/services/nft.service.d.ts +70 -0
- package/dist/services/nft.service.d.ts.map +1 -0
- package/dist/services/nft.service.js +148 -0
- package/dist/services/nft.service.js.map +1 -0
- package/dist/services/sbtc.service.d.ts +44 -0
- package/dist/services/sbtc.service.d.ts.map +1 -0
- package/dist/services/sbtc.service.js +100 -0
- package/dist/services/sbtc.service.js.map +1 -0
- package/dist/services/scaffold.service.d.ts +41 -0
- package/dist/services/scaffold.service.d.ts.map +1 -0
- package/dist/services/scaffold.service.js +1055 -0
- package/dist/services/scaffold.service.js.map +1 -0
- package/dist/services/stacking.service.d.ts +58 -0
- package/dist/services/stacking.service.d.ts.map +1 -0
- package/dist/services/stacking.service.js +153 -0
- package/dist/services/stacking.service.js.map +1 -0
- package/dist/services/tokens.service.d.ts +62 -0
- package/dist/services/tokens.service.d.ts.map +1 -0
- package/dist/services/tokens.service.js +119 -0
- package/dist/services/tokens.service.js.map +1 -0
- package/dist/services/wallet-manager.d.ts +107 -0
- package/dist/services/wallet-manager.d.ts.map +1 -0
- package/dist/services/wallet-manager.js +389 -0
- package/dist/services/wallet-manager.js.map +1 -0
- package/dist/services/x402.service.d.ts +26 -0
- package/dist/services/x402.service.d.ts.map +1 -0
- package/dist/services/x402.service.js +125 -0
- package/dist/services/x402.service.js.map +1 -0
- package/dist/tools/bitflow.tools.d.ts +3 -0
- package/dist/tools/bitflow.tools.d.ts.map +1 -0
- package/dist/tools/bitflow.tools.js +501 -0
- package/dist/tools/bitflow.tools.js.map +1 -0
- package/dist/tools/bns.tools.d.ts +3 -0
- package/dist/tools/bns.tools.d.ts.map +1 -0
- package/dist/tools/bns.tools.js +164 -0
- package/dist/tools/bns.tools.js.map +1 -0
- package/dist/tools/contract.tools.d.ts +3 -0
- package/dist/tools/contract.tools.d.ts.map +1 -0
- package/dist/tools/contract.tools.js +126 -0
- package/dist/tools/contract.tools.js.map +1 -0
- package/dist/tools/defi.tools.d.ts +3 -0
- package/dist/tools/defi.tools.d.ts.map +1 -0
- package/dist/tools/defi.tools.js +425 -0
- package/dist/tools/defi.tools.js.map +1 -0
- package/dist/tools/endpoint.tools.d.ts +3 -0
- package/dist/tools/endpoint.tools.d.ts.map +1 -0
- package/dist/tools/endpoint.tools.js +157 -0
- package/dist/tools/endpoint.tools.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/nft.tools.d.ts +3 -0
- package/dist/tools/nft.tools.d.ts.map +1 -0
- package/dist/tools/nft.tools.js +154 -0
- package/dist/tools/nft.tools.js.map +1 -0
- package/dist/tools/openrouter.tools.d.ts +3 -0
- package/dist/tools/openrouter.tools.d.ts.map +1 -0
- package/dist/tools/openrouter.tools.js +664 -0
- package/dist/tools/openrouter.tools.js.map +1 -0
- package/dist/tools/query.tools.d.ts +3 -0
- package/dist/tools/query.tools.d.ts.map +1 -0
- package/dist/tools/query.tools.js +209 -0
- package/dist/tools/query.tools.js.map +1 -0
- package/dist/tools/sbtc.tools.d.ts +3 -0
- package/dist/tools/sbtc.tools.d.ts.map +1 -0
- package/dist/tools/sbtc.tools.js +103 -0
- package/dist/tools/sbtc.tools.js.map +1 -0
- package/dist/tools/scaffold.tools.d.ts +3 -0
- package/dist/tools/scaffold.tools.d.ts.map +1 -0
- package/dist/tools/scaffold.tools.js +216 -0
- package/dist/tools/scaffold.tools.js.map +1 -0
- package/dist/tools/stacking.tools.d.ts +3 -0
- package/dist/tools/stacking.tools.d.ts.map +1 -0
- package/dist/tools/stacking.tools.js +112 -0
- package/dist/tools/stacking.tools.js.map +1 -0
- package/dist/tools/tokens.tools.d.ts +3 -0
- package/dist/tools/tokens.tools.d.ts.map +1 -0
- package/dist/tools/tokens.tools.js +154 -0
- package/dist/tools/tokens.tools.js.map +1 -0
- package/dist/tools/transfer.tools.d.ts +3 -0
- package/dist/tools/transfer.tools.d.ts.map +1 -0
- package/dist/tools/transfer.tools.js +62 -0
- package/dist/tools/transfer.tools.js.map +1 -0
- package/dist/tools/wallet-management.tools.d.ts +6 -0
- package/dist/tools/wallet-management.tools.d.ts.map +1 -0
- package/dist/tools/wallet-management.tools.js +390 -0
- package/dist/tools/wallet-management.tools.js.map +1 -0
- package/dist/tools/wallet.tools.d.ts +3 -0
- package/dist/tools/wallet.tools.d.ts.map +1 -0
- package/dist/tools/wallet.tools.js +105 -0
- package/dist/tools/wallet.tools.js.map +1 -0
- package/dist/transactions/builder.d.ts +56 -0
- package/dist/transactions/builder.d.ts.map +1 -0
- package/dist/transactions/builder.js +134 -0
- package/dist/transactions/builder.js.map +1 -0
- package/dist/transactions/clarity-values.d.ts +67 -0
- package/dist/transactions/clarity-values.d.ts.map +1 -0
- package/dist/transactions/clarity-values.js +169 -0
- package/dist/transactions/clarity-values.js.map +1 -0
- package/dist/transactions/post-conditions.d.ts +27 -0
- package/dist/transactions/post-conditions.d.ts.map +1 -0
- package/dist/transactions/post-conditions.js +101 -0
- package/dist/transactions/post-conditions.js.map +1 -0
- package/dist/utils/encryption.d.ts +33 -0
- package/dist/utils/encryption.d.ts.map +1 -0
- package/dist/utils/encryption.js +110 -0
- package/dist/utils/encryption.js.map +1 -0
- package/dist/utils/errors.d.ts +84 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +132 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/formatting.d.ts +51 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +114 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/storage.d.ts +90 -0
- package/dist/utils/storage.d.ts.map +1 -0
- package/dist/utils/storage.js +196 -0
- package/dist/utils/storage.js.map +1 -0
- package/dist/utils/validation.d.ts +67 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +74 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/wallet.d.ts +86 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +279 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Token decimals for conversion
|
|
4
|
+
const TOKEN_DECIMALS = {
|
|
5
|
+
STX: 6,
|
|
6
|
+
sBTC: 8,
|
|
7
|
+
USDCx: 6,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Convert human-readable amount to smallest unit (microSTX, sats, etc.)
|
|
11
|
+
*/
|
|
12
|
+
function toSmallestUnit(amount, tokenType) {
|
|
13
|
+
const decimals = TOKEN_DECIMALS[tokenType];
|
|
14
|
+
const [whole, fraction = ""] = amount.split(".");
|
|
15
|
+
const paddedFraction = fraction.padEnd(decimals, "0").slice(0, decimals);
|
|
16
|
+
return BigInt(whole + paddedFraction).toString();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generate Hono route code for each endpoint
|
|
20
|
+
*/
|
|
21
|
+
function generateEndpointCode(endpoints) {
|
|
22
|
+
return endpoints
|
|
23
|
+
.map((ep) => {
|
|
24
|
+
const amountSmallest = toSmallestUnit(ep.amount, ep.tokenType);
|
|
25
|
+
// Generate real example logic based on endpoint characteristics
|
|
26
|
+
const exampleLogic = generateExampleLogic(ep);
|
|
27
|
+
return `
|
|
28
|
+
// ${ep.description}
|
|
29
|
+
app.${ep.method.toLowerCase()}('${ep.path}',
|
|
30
|
+
x402Middleware({
|
|
31
|
+
amount: '${amountSmallest}',
|
|
32
|
+
address: env.RECIPIENT_ADDRESS,
|
|
33
|
+
network: env.NETWORK as 'mainnet' | 'testnet',
|
|
34
|
+
tokenType: '${ep.tokenType}',
|
|
35
|
+
facilitatorUrl: env.FACILITATOR_URL,
|
|
36
|
+
}),
|
|
37
|
+
async (c) => {
|
|
38
|
+
const payment = c.get('payment');
|
|
39
|
+
${exampleLogic}
|
|
40
|
+
}
|
|
41
|
+
);`;
|
|
42
|
+
})
|
|
43
|
+
.join("\n");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate real example logic for endpoints based on their configuration.
|
|
47
|
+
* This replaces placeholder/TODO code with working examples.
|
|
48
|
+
*/
|
|
49
|
+
function generateExampleLogic(ep) {
|
|
50
|
+
if (ep.method === "POST") {
|
|
51
|
+
return `
|
|
52
|
+
// Parse request body
|
|
53
|
+
const body = await c.req.json<Record<string, unknown>>().catch(() => ({}));
|
|
54
|
+
|
|
55
|
+
// Your business logic here - this example echoes the request
|
|
56
|
+
const result = {
|
|
57
|
+
received: body,
|
|
58
|
+
processedAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return c.json({
|
|
62
|
+
success: true,
|
|
63
|
+
data: result,
|
|
64
|
+
payment: {
|
|
65
|
+
txId: payment?.txId,
|
|
66
|
+
sender: payment?.sender,
|
|
67
|
+
amount: payment?.amount?.toString(),
|
|
68
|
+
},
|
|
69
|
+
});`;
|
|
70
|
+
}
|
|
71
|
+
// GET endpoint - return example data
|
|
72
|
+
return `
|
|
73
|
+
// Your business logic here - this example returns sample data
|
|
74
|
+
const data = {
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
description: '${ep.description}',
|
|
77
|
+
generatedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return c.json({
|
|
81
|
+
success: true,
|
|
82
|
+
data,
|
|
83
|
+
payment: {
|
|
84
|
+
txId: payment?.txId,
|
|
85
|
+
sender: payment?.sender,
|
|
86
|
+
amount: payment?.amount?.toString(),
|
|
87
|
+
},
|
|
88
|
+
});`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Generate endpoint documentation for README
|
|
92
|
+
*/
|
|
93
|
+
function generateEndpointDocs(endpoints) {
|
|
94
|
+
return endpoints
|
|
95
|
+
.map((ep) => {
|
|
96
|
+
return `### ${ep.method} ${ep.path}
|
|
97
|
+
- **Description:** ${ep.description}
|
|
98
|
+
- **Cost:** ${ep.amount} ${ep.tokenType}
|
|
99
|
+
- **Payment Required:** Yes`;
|
|
100
|
+
})
|
|
101
|
+
.join("\n\n");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate token list for README
|
|
105
|
+
*/
|
|
106
|
+
function generateTokenList(endpoints) {
|
|
107
|
+
const tokens = [...new Set(endpoints.map((ep) => ep.tokenType))];
|
|
108
|
+
return tokens.map((t) => `- ${t}`).join("\n");
|
|
109
|
+
}
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// FILE TEMPLATES
|
|
112
|
+
// =============================================================================
|
|
113
|
+
function getIndexTemplate(endpoints) {
|
|
114
|
+
const endpointCode = generateEndpointCode(endpoints);
|
|
115
|
+
return `import { Hono } from 'hono';
|
|
116
|
+
import { cors } from 'hono/cors';
|
|
117
|
+
import { x402Middleware } from './x402-middleware';
|
|
118
|
+
|
|
119
|
+
type Bindings = {
|
|
120
|
+
RECIPIENT_ADDRESS: string;
|
|
121
|
+
NETWORK: string;
|
|
122
|
+
FACILITATOR_URL: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type Variables = {
|
|
126
|
+
payment?: {
|
|
127
|
+
txId: string;
|
|
128
|
+
status: string;
|
|
129
|
+
sender: string;
|
|
130
|
+
recipient: string;
|
|
131
|
+
amount: bigint;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
136
|
+
|
|
137
|
+
app.use('*', cors());
|
|
138
|
+
|
|
139
|
+
// Startup validation - fail fast if required secrets are missing
|
|
140
|
+
app.use('*', async (c, next) => {
|
|
141
|
+
const missingSecrets: string[] = [];
|
|
142
|
+
|
|
143
|
+
if (!c.env.RECIPIENT_ADDRESS) {
|
|
144
|
+
missingSecrets.push('RECIPIENT_ADDRESS');
|
|
145
|
+
}
|
|
146
|
+
if (!c.env.FACILITATOR_URL) {
|
|
147
|
+
missingSecrets.push('FACILITATOR_URL');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (missingSecrets.length > 0) {
|
|
151
|
+
return c.json({
|
|
152
|
+
error: 'Server configuration error',
|
|
153
|
+
message: \`Missing required secrets: \${missingSecrets.join(', ')}\`,
|
|
154
|
+
hint: missingSecrets.map(s => \`Run: npm run wrangler -- secret put \${s}\`).join(' && '),
|
|
155
|
+
}, 503);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await next();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Health check (free)
|
|
162
|
+
app.get('/health', (c) => {
|
|
163
|
+
return c.json({
|
|
164
|
+
status: 'ok',
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
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
|
+
${endpointCode}
|
|
189
|
+
|
|
190
|
+
export default app;
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function getMiddlewareTemplate() {
|
|
194
|
+
return `import type { Context, Next } from 'hono';
|
|
195
|
+
|
|
196
|
+
export interface X402Config {
|
|
197
|
+
amount: string;
|
|
198
|
+
address: string;
|
|
199
|
+
network: 'mainnet' | 'testnet';
|
|
200
|
+
tokenType: 'STX' | 'sBTC' | 'USDCx';
|
|
201
|
+
facilitatorUrl?: string;
|
|
202
|
+
resource?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface TokenContract {
|
|
206
|
+
address: string;
|
|
207
|
+
name: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
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
|
+
};
|
|
223
|
+
|
|
224
|
+
interface PaymentRequirement {
|
|
225
|
+
maxAmountRequired: string;
|
|
226
|
+
resource: string;
|
|
227
|
+
payTo: string;
|
|
228
|
+
network: string;
|
|
229
|
+
nonce: string;
|
|
230
|
+
expiresAt: string;
|
|
231
|
+
tokenType: string;
|
|
232
|
+
tokenContract?: TokenContract;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface SettleRequest {
|
|
236
|
+
signed_transaction: string;
|
|
237
|
+
expected_recipient: string;
|
|
238
|
+
min_amount: string;
|
|
239
|
+
network: string;
|
|
240
|
+
token_type: string;
|
|
241
|
+
resource: string;
|
|
242
|
+
method: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
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;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* x402 Payment Middleware for Hono
|
|
257
|
+
*
|
|
258
|
+
* Handles the x402 payment flow:
|
|
259
|
+
* 1. If no X-PAYMENT header, return 402 with payment requirements
|
|
260
|
+
* 2. If X-PAYMENT header present, verify payment via facilitator
|
|
261
|
+
* 3. On success, attach payment info and continue to handler
|
|
262
|
+
*/
|
|
263
|
+
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');
|
|
269
|
+
|
|
270
|
+
if (!signedPayment) {
|
|
271
|
+
// Return 402 Payment Required with payment details
|
|
272
|
+
const paymentReq: PaymentRequirement = {
|
|
273
|
+
maxAmountRequired: config.amount,
|
|
274
|
+
resource: config.resource || c.req.path,
|
|
275
|
+
payTo: config.address,
|
|
276
|
+
network: config.network,
|
|
277
|
+
nonce: crypto.randomUUID(),
|
|
278
|
+
expiresAt: new Date(Date.now() + 300000).toISOString(),
|
|
279
|
+
tokenType: config.tokenType,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (tokenContract) {
|
|
283
|
+
paymentReq.tokenContract = tokenContract;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return c.json(paymentReq, 402);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Verify and settle payment via facilitator
|
|
290
|
+
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,
|
|
299
|
+
};
|
|
300
|
+
|
|
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
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
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
|
+
});
|
|
327
|
+
|
|
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
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
function getWranglerTemplate(projectName, network, facilitatorUrl) {
|
|
343
|
+
return `{
|
|
344
|
+
"name": "${projectName}",
|
|
345
|
+
"main": "src/index.ts",
|
|
346
|
+
"compatibility_date": "2026-01-14",
|
|
347
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
348
|
+
"vars": {
|
|
349
|
+
"NETWORK": "${network}",
|
|
350
|
+
"FACILITATOR_URL": "${facilitatorUrl}"
|
|
351
|
+
},
|
|
352
|
+
"env": {
|
|
353
|
+
"production": {
|
|
354
|
+
"vars": {
|
|
355
|
+
"NETWORK": "mainnet"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
function getPackageJsonTemplate(projectName) {
|
|
363
|
+
return `{
|
|
364
|
+
"name": "${projectName}",
|
|
365
|
+
"version": "1.0.0",
|
|
366
|
+
"private": true,
|
|
367
|
+
"type": "module",
|
|
368
|
+
"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"
|
|
375
|
+
},
|
|
376
|
+
"dependencies": {
|
|
377
|
+
"hono": "^4.7.0"
|
|
378
|
+
},
|
|
379
|
+
"devDependencies": {
|
|
380
|
+
"@cloudflare/workers-types": "^4.20250109.0",
|
|
381
|
+
"typescript": "^5.7.0",
|
|
382
|
+
"wrangler": "^4.5.0"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
function getTsconfigTemplate() {
|
|
388
|
+
return `{
|
|
389
|
+
"compilerOptions": {
|
|
390
|
+
"target": "ES2022",
|
|
391
|
+
"module": "ESNext",
|
|
392
|
+
"moduleResolution": "bundler",
|
|
393
|
+
"strict": true,
|
|
394
|
+
"lib": ["ES2022"],
|
|
395
|
+
"types": ["@cloudflare/workers-types"],
|
|
396
|
+
"esModuleInterop": true,
|
|
397
|
+
"skipLibCheck": true,
|
|
398
|
+
"forceConsistentCasingInFileNames": true,
|
|
399
|
+
"outDir": "dist",
|
|
400
|
+
"rootDir": "src"
|
|
401
|
+
},
|
|
402
|
+
"include": ["src/**/*"],
|
|
403
|
+
"exclude": ["node_modules"]
|
|
404
|
+
}
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
function getEnvExampleTemplate(recipientAddress) {
|
|
408
|
+
return `# Cloudflare credentials
|
|
409
|
+
CLOUDFLARE_API_TOKEN=your-api-token-here
|
|
410
|
+
CLOUDFLARE_ACCOUNT_ID=your-account-id-here
|
|
411
|
+
|
|
412
|
+
# x402 recipient address (set via wrangler secret)
|
|
413
|
+
# wrangler secret put RECIPIENT_ADDRESS
|
|
414
|
+
# Value: ${recipientAddress}
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
function getGitignoreTemplate() {
|
|
418
|
+
return `node_modules/
|
|
419
|
+
dist/
|
|
420
|
+
.env
|
|
421
|
+
.dev.vars
|
|
422
|
+
.wrangler/
|
|
423
|
+
`;
|
|
424
|
+
}
|
|
425
|
+
function getReadmeTemplate(projectName, endpoints, recipientAddress) {
|
|
426
|
+
const tokenList = generateTokenList(endpoints);
|
|
427
|
+
const endpointDocs = generateEndpointDocs(endpoints);
|
|
428
|
+
return `# ${projectName}
|
|
429
|
+
|
|
430
|
+
x402-enabled API endpoints on Cloudflare Workers.
|
|
431
|
+
|
|
432
|
+
## Payment Tokens
|
|
433
|
+
|
|
434
|
+
This API accepts payments in:
|
|
435
|
+
${tokenList}
|
|
436
|
+
|
|
437
|
+
## Recipient Address
|
|
438
|
+
|
|
439
|
+
Payments are sent to: \`${recipientAddress}\`
|
|
440
|
+
|
|
441
|
+
## Endpoints
|
|
442
|
+
|
|
443
|
+
### GET /health
|
|
444
|
+
- **Description:** Health check endpoint
|
|
445
|
+
- **Cost:** Free
|
|
446
|
+
- **Payment Required:** No
|
|
447
|
+
|
|
448
|
+
${endpointDocs}
|
|
449
|
+
|
|
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
|
+
\`\`\`
|
|
461
|
+
|
|
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
|
|
471
|
+
|
|
472
|
+
\`\`\`bash
|
|
473
|
+
npm run dev
|
|
474
|
+
\`\`\`
|
|
475
|
+
|
|
476
|
+
The server will start at http://localhost:8787
|
|
477
|
+
|
|
478
|
+
## Deploy
|
|
479
|
+
|
|
480
|
+
\`\`\`bash
|
|
481
|
+
# Dry run first
|
|
482
|
+
npm run deploy:dry
|
|
483
|
+
|
|
484
|
+
# Deploy to staging
|
|
485
|
+
npm run deploy
|
|
486
|
+
|
|
487
|
+
# Deploy to production
|
|
488
|
+
npm run deploy:production
|
|
489
|
+
\`\`\`
|
|
490
|
+
|
|
491
|
+
## x402 Payment Flow
|
|
492
|
+
|
|
493
|
+
1. Client makes request without payment header
|
|
494
|
+
2. Server returns HTTP 402 with payment requirements:
|
|
495
|
+
\`\`\`json
|
|
496
|
+
{
|
|
497
|
+
"maxAmountRequired": "1000",
|
|
498
|
+
"resource": "/api/endpoint",
|
|
499
|
+
"payTo": "${recipientAddress}",
|
|
500
|
+
"network": "testnet",
|
|
501
|
+
"tokenType": "STX"
|
|
502
|
+
}
|
|
503
|
+
\`\`\`
|
|
504
|
+
3. Client signs payment transaction (does NOT broadcast)
|
|
505
|
+
4. Client retries request with \`X-PAYMENT\` header containing signed tx
|
|
506
|
+
5. Server verifies and settles payment via facilitator
|
|
507
|
+
6. Server returns actual response
|
|
508
|
+
|
|
509
|
+
## Testing with curl
|
|
510
|
+
|
|
511
|
+
\`\`\`bash
|
|
512
|
+
# Health check (free)
|
|
513
|
+
curl http://localhost:8787/health
|
|
514
|
+
|
|
515
|
+
# Protected endpoint (returns 402)
|
|
516
|
+
curl http://localhost:8787${endpoints[0]?.path || "/api/endpoint"}
|
|
517
|
+
\`\`\`
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
Generated with @aibtc/mcp-server scaffold tool.
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
// =============================================================================
|
|
525
|
+
// MAIN SCAFFOLD FUNCTION
|
|
526
|
+
// =============================================================================
|
|
527
|
+
export async function scaffoldProject(config) {
|
|
528
|
+
const { outputDir, projectName, endpoints, recipientAddress, network, facilitatorUrl } = config;
|
|
529
|
+
// Validate output directory exists
|
|
530
|
+
try {
|
|
531
|
+
const stat = await fs.stat(outputDir);
|
|
532
|
+
if (!stat.isDirectory()) {
|
|
533
|
+
throw new Error(`Output path is not a directory: ${outputDir}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (error.code === "ENOENT") {
|
|
538
|
+
throw new Error(`Output directory does not exist: ${outputDir}`);
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
const projectPath = path.join(outputDir, projectName);
|
|
543
|
+
const srcPath = path.join(projectPath, "src");
|
|
544
|
+
// Create project directory structure
|
|
545
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
546
|
+
await fs.mkdir(srcPath, { recursive: true });
|
|
547
|
+
const filesCreated = [];
|
|
548
|
+
// Generate and write files
|
|
549
|
+
const files = [
|
|
550
|
+
{ name: "src/index.ts", content: getIndexTemplate(endpoints) },
|
|
551
|
+
{ name: "src/x402-middleware.ts", content: getMiddlewareTemplate() },
|
|
552
|
+
{ name: "wrangler.jsonc", content: getWranglerTemplate(projectName, network, facilitatorUrl) },
|
|
553
|
+
{ name: "package.json", content: getPackageJsonTemplate(projectName) },
|
|
554
|
+
{ name: "tsconfig.json", content: getTsconfigTemplate() },
|
|
555
|
+
{ name: ".env.example", content: getEnvExampleTemplate(recipientAddress) },
|
|
556
|
+
{ name: ".gitignore", content: getGitignoreTemplate() },
|
|
557
|
+
{ name: "README.md", content: getReadmeTemplate(projectName, endpoints, recipientAddress) },
|
|
558
|
+
];
|
|
559
|
+
for (const file of files) {
|
|
560
|
+
const filePath = path.join(projectPath, file.name);
|
|
561
|
+
await fs.writeFile(filePath, file.content, "utf-8");
|
|
562
|
+
filesCreated.push(file.name);
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
projectPath,
|
|
566
|
+
filesCreated,
|
|
567
|
+
nextSteps: [
|
|
568
|
+
`cd ${projectPath}`,
|
|
569
|
+
"npm install",
|
|
570
|
+
"cp .env.example .env",
|
|
571
|
+
"# Add your Cloudflare credentials to .env",
|
|
572
|
+
`npm run wrangler -- secret put RECIPIENT_ADDRESS (enter: ${recipientAddress})`,
|
|
573
|
+
"npm run dev",
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// =============================================================================
|
|
578
|
+
// AI ENDPOINT TEMPLATES (OpenRouter)
|
|
579
|
+
// =============================================================================
|
|
580
|
+
const AI_TYPE_CONFIGS = {
|
|
581
|
+
chat: {
|
|
582
|
+
systemPrompt: "You are a helpful AI assistant.",
|
|
583
|
+
description: "Chat with an AI assistant",
|
|
584
|
+
},
|
|
585
|
+
completion: {
|
|
586
|
+
systemPrompt: "You are a creative writing assistant. Complete the given text naturally.",
|
|
587
|
+
description: "AI text completion",
|
|
588
|
+
},
|
|
589
|
+
summarize: {
|
|
590
|
+
systemPrompt: "You are a summarization expert. Provide concise summaries of the given text, capturing the key points.",
|
|
591
|
+
description: "Summarize text using AI",
|
|
592
|
+
},
|
|
593
|
+
translate: {
|
|
594
|
+
systemPrompt: "You are a professional translator. Translate the given text accurately while preserving meaning and tone.",
|
|
595
|
+
description: "Translate text using AI",
|
|
596
|
+
},
|
|
597
|
+
custom: {
|
|
598
|
+
systemPrompt: "You are a helpful AI assistant.",
|
|
599
|
+
description: "Custom AI endpoint",
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
function generateAIEndpointCode(endpoints, defaultModel) {
|
|
603
|
+
return endpoints
|
|
604
|
+
.map((ep) => {
|
|
605
|
+
const amountSmallest = toSmallestUnit(ep.amount, ep.tokenType);
|
|
606
|
+
const config = AI_TYPE_CONFIGS[ep.aiType];
|
|
607
|
+
const systemPrompt = ep.systemPrompt || config.systemPrompt;
|
|
608
|
+
const model = ep.model || defaultModel;
|
|
609
|
+
return `
|
|
610
|
+
// ${ep.description}
|
|
611
|
+
app.post('${ep.path}',
|
|
612
|
+
x402Middleware({
|
|
613
|
+
amount: '${amountSmallest}',
|
|
614
|
+
address: env.RECIPIENT_ADDRESS,
|
|
615
|
+
network: env.NETWORK as 'mainnet' | 'testnet',
|
|
616
|
+
tokenType: '${ep.tokenType}',
|
|
617
|
+
facilitatorUrl: env.FACILITATOR_URL,
|
|
618
|
+
}),
|
|
619
|
+
async (c) => {
|
|
620
|
+
const payment = c.get('payment');
|
|
621
|
+
const body = await c.req.json<{ prompt?: string; message?: string; text?: string; targetLanguage?: string }>();
|
|
622
|
+
const userInput = body.prompt || body.message || body.text || '';
|
|
623
|
+
|
|
624
|
+
if (!userInput) {
|
|
625
|
+
return c.json({ error: 'Missing required field: prompt, message, or text' }, 400);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const result = await callOpenRouter({
|
|
629
|
+
apiKey: env.OPENROUTER_API_KEY,
|
|
630
|
+
model: '${model}',
|
|
631
|
+
systemPrompt: \`${systemPrompt.replace(/`/g, "\\`")}\`,
|
|
632
|
+
userMessage: ${ep.aiType === "translate" ? "`Translate to ${body.targetLanguage || 'English'}: ${userInput}`" : "userInput"},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return c.json({
|
|
636
|
+
result: result.content,
|
|
637
|
+
model: result.model,
|
|
638
|
+
usage: result.usage,
|
|
639
|
+
txId: payment?.txId,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
);`;
|
|
643
|
+
})
|
|
644
|
+
.join("\n");
|
|
645
|
+
}
|
|
646
|
+
function generateAIEndpointDocs(endpoints) {
|
|
647
|
+
return endpoints
|
|
648
|
+
.map((ep) => {
|
|
649
|
+
const inputField = ep.aiType === "translate" ? "text, targetLanguage (optional)" : "prompt or message or text";
|
|
650
|
+
return `### POST ${ep.path}
|
|
651
|
+
- **Description:** ${ep.description}
|
|
652
|
+
- **Cost:** ${ep.amount} ${ep.tokenType}
|
|
653
|
+
- **AI Type:** ${ep.aiType}
|
|
654
|
+
- **Input:** \`{ ${inputField} }\`
|
|
655
|
+
- **Payment Required:** Yes`;
|
|
656
|
+
})
|
|
657
|
+
.join("\n\n");
|
|
658
|
+
}
|
|
659
|
+
function getAIIndexTemplate(endpoints, defaultModel) {
|
|
660
|
+
const endpointCode = generateAIEndpointCode(endpoints, defaultModel);
|
|
661
|
+
return `import { Hono } from 'hono';
|
|
662
|
+
import { cors } from 'hono/cors';
|
|
663
|
+
import { x402Middleware } from './x402-middleware';
|
|
664
|
+
import { callOpenRouter } from './openrouter';
|
|
665
|
+
|
|
666
|
+
type Bindings = {
|
|
667
|
+
RECIPIENT_ADDRESS: string;
|
|
668
|
+
NETWORK: string;
|
|
669
|
+
FACILITATOR_URL: string;
|
|
670
|
+
OPENROUTER_API_KEY: string;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
type Variables = {
|
|
674
|
+
payment?: {
|
|
675
|
+
txId: string;
|
|
676
|
+
status: string;
|
|
677
|
+
sender: string;
|
|
678
|
+
recipient: string;
|
|
679
|
+
amount: bigint;
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
684
|
+
|
|
685
|
+
app.use('*', cors());
|
|
686
|
+
|
|
687
|
+
// Startup validation - fail fast if required secrets are missing
|
|
688
|
+
app.use('*', async (c, next) => {
|
|
689
|
+
const missingSecrets: string[] = [];
|
|
690
|
+
|
|
691
|
+
if (!c.env.RECIPIENT_ADDRESS) {
|
|
692
|
+
missingSecrets.push('RECIPIENT_ADDRESS');
|
|
693
|
+
}
|
|
694
|
+
if (!c.env.FACILITATOR_URL) {
|
|
695
|
+
missingSecrets.push('FACILITATOR_URL');
|
|
696
|
+
}
|
|
697
|
+
if (!c.env.OPENROUTER_API_KEY) {
|
|
698
|
+
missingSecrets.push('OPENROUTER_API_KEY');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (missingSecrets.length > 0) {
|
|
702
|
+
return c.json({
|
|
703
|
+
error: 'Server configuration error',
|
|
704
|
+
message: \`Missing required secrets: \${missingSecrets.join(', ')}\`,
|
|
705
|
+
hint: missingSecrets.map(s => \`Run: npm run wrangler -- secret put \${s}\`).join(' && '),
|
|
706
|
+
}, 503);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
await next();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Health check (free)
|
|
713
|
+
app.get('/health', (c) => {
|
|
714
|
+
return c.json({
|
|
715
|
+
status: 'ok',
|
|
716
|
+
timestamp: new Date().toISOString(),
|
|
717
|
+
});
|
|
718
|
+
});
|
|
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
|
+
${endpointCode}
|
|
743
|
+
|
|
744
|
+
export default app;
|
|
745
|
+
`;
|
|
746
|
+
}
|
|
747
|
+
function getOpenRouterTemplate() {
|
|
748
|
+
return `/**
|
|
749
|
+
* OpenRouter API Client
|
|
750
|
+
* https://openrouter.ai/docs
|
|
751
|
+
*/
|
|
752
|
+
|
|
753
|
+
export interface OpenRouterRequest {
|
|
754
|
+
apiKey: string;
|
|
755
|
+
model: string;
|
|
756
|
+
systemPrompt: string;
|
|
757
|
+
userMessage: string;
|
|
758
|
+
maxTokens?: number;
|
|
759
|
+
temperature?: number;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export interface OpenRouterResponse {
|
|
763
|
+
content: string;
|
|
764
|
+
model: string;
|
|
765
|
+
usage: {
|
|
766
|
+
promptTokens: number;
|
|
767
|
+
completionTokens: number;
|
|
768
|
+
totalTokens: number;
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
interface OpenRouterAPIResponse {
|
|
773
|
+
id: string;
|
|
774
|
+
model: string;
|
|
775
|
+
choices: Array<{
|
|
776
|
+
message: {
|
|
777
|
+
role: string;
|
|
778
|
+
content: string;
|
|
779
|
+
};
|
|
780
|
+
finish_reason: string;
|
|
781
|
+
}>;
|
|
782
|
+
usage: {
|
|
783
|
+
prompt_tokens: number;
|
|
784
|
+
completion_tokens: number;
|
|
785
|
+
total_tokens: number;
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export async function callOpenRouter(request: OpenRouterRequest): Promise<OpenRouterResponse> {
|
|
790
|
+
const {
|
|
791
|
+
apiKey,
|
|
792
|
+
model,
|
|
793
|
+
systemPrompt,
|
|
794
|
+
userMessage,
|
|
795
|
+
maxTokens = 1024,
|
|
796
|
+
temperature = 0.7,
|
|
797
|
+
} = request;
|
|
798
|
+
|
|
799
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
headers: {
|
|
802
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
803
|
+
'Content-Type': 'application/json',
|
|
804
|
+
'HTTP-Referer': 'https://x402stacks.xyz',
|
|
805
|
+
'X-Title': 'x402 AI Endpoint',
|
|
806
|
+
},
|
|
807
|
+
body: JSON.stringify({
|
|
808
|
+
model,
|
|
809
|
+
messages: [
|
|
810
|
+
{ role: 'system', content: systemPrompt },
|
|
811
|
+
{ role: 'user', content: userMessage },
|
|
812
|
+
],
|
|
813
|
+
max_tokens: maxTokens,
|
|
814
|
+
temperature,
|
|
815
|
+
}),
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (!response.ok) {
|
|
819
|
+
const error = await response.text();
|
|
820
|
+
throw new Error(\`OpenRouter API error: \${response.status} - \${error}\`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const data = (await response.json()) as OpenRouterAPIResponse;
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
content: data.choices[0]?.message?.content || '',
|
|
827
|
+
model: data.model,
|
|
828
|
+
usage: {
|
|
829
|
+
promptTokens: data.usage.prompt_tokens,
|
|
830
|
+
completionTokens: data.usage.completion_tokens,
|
|
831
|
+
totalTokens: data.usage.total_tokens,
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
`;
|
|
836
|
+
}
|
|
837
|
+
function getAIPackageJsonTemplate(projectName) {
|
|
838
|
+
return `{
|
|
839
|
+
"name": "${projectName}",
|
|
840
|
+
"version": "1.0.0",
|
|
841
|
+
"private": true,
|
|
842
|
+
"type": "module",
|
|
843
|
+
"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"
|
|
850
|
+
},
|
|
851
|
+
"dependencies": {
|
|
852
|
+
"hono": "^4.7.0"
|
|
853
|
+
},
|
|
854
|
+
"devDependencies": {
|
|
855
|
+
"@cloudflare/workers-types": "^4.20250109.0",
|
|
856
|
+
"typescript": "^5.7.0",
|
|
857
|
+
"wrangler": "^4.5.0"
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
`;
|
|
861
|
+
}
|
|
862
|
+
function getAIEnvExampleTemplate(recipientAddress) {
|
|
863
|
+
return `# Cloudflare credentials
|
|
864
|
+
CLOUDFLARE_API_TOKEN=your-api-token-here
|
|
865
|
+
CLOUDFLARE_ACCOUNT_ID=your-account-id-here
|
|
866
|
+
|
|
867
|
+
# x402 recipient address (set via wrangler secret)
|
|
868
|
+
# wrangler secret put RECIPIENT_ADDRESS
|
|
869
|
+
# Value: ${recipientAddress}
|
|
870
|
+
|
|
871
|
+
# OpenRouter API key (set via wrangler secret)
|
|
872
|
+
# Get your key at https://openrouter.ai/keys
|
|
873
|
+
# wrangler secret put OPENROUTER_API_KEY
|
|
874
|
+
`;
|
|
875
|
+
}
|
|
876
|
+
function getAIReadmeTemplate(projectName, endpoints, recipientAddress, defaultModel) {
|
|
877
|
+
const tokenList = [...new Set(endpoints.map((ep) => `- ${ep.tokenType}`))].join("\n");
|
|
878
|
+
const endpointDocs = generateAIEndpointDocs(endpoints);
|
|
879
|
+
return `# ${projectName}
|
|
880
|
+
|
|
881
|
+
x402-enabled AI API endpoints on Cloudflare Workers, powered by OpenRouter.
|
|
882
|
+
|
|
883
|
+
## AI Provider
|
|
884
|
+
|
|
885
|
+
This API uses [OpenRouter](https://openrouter.ai) to access AI models.
|
|
886
|
+
Default model: \`${defaultModel}\`
|
|
887
|
+
|
|
888
|
+
## Payment Tokens
|
|
889
|
+
|
|
890
|
+
This API accepts payments in:
|
|
891
|
+
${tokenList}
|
|
892
|
+
|
|
893
|
+
## Recipient Address
|
|
894
|
+
|
|
895
|
+
Payments are sent to: \`${recipientAddress}\`
|
|
896
|
+
|
|
897
|
+
## Endpoints
|
|
898
|
+
|
|
899
|
+
### GET /health
|
|
900
|
+
- **Description:** Health check endpoint
|
|
901
|
+
- **Cost:** Free
|
|
902
|
+
- **Payment Required:** No
|
|
903
|
+
|
|
904
|
+
${endpointDocs}
|
|
905
|
+
|
|
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}
|
|
925
|
+
|
|
926
|
+
# OpenRouter API key (get from https://openrouter.ai/keys)
|
|
927
|
+
npm run wrangler -- secret put OPENROUTER_API_KEY
|
|
928
|
+
\`\`\`
|
|
929
|
+
|
|
930
|
+
## Local Development
|
|
931
|
+
|
|
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
|
+
\`\`\`bash
|
|
940
|
+
npm run dev
|
|
941
|
+
\`\`\`
|
|
942
|
+
|
|
943
|
+
The server will start at http://localhost:8787
|
|
944
|
+
|
|
945
|
+
## Deploy
|
|
946
|
+
|
|
947
|
+
\`\`\`bash
|
|
948
|
+
# Dry run first
|
|
949
|
+
npm run deploy:dry
|
|
950
|
+
|
|
951
|
+
# Deploy to staging
|
|
952
|
+
npm run deploy
|
|
953
|
+
|
|
954
|
+
# Deploy to production
|
|
955
|
+
npm run deploy:production
|
|
956
|
+
\`\`\`
|
|
957
|
+
|
|
958
|
+
## Example Usage
|
|
959
|
+
|
|
960
|
+
\`\`\`bash
|
|
961
|
+
# Health check (free)
|
|
962
|
+
curl http://localhost:8787/health
|
|
963
|
+
|
|
964
|
+
# AI endpoint (returns 402 without payment)
|
|
965
|
+
curl -X POST http://localhost:8787${endpoints[0]?.path || "/api/chat"} \\
|
|
966
|
+
-H "Content-Type: application/json" \\
|
|
967
|
+
-d '{"prompt": "Hello, how are you?"}'
|
|
968
|
+
\`\`\`
|
|
969
|
+
|
|
970
|
+
## x402 Payment Flow
|
|
971
|
+
|
|
972
|
+
1. Client makes request without payment header
|
|
973
|
+
2. Server returns HTTP 402 with payment requirements
|
|
974
|
+
3. Client signs payment transaction (does NOT broadcast)
|
|
975
|
+
4. Client retries request with \`X-PAYMENT\` header containing signed tx
|
|
976
|
+
5. Server verifies and settles payment via facilitator
|
|
977
|
+
6. Server calls OpenRouter API and returns AI response
|
|
978
|
+
|
|
979
|
+
## OpenRouter Models
|
|
980
|
+
|
|
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
|
|
984
|
+
- \`openai/gpt-4o\` - OpenAI's latest
|
|
985
|
+
- \`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
|
|
988
|
+
|
|
989
|
+
See all models: https://openrouter.ai/models
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
Generated with @aibtc/mcp-server scaffold tool.
|
|
994
|
+
`;
|
|
995
|
+
}
|
|
996
|
+
// =============================================================================
|
|
997
|
+
// AI SCAFFOLD FUNCTION
|
|
998
|
+
// =============================================================================
|
|
999
|
+
export async function scaffoldAIProject(config) {
|
|
1000
|
+
const { outputDir, projectName, endpoints, recipientAddress, network, facilitatorUrl, defaultModel, } = config;
|
|
1001
|
+
// Validate output directory exists
|
|
1002
|
+
try {
|
|
1003
|
+
const stat = await fs.stat(outputDir);
|
|
1004
|
+
if (!stat.isDirectory()) {
|
|
1005
|
+
throw new Error(`Output path is not a directory: ${outputDir}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
if (error.code === "ENOENT") {
|
|
1010
|
+
throw new Error(`Output directory does not exist: ${outputDir}`);
|
|
1011
|
+
}
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
const projectPath = path.join(outputDir, projectName);
|
|
1015
|
+
const srcPath = path.join(projectPath, "src");
|
|
1016
|
+
// Create project directory structure
|
|
1017
|
+
await fs.mkdir(projectPath, { recursive: true });
|
|
1018
|
+
await fs.mkdir(srcPath, { recursive: true });
|
|
1019
|
+
const filesCreated = [];
|
|
1020
|
+
// Generate and write files
|
|
1021
|
+
const files = [
|
|
1022
|
+
{ name: "src/index.ts", content: getAIIndexTemplate(endpoints, defaultModel) },
|
|
1023
|
+
{ name: "src/x402-middleware.ts", content: getMiddlewareTemplate() },
|
|
1024
|
+
{ name: "src/openrouter.ts", content: getOpenRouterTemplate() },
|
|
1025
|
+
{ name: "wrangler.jsonc", content: getWranglerTemplate(projectName, network, facilitatorUrl) },
|
|
1026
|
+
{ name: "package.json", content: getAIPackageJsonTemplate(projectName) },
|
|
1027
|
+
{ name: "tsconfig.json", content: getTsconfigTemplate() },
|
|
1028
|
+
{ name: ".env.example", content: getAIEnvExampleTemplate(recipientAddress) },
|
|
1029
|
+
{ name: ".gitignore", content: getGitignoreTemplate() },
|
|
1030
|
+
{
|
|
1031
|
+
name: "README.md",
|
|
1032
|
+
content: getAIReadmeTemplate(projectName, endpoints, recipientAddress, defaultModel),
|
|
1033
|
+
},
|
|
1034
|
+
];
|
|
1035
|
+
for (const file of files) {
|
|
1036
|
+
const filePath = path.join(projectPath, file.name);
|
|
1037
|
+
await fs.writeFile(filePath, file.content, "utf-8");
|
|
1038
|
+
filesCreated.push(file.name);
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
projectPath,
|
|
1042
|
+
filesCreated,
|
|
1043
|
+
nextSteps: [
|
|
1044
|
+
`cd ${projectPath}`,
|
|
1045
|
+
"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",
|
|
1051
|
+
"npm run dev",
|
|
1052
|
+
],
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
//# sourceMappingURL=scaffold.service.js.map
|