x402-bazaar 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.
- package/bin/cli.js +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +79 -366
- package/src/generators/env-file.js +15 -6
- package/src/generators/mcp-config.js +7 -29
package/bin/cli.js
CHANGED
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
|
|
5
5
|
import { join, dirname, resolve } from 'path';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
9
10
|
import { isInteractive, promptOrDefault, printNonInteractiveHint } from '../utils/prompt.js';
|
|
@@ -126,8 +127,18 @@ export async function initCommand(options) {
|
|
|
126
127
|
copyFileSync(mcpSource, mcpServerDest);
|
|
127
128
|
spinner.text = 'Copied mcp-server.mjs from local project...';
|
|
128
129
|
} else {
|
|
129
|
-
spinner.text = '
|
|
130
|
-
|
|
130
|
+
spinner.text = 'Downloading MCP server from GitHub...';
|
|
131
|
+
try {
|
|
132
|
+
const dlRes = await fetch('https://raw.githubusercontent.com/Wintyx57/x402-backend/main/mcp-server.mjs');
|
|
133
|
+
if (!dlRes.ok) throw new Error(`HTTP ${dlRes.status}`);
|
|
134
|
+
writeFileSync(mcpServerDest, await dlRes.text());
|
|
135
|
+
} catch (dlErr) {
|
|
136
|
+
spinner.fail(`Could not download MCP server: ${dlErr.message}`);
|
|
137
|
+
log.error('Download the file manually from:');
|
|
138
|
+
log.dim(' https://github.com/Wintyx57/x402-backend/blob/main/mcp-server.mjs');
|
|
139
|
+
log.dim(` Save it to: ${mcpServerDest}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
// Create package.json for the MCP server runtime
|
|
@@ -135,14 +146,16 @@ export async function initCommand(options) {
|
|
|
135
146
|
if (!existsSync(pkgJsonPath)) {
|
|
136
147
|
writeFileSync(pkgJsonPath, JSON.stringify({
|
|
137
148
|
name: 'x402-bazaar-mcp',
|
|
138
|
-
version: '
|
|
149
|
+
version: '2.0.0',
|
|
139
150
|
type: 'module',
|
|
140
151
|
private: true,
|
|
141
152
|
dependencies: {
|
|
142
|
-
'
|
|
153
|
+
'viem': '^2.0.0',
|
|
143
154
|
'@modelcontextprotocol/sdk': '^1.26.0',
|
|
144
155
|
'dotenv': '^17.2.4',
|
|
145
156
|
'zod': '^4.3.6',
|
|
157
|
+
'ed2curve': '^0.3.0',
|
|
158
|
+
'@scure/bip32': '^1.6.0',
|
|
146
159
|
},
|
|
147
160
|
}, null, 2));
|
|
148
161
|
}
|
|
@@ -183,9 +196,9 @@ export async function initCommand(options) {
|
|
|
183
196
|
console.log('');
|
|
184
197
|
|
|
185
198
|
let walletMode = 'readonly';
|
|
199
|
+
let agentPrivateKey = '';
|
|
186
200
|
let coinbaseApiKey = '';
|
|
187
201
|
let coinbaseApiSecret = '';
|
|
188
|
-
let seedPath = '';
|
|
189
202
|
let maxBudget = '1.00';
|
|
190
203
|
let network = 'mainnet';
|
|
191
204
|
let serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
@@ -200,31 +213,79 @@ export async function initCommand(options) {
|
|
|
200
213
|
message: 'How do you want to configure payments?',
|
|
201
214
|
choices: [
|
|
202
215
|
{
|
|
203
|
-
name: `${chalk.bold('
|
|
204
|
-
value: '
|
|
216
|
+
name: `${chalk.bold('Generate a new wallet')} — Creates a fresh Ethereum wallet automatically (Recommended)`,
|
|
217
|
+
value: 'generate',
|
|
205
218
|
},
|
|
206
219
|
{
|
|
207
|
-
name: `${chalk.bold('
|
|
208
|
-
value: '
|
|
220
|
+
name: `${chalk.bold('Import private key')} — Use an existing Ethereum private key`,
|
|
221
|
+
value: 'import',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: `${chalk.bold('Coinbase API keys')} — Legacy: use Coinbase CDP seed file`,
|
|
225
|
+
value: 'coinbase',
|
|
209
226
|
},
|
|
210
227
|
{
|
|
211
228
|
name: `${chalk.bold('Read-only mode')} — Browse marketplace for free (no payments)`,
|
|
212
229
|
value: 'readonly',
|
|
213
230
|
},
|
|
214
231
|
],
|
|
215
|
-
default: '
|
|
232
|
+
default: 'generate',
|
|
216
233
|
}]);
|
|
217
234
|
|
|
218
235
|
walletMode = mode;
|
|
219
236
|
|
|
220
|
-
if (mode === '
|
|
237
|
+
if (mode === 'generate') {
|
|
238
|
+
agentPrivateKey = '0x' + randomBytes(32).toString('hex');
|
|
239
|
+
log.success('New wallet generated!');
|
|
240
|
+
|
|
241
|
+
// Derive address using viem (installed in step 2)
|
|
242
|
+
try {
|
|
243
|
+
const addr = execSync(
|
|
244
|
+
`node --input-type=module -e "import{privateKeyToAccount}from'viem/accounts';console.log(privateKeyToAccount('${agentPrivateKey}').address)"`,
|
|
245
|
+
{ cwd: installDir, stdio: 'pipe', timeout: 15000 }
|
|
246
|
+
).toString().trim();
|
|
247
|
+
log.info(`Wallet address: ${chalk.bold(addr)}`);
|
|
248
|
+
console.log('');
|
|
249
|
+
log.dim(' Fund this address with USDC on Base to start paying for APIs.');
|
|
250
|
+
log.dim(` View on BaseScan: https://basescan.org/address/${addr}`);
|
|
251
|
+
} catch {
|
|
252
|
+
log.info('Wallet address will be shown when you first use the MCP server.');
|
|
253
|
+
log.dim(' Use the get_wallet_balance tool to see your address.');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (mode === 'import') {
|
|
258
|
+
if (!isInteractive()) {
|
|
259
|
+
log.warn('Cannot enter private key in a non-interactive terminal.');
|
|
260
|
+
log.info('Set AGENT_PRIVATE_KEY in your .env file manually after setup.');
|
|
261
|
+
walletMode = 'readonly';
|
|
262
|
+
} else {
|
|
263
|
+
const { key } = await promptOrDefault([{
|
|
264
|
+
type: 'password',
|
|
265
|
+
name: 'key',
|
|
266
|
+
message: 'Ethereum private key (0x...):',
|
|
267
|
+
mask: '*',
|
|
268
|
+
validate: (v) => {
|
|
269
|
+
const trimmed = v.trim();
|
|
270
|
+
if (trimmed.length === 0) return 'Private key is required';
|
|
271
|
+
const hex = trimmed.startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
272
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return 'Invalid private key (expected 64 hex characters)';
|
|
273
|
+
return true;
|
|
274
|
+
},
|
|
275
|
+
}]);
|
|
276
|
+
agentPrivateKey = key.trim().startsWith('0x') ? key.trim() : `0x${key.trim()}`;
|
|
277
|
+
log.success('Private key imported.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (mode === 'coinbase') {
|
|
221
282
|
if (!isInteractive()) {
|
|
222
283
|
log.warn('Cannot enter API credentials in a non-interactive terminal.');
|
|
223
284
|
log.info('Falling back to read-only mode.');
|
|
224
|
-
log.dim(' To configure wallet, run in a standalone terminal:');
|
|
225
|
-
log.dim(' npx x402-bazaar init');
|
|
226
285
|
walletMode = 'readonly';
|
|
227
286
|
} else {
|
|
287
|
+
log.dim(' Legacy mode: requires Coinbase CDP API keys + agent-seed.json');
|
|
288
|
+
console.log('');
|
|
228
289
|
const walletAnswers = await promptOrDefault([
|
|
229
290
|
{
|
|
230
291
|
type: 'input',
|
|
@@ -243,43 +304,12 @@ export async function initCommand(options) {
|
|
|
243
304
|
coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
|
|
244
305
|
coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
|
|
245
306
|
|
|
246
|
-
// Check if agent-seed.json exists
|
|
247
307
|
const existingSeed = join(installDir, 'agent-seed.json');
|
|
248
308
|
if (!existsSync(existingSeed)) {
|
|
249
|
-
log.warn('No agent-seed.json found.
|
|
250
|
-
log.dim('
|
|
251
|
-
log.dim(' Or
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (mode === 'new') {
|
|
257
|
-
console.log('');
|
|
258
|
-
log.info('To get Coinbase API keys:');
|
|
259
|
-
console.log('');
|
|
260
|
-
log.dim(' 1. Go to https://portal.cdp.coinbase.com/');
|
|
261
|
-
log.dim(' 2. Create a project (free)');
|
|
262
|
-
log.dim(' 3. Go to "API Keys" and generate a key pair');
|
|
263
|
-
log.dim(' 4. Save both the API Key Name and the Private Key');
|
|
264
|
-
log.dim(' 5. Run npx x402-bazaar init again with your keys');
|
|
265
|
-
console.log('');
|
|
266
|
-
|
|
267
|
-
if (!isInteractive()) {
|
|
268
|
-
log.info('Continuing in read-only mode (non-interactive terminal).');
|
|
269
|
-
walletMode = 'readonly';
|
|
270
|
-
} else {
|
|
271
|
-
const { proceed } = await promptOrDefault([{
|
|
272
|
-
type: 'confirm',
|
|
273
|
-
name: 'proceed',
|
|
274
|
-
message: 'Continue in read-only mode for now?',
|
|
275
|
-
default: true,
|
|
276
|
-
}]);
|
|
277
|
-
|
|
278
|
-
if (!proceed) {
|
|
279
|
-
log.info('Run npx x402-bazaar init when you have your API keys.');
|
|
280
|
-
process.exit(0);
|
|
309
|
+
log.warn('No agent-seed.json found.');
|
|
310
|
+
log.dim(' Copy your existing agent-seed.json to: ' + installDir);
|
|
311
|
+
log.dim(' Or re-run init and choose "Generate a new wallet" instead.');
|
|
281
312
|
}
|
|
282
|
-
walletMode = 'readonly';
|
|
283
313
|
}
|
|
284
314
|
}
|
|
285
315
|
|
|
@@ -315,7 +345,6 @@ export async function initCommand(options) {
|
|
|
315
345
|
|
|
316
346
|
network = configAnswers.network;
|
|
317
347
|
maxBudget = configAnswers.maxBudget;
|
|
318
|
-
seedPath = join(installDir, 'agent-seed.json');
|
|
319
348
|
}
|
|
320
349
|
|
|
321
350
|
console.log('');
|
|
@@ -330,9 +359,9 @@ export async function initCommand(options) {
|
|
|
330
359
|
serverUrl,
|
|
331
360
|
maxBudget,
|
|
332
361
|
network,
|
|
362
|
+
agentPrivateKey,
|
|
333
363
|
coinbaseApiKey,
|
|
334
364
|
coinbaseApiSecret,
|
|
335
|
-
seedPath,
|
|
336
365
|
readOnly: walletMode === 'readonly',
|
|
337
366
|
});
|
|
338
367
|
|
|
@@ -342,9 +371,9 @@ export async function initCommand(options) {
|
|
|
342
371
|
serverUrl,
|
|
343
372
|
maxBudget,
|
|
344
373
|
network,
|
|
374
|
+
agentPrivateKey,
|
|
345
375
|
coinbaseApiKey,
|
|
346
376
|
coinbaseApiSecret,
|
|
347
|
-
seedPath,
|
|
348
377
|
});
|
|
349
378
|
const envPath = join(installDir, '.env');
|
|
350
379
|
writeFileSync(envPath, envContent);
|
|
@@ -487,319 +516,3 @@ function writeConfig(envInfo, newConfig) {
|
|
|
487
516
|
}
|
|
488
517
|
}
|
|
489
518
|
|
|
490
|
-
/**
|
|
491
|
-
* Generate a standalone MCP server file.
|
|
492
|
-
* Used as fallback when the local x402-bazaar project is not found.
|
|
493
|
-
*/
|
|
494
|
-
function generateMcpServerFile() {
|
|
495
|
-
return `import 'dotenv/config';
|
|
496
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
497
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
498
|
-
import { z } from 'zod';
|
|
499
|
-
import { createRequire } from 'module';
|
|
500
|
-
|
|
501
|
-
const require = createRequire(import.meta.url);
|
|
502
|
-
const { Coinbase, Wallet } = require('@coinbase/coinbase-sdk');
|
|
503
|
-
|
|
504
|
-
// ─── Config ──────────────────────────────────────────────────────────
|
|
505
|
-
const SERVER_URL = process.env.X402_SERVER_URL || 'https://x402-api.onrender.com';
|
|
506
|
-
const MAX_BUDGET = parseFloat(process.env.MAX_BUDGET_USDC || '1.00');
|
|
507
|
-
const NETWORK = process.env.NETWORK || 'mainnet';
|
|
508
|
-
const explorerBase = NETWORK === 'testnet'
|
|
509
|
-
? 'https://sepolia.basescan.org'
|
|
510
|
-
: 'https://basescan.org';
|
|
511
|
-
const networkLabel = NETWORK === 'testnet' ? 'Base Sepolia' : 'Base Mainnet';
|
|
512
|
-
|
|
513
|
-
// ─── Budget Tracking ─────────────────────────────────────────────────
|
|
514
|
-
let sessionSpending = 0;
|
|
515
|
-
const sessionPayments = [];
|
|
516
|
-
|
|
517
|
-
// ─── Wallet ──────────────────────────────────────────────────────────
|
|
518
|
-
let wallet = null;
|
|
519
|
-
let walletReady = false;
|
|
520
|
-
|
|
521
|
-
async function initWallet() {
|
|
522
|
-
if (walletReady) return;
|
|
523
|
-
|
|
524
|
-
Coinbase.configure({
|
|
525
|
-
apiKeyName: process.env.COINBASE_API_KEY,
|
|
526
|
-
privateKey: process.env.COINBASE_API_SECRET,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
const seedPath = process.env.AGENT_SEED_PATH || 'agent-seed.json';
|
|
530
|
-
const fs = await import('fs');
|
|
531
|
-
const seedData = JSON.parse(fs.readFileSync(seedPath, 'utf-8'));
|
|
532
|
-
const seedWalletId = Object.keys(seedData)[0];
|
|
533
|
-
|
|
534
|
-
wallet = await Wallet.fetch(seedWalletId);
|
|
535
|
-
await wallet.loadSeed(seedPath);
|
|
536
|
-
walletReady = true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// ─── x402 Payment Flow ──────────────────────────────────────────────
|
|
540
|
-
async function payAndRequest(url, options = {}) {
|
|
541
|
-
const res = await fetch(url, options);
|
|
542
|
-
const body = await res.json();
|
|
543
|
-
|
|
544
|
-
if (res.status !== 402) {
|
|
545
|
-
return body;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// HTTP 402 — Payment Required
|
|
549
|
-
const details = body.payment_details;
|
|
550
|
-
const cost = parseFloat(details.amount);
|
|
551
|
-
|
|
552
|
-
// Budget check
|
|
553
|
-
if (sessionSpending + cost > MAX_BUDGET) {
|
|
554
|
-
throw new Error(
|
|
555
|
-
\`Budget limit reached. Spent: \${sessionSpending.toFixed(2)} USDC / \${MAX_BUDGET.toFixed(2)} USDC limit. \` +
|
|
556
|
-
\`This call costs \${cost} USDC. Increase MAX_BUDGET_USDC env var to allow more spending.\`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
await initWallet();
|
|
561
|
-
|
|
562
|
-
const transfer = await wallet.createTransfer({
|
|
563
|
-
amount: details.amount,
|
|
564
|
-
assetId: Coinbase.assets.Usdc,
|
|
565
|
-
destination: details.recipient,
|
|
566
|
-
});
|
|
567
|
-
const confirmed = await transfer.wait({ timeoutSeconds: 120 });
|
|
568
|
-
const txHash = confirmed.getTransactionHash();
|
|
569
|
-
|
|
570
|
-
// Track spending
|
|
571
|
-
sessionSpending += cost;
|
|
572
|
-
sessionPayments.push({
|
|
573
|
-
amount: cost,
|
|
574
|
-
txHash,
|
|
575
|
-
timestamp: new Date().toISOString(),
|
|
576
|
-
endpoint: url.replace(SERVER_URL, ''),
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// Retry with payment proof
|
|
580
|
-
const retryHeaders = { ...options.headers, 'X-Payment-TxHash': txHash };
|
|
581
|
-
const retryRes = await fetch(url, { ...options, headers: retryHeaders });
|
|
582
|
-
const result = await retryRes.json();
|
|
583
|
-
|
|
584
|
-
// Enrich result with payment info
|
|
585
|
-
result._payment = {
|
|
586
|
-
amount: details.amount,
|
|
587
|
-
currency: 'USDC',
|
|
588
|
-
txHash,
|
|
589
|
-
explorer: \`\${explorerBase}/tx/\${txHash}\`,
|
|
590
|
-
session_spent: sessionSpending.toFixed(2),
|
|
591
|
-
session_remaining: (MAX_BUDGET - sessionSpending).toFixed(2),
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
return result;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// ─── MCP Server ─────────────────────────────────────────────────────
|
|
598
|
-
const server = new McpServer({
|
|
599
|
-
name: 'x402-bazaar',
|
|
600
|
-
version: '1.1.0',
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// --- Tool: discover_marketplace (FREE) ---
|
|
604
|
-
server.tool(
|
|
605
|
-
'discover_marketplace',
|
|
606
|
-
'Discover the x402 Bazaar marketplace. Returns available endpoints, total services, and protocol info. Free — no payment needed.',
|
|
607
|
-
{},
|
|
608
|
-
async () => {
|
|
609
|
-
try {
|
|
610
|
-
const res = await fetch(SERVER_URL);
|
|
611
|
-
const data = await res.json();
|
|
612
|
-
return {
|
|
613
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
614
|
-
};
|
|
615
|
-
} catch (err) {
|
|
616
|
-
return {
|
|
617
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
618
|
-
isError: true,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
// --- Tool: search_services (0.05 USDC) ---
|
|
625
|
-
server.tool(
|
|
626
|
-
'search_services',
|
|
627
|
-
\`Search for API services on x402 Bazaar by keyword. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session. Check get_budget_status before calling if unsure about remaining budget.\`,
|
|
628
|
-
{ query: z.string().describe('Search keyword (e.g. "weather", "crypto", "ai")') },
|
|
629
|
-
async ({ query }) => {
|
|
630
|
-
try {
|
|
631
|
-
const result = await payAndRequest(
|
|
632
|
-
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
633
|
-
);
|
|
634
|
-
return {
|
|
635
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
636
|
-
};
|
|
637
|
-
} catch (err) {
|
|
638
|
-
return {
|
|
639
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
640
|
-
isError: true,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
// --- Tool: list_services (0.05 USDC) ---
|
|
647
|
-
server.tool(
|
|
648
|
-
'list_services',
|
|
649
|
-
\`List all API services available on x402 Bazaar. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
650
|
-
{},
|
|
651
|
-
async () => {
|
|
652
|
-
try {
|
|
653
|
-
const result = await payAndRequest(\`\${SERVER_URL}/services\`);
|
|
654
|
-
return {
|
|
655
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
656
|
-
};
|
|
657
|
-
} catch (err) {
|
|
658
|
-
return {
|
|
659
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
660
|
-
isError: true,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
// --- Tool: find_tool_for_task (0.05 USDC — smart service lookup) ---
|
|
667
|
-
server.tool(
|
|
668
|
-
'find_tool_for_task',
|
|
669
|
-
\`Describe what you need in plain English and get the best matching API service ready to call. Returns the single best match with name, URL, price, and usage instructions. Costs 0.05 USDC. Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
670
|
-
{ task: z.string().describe('What you need, in natural language (e.g. "get current weather for a city", "translate text to French")') },
|
|
671
|
-
async ({ task }) => {
|
|
672
|
-
try {
|
|
673
|
-
const stopWords = new Set(['i', 'need', 'want', 'to', 'a', 'an', 'the', 'for', 'of', 'and', 'or', 'in', 'on', 'with', 'that', 'this', 'get', 'find', 'me', 'my', 'some', 'can', 'you', 'do', 'is', 'it', 'be', 'have', 'use', 'please', 'should', 'would', 'could']);
|
|
674
|
-
const keywords = task.toLowerCase()
|
|
675
|
-
.replace(/[^a-z0-9\\s]/g, '')
|
|
676
|
-
.split(/\\s+/)
|
|
677
|
-
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
678
|
-
const query = keywords.slice(0, 3).join(' ') || task.slice(0, 30);
|
|
679
|
-
|
|
680
|
-
const result = await payAndRequest(
|
|
681
|
-
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
682
|
-
);
|
|
683
|
-
|
|
684
|
-
const services = result.data || result.services || [];
|
|
685
|
-
if (services.length === 0) {
|
|
686
|
-
return {
|
|
687
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
688
|
-
found: false,
|
|
689
|
-
query_used: query,
|
|
690
|
-
message: \`No services found matching "\${task}". Try rephrasing or use search_services with different keywords.\`,
|
|
691
|
-
_payment: result._payment,
|
|
692
|
-
}, null, 2) }],
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const best = services[0];
|
|
697
|
-
return {
|
|
698
|
-
content: [{ type: 'text', text: JSON.stringify({
|
|
699
|
-
found: true,
|
|
700
|
-
query_used: query,
|
|
701
|
-
service: {
|
|
702
|
-
name: best.name,
|
|
703
|
-
description: best.description,
|
|
704
|
-
url: best.url,
|
|
705
|
-
price_usdc: best.price_usdc,
|
|
706
|
-
tags: best.tags,
|
|
707
|
-
},
|
|
708
|
-
action: \`Call this API using call_api("\${best.url}"). \${Number(best.price_usdc) === 0 ? 'This API is free.' : \`This API costs \${best.price_usdc} USDC per call.\`}\`,
|
|
709
|
-
alternatives_count: services.length - 1,
|
|
710
|
-
_payment: result._payment,
|
|
711
|
-
}, null, 2) }],
|
|
712
|
-
};
|
|
713
|
-
} catch (err) {
|
|
714
|
-
return {
|
|
715
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
716
|
-
isError: true,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
);
|
|
721
|
-
|
|
722
|
-
// --- Tool: call_api (FREE — calls external APIs) ---
|
|
723
|
-
server.tool(
|
|
724
|
-
'call_api',
|
|
725
|
-
'Call an external API URL and return the response. Use this to fetch real data from service URLs discovered on the marketplace. Free — no marketplace payment needed.',
|
|
726
|
-
{ url: z.string().url().describe('The full API URL to call') },
|
|
727
|
-
async ({ url }) => {
|
|
728
|
-
try {
|
|
729
|
-
const res = await fetch(url);
|
|
730
|
-
const text = await res.text();
|
|
731
|
-
let parsed;
|
|
732
|
-
try {
|
|
733
|
-
parsed = JSON.parse(text);
|
|
734
|
-
} catch {
|
|
735
|
-
parsed = { response: text.slice(0, 5000) };
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
739
|
-
};
|
|
740
|
-
} catch (err) {
|
|
741
|
-
return {
|
|
742
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
743
|
-
isError: true,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
);
|
|
748
|
-
|
|
749
|
-
// --- Tool: get_wallet_balance (FREE) ---
|
|
750
|
-
server.tool(
|
|
751
|
-
'get_wallet_balance',
|
|
752
|
-
'Check the USDC balance of the agent wallet on-chain. Free.',
|
|
753
|
-
{},
|
|
754
|
-
async () => {
|
|
755
|
-
try {
|
|
756
|
-
await initWallet();
|
|
757
|
-
const balance = await wallet.getBalance(Coinbase.assets.Usdc);
|
|
758
|
-
const address = (await wallet.getDefaultAddress()).getId();
|
|
759
|
-
return {
|
|
760
|
-
content: [{
|
|
761
|
-
type: 'text',
|
|
762
|
-
text: JSON.stringify({
|
|
763
|
-
address,
|
|
764
|
-
balance_usdc: balance.toString(),
|
|
765
|
-
network: networkLabel,
|
|
766
|
-
explorer: \`\${explorerBase}/address/\${address}\`,
|
|
767
|
-
}, null, 2),
|
|
768
|
-
}],
|
|
769
|
-
};
|
|
770
|
-
} catch (err) {
|
|
771
|
-
return {
|
|
772
|
-
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
773
|
-
isError: true,
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
// --- Tool: get_budget_status (FREE) ---
|
|
780
|
-
server.tool(
|
|
781
|
-
'get_budget_status',
|
|
782
|
-
'Check the session spending budget. Shows how much USDC has been spent, remaining budget, and a list of all payments made this session. Free — call this before paid requests to verify budget.',
|
|
783
|
-
{},
|
|
784
|
-
async () => {
|
|
785
|
-
return {
|
|
786
|
-
content: [{
|
|
787
|
-
type: 'text',
|
|
788
|
-
text: JSON.stringify({
|
|
789
|
-
budget_limit: MAX_BUDGET.toFixed(2) + ' USDC',
|
|
790
|
-
spent: sessionSpending.toFixed(2) + ' USDC',
|
|
791
|
-
remaining: (MAX_BUDGET - sessionSpending).toFixed(2) + ' USDC',
|
|
792
|
-
payments_count: sessionPayments.length,
|
|
793
|
-
payments: sessionPayments,
|
|
794
|
-
network: networkLabel,
|
|
795
|
-
}, null, 2),
|
|
796
|
-
}],
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
// ─── Start ──────────────────────────────────────────────────────────
|
|
802
|
-
const transport = new StdioServerTransport();
|
|
803
|
-
await server.connect(transport);
|
|
804
|
-
`;
|
|
805
|
-
}
|
|
@@ -5,11 +5,11 @@ export function generateEnvContent({
|
|
|
5
5
|
serverUrl = 'https://x402-api.onrender.com',
|
|
6
6
|
maxBudget = '1.00',
|
|
7
7
|
network = 'mainnet',
|
|
8
|
+
agentPrivateKey = '',
|
|
8
9
|
coinbaseApiKey = '',
|
|
9
10
|
coinbaseApiSecret = '',
|
|
10
|
-
seedPath = 'agent-seed.json',
|
|
11
11
|
}) {
|
|
12
|
-
|
|
12
|
+
let content = `# x402 Bazaar MCP Server Configuration
|
|
13
13
|
# Generated by: npx x402-bazaar init
|
|
14
14
|
|
|
15
15
|
# Marketplace server URL
|
|
@@ -20,13 +20,22 @@ MAX_BUDGET_USDC=${maxBudget}
|
|
|
20
20
|
|
|
21
21
|
# Network: mainnet or testnet
|
|
22
22
|
NETWORK=${network}
|
|
23
|
+
`;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
if (agentPrivateKey) {
|
|
26
|
+
content += `
|
|
27
|
+
# Agent wallet private key (Ethereum)
|
|
28
|
+
# WARNING: Keep this secret! Never commit to git.
|
|
29
|
+
AGENT_PRIVATE_KEY=${agentPrivateKey}
|
|
30
|
+
`;
|
|
31
|
+
} else if (coinbaseApiKey) {
|
|
32
|
+
content += `
|
|
33
|
+
# Coinbase Developer Platform API credentials (legacy mode)
|
|
25
34
|
# Get yours at: https://portal.cdp.coinbase.com/
|
|
26
35
|
COINBASE_API_KEY=${coinbaseApiKey}
|
|
27
36
|
COINBASE_API_SECRET=${coinbaseApiSecret}
|
|
28
|
-
|
|
29
|
-
# Path to the agent wallet seed file
|
|
30
|
-
AGENT_SEED_PATH=${seedPath}
|
|
31
37
|
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return content;
|
|
32
41
|
}
|
|
@@ -10,16 +10,12 @@ export function generateMcpConfig({
|
|
|
10
10
|
serverUrl = 'https://x402-api.onrender.com',
|
|
11
11
|
maxBudget = '1.00',
|
|
12
12
|
network = 'mainnet',
|
|
13
|
+
agentPrivateKey = '',
|
|
13
14
|
coinbaseApiKey = '',
|
|
14
15
|
coinbaseApiSecret = '',
|
|
15
|
-
seedPath = '',
|
|
16
16
|
readOnly = false,
|
|
17
17
|
}) {
|
|
18
|
-
const os = platform();
|
|
19
|
-
const sep = os === 'win32' ? '\\' : '/';
|
|
20
|
-
|
|
21
18
|
const mcpServerPath = join(installDir, 'mcp-server.mjs');
|
|
22
|
-
const defaultSeedPath = seedPath || join(installDir, 'agent-seed.json');
|
|
23
19
|
|
|
24
20
|
const env = {
|
|
25
21
|
X402_SERVER_URL: serverUrl,
|
|
@@ -28,9 +24,12 @@ export function generateMcpConfig({
|
|
|
28
24
|
};
|
|
29
25
|
|
|
30
26
|
if (!readOnly) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
if (agentPrivateKey) {
|
|
28
|
+
env.AGENT_PRIVATE_KEY = agentPrivateKey;
|
|
29
|
+
} else if (coinbaseApiKey) {
|
|
30
|
+
env.COINBASE_API_KEY = coinbaseApiKey;
|
|
31
|
+
env.COINBASE_API_SECRET = coinbaseApiSecret;
|
|
32
|
+
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
const serverEntry = {
|
|
@@ -41,27 +40,6 @@ export function generateMcpConfig({
|
|
|
41
40
|
|
|
42
41
|
// Format for the target environment
|
|
43
42
|
switch (environment) {
|
|
44
|
-
case 'claude-desktop':
|
|
45
|
-
return {
|
|
46
|
-
mcpServers: {
|
|
47
|
-
'x402-bazaar': serverEntry,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
case 'cursor':
|
|
52
|
-
return {
|
|
53
|
-
mcpServers: {
|
|
54
|
-
'x402-bazaar': serverEntry,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
case 'claude-code':
|
|
59
|
-
return {
|
|
60
|
-
mcpServers: {
|
|
61
|
-
'x402-bazaar': serverEntry,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
43
|
case 'vscode-continue':
|
|
66
44
|
return {
|
|
67
45
|
models: [],
|