x402-bazaar 1.2.1 → 2.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/bin/cli.js +47 -7
- package/package.json +2 -2
- package/src/commands/call.js +197 -0
- package/src/commands/init.js +70 -48
- package/src/commands/list.js +125 -0
- package/src/commands/search.js +120 -0
- package/src/commands/wallet.js +156 -0
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,10 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { initCommand } from '../src/commands/init.js';
|
|
5
5
|
import { configCommand } from '../src/commands/config.js';
|
|
6
6
|
import { statusCommand } from '../src/commands/status.js';
|
|
7
|
+
import { listCommand } from '../src/commands/list.js';
|
|
8
|
+
import { searchCommand } from '../src/commands/search.js';
|
|
9
|
+
import { callCommand } from '../src/commands/call.js';
|
|
10
|
+
import { walletCommand } from '../src/commands/wallet.js';
|
|
7
11
|
import chalk from 'chalk';
|
|
8
12
|
|
|
9
13
|
// Global error handler
|
|
@@ -21,7 +25,7 @@ const program = new Command();
|
|
|
21
25
|
program
|
|
22
26
|
.name('x402-bazaar')
|
|
23
27
|
.description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
|
|
24
|
-
.version('
|
|
28
|
+
.version('2.0.0');
|
|
25
29
|
|
|
26
30
|
program
|
|
27
31
|
.command('init')
|
|
@@ -49,17 +53,53 @@ program
|
|
|
49
53
|
.option('--server-url <url>', 'Server URL to check', 'https://x402-api.onrender.com')
|
|
50
54
|
.action(statusCommand);
|
|
51
55
|
|
|
56
|
+
program
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List all services on x402 Bazaar')
|
|
59
|
+
.option('--chain <chain>', 'Filter by chain (base or skale)')
|
|
60
|
+
.option('--category <category>', 'Filter by category (ai, data, weather, etc.)')
|
|
61
|
+
.option('--free', 'Show only free services')
|
|
62
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
63
|
+
.action(listCommand);
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('search <query>')
|
|
67
|
+
.description('Search for services by keyword')
|
|
68
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
69
|
+
.action(searchCommand);
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command('call <endpoint>')
|
|
73
|
+
.description('Call a marketplace endpoint (testing/debugging)')
|
|
74
|
+
.option('--param <key=value>', 'Add parameter (can be used multiple times)', (value, previous) => {
|
|
75
|
+
return previous ? [...previous, value] : [value];
|
|
76
|
+
}, [])
|
|
77
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
78
|
+
.action(callCommand);
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('wallet')
|
|
82
|
+
.description('Check USDC wallet balance on Base')
|
|
83
|
+
.option('--address <address>', 'Ethereum address to check')
|
|
84
|
+
.action(walletCommand);
|
|
85
|
+
|
|
52
86
|
// Default: show help if no command given
|
|
53
87
|
if (process.argv.length <= 2) {
|
|
54
88
|
console.log('');
|
|
55
|
-
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI'));
|
|
89
|
+
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v2'));
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' Setup commands:');
|
|
92
|
+
console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
|
|
93
|
+
console.log(chalk.cyan(' npx x402-bazaar config') + chalk.dim(' Generate MCP config'));
|
|
94
|
+
console.log(chalk.cyan(' npx x402-bazaar status') + chalk.dim(' Check server connection'));
|
|
56
95
|
console.log('');
|
|
57
|
-
console.log('
|
|
58
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
59
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
60
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
96
|
+
console.log(' Marketplace commands:');
|
|
97
|
+
console.log(chalk.cyan(' npx x402-bazaar list') + chalk.dim(' Browse all services'));
|
|
98
|
+
console.log(chalk.cyan(' npx x402-bazaar search <query>') + chalk.dim(' Find services by keyword'));
|
|
99
|
+
console.log(chalk.cyan(' npx x402-bazaar call <endpoint>') + chalk.dim(' Test an API endpoint'));
|
|
100
|
+
console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check wallet balance'));
|
|
61
101
|
console.log('');
|
|
62
|
-
console.log(chalk.dim(' Run with --help for
|
|
102
|
+
console.log(chalk.dim(' Run any command with --help for detailed options'));
|
|
63
103
|
console.log('');
|
|
64
104
|
process.exit(0);
|
|
65
105
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bazaar",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI to set up x402 Bazaar MCP server for AI agents. One command to connect your agent
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI to set up x402 Bazaar MCP server for AI agents. Browse, search, and call marketplace APIs. One command to connect your agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export async function callCommand(endpoint, options) {
|
|
6
|
+
if (!endpoint || endpoint.trim().length === 0) {
|
|
7
|
+
log.error('Endpoint is required');
|
|
8
|
+
log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
|
|
9
|
+
log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
|
|
10
|
+
log.dim(' Example: x402-bazaar call /api/search --param q="AI agents"');
|
|
11
|
+
console.log('');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
16
|
+
|
|
17
|
+
// Normalize endpoint (add leading slash if missing)
|
|
18
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
19
|
+
const fullUrl = `${serverUrl}${normalizedEndpoint}`;
|
|
20
|
+
|
|
21
|
+
log.banner();
|
|
22
|
+
log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Parse params
|
|
26
|
+
const params = {};
|
|
27
|
+
if (options.param) {
|
|
28
|
+
const paramArray = Array.isArray(options.param) ? options.param : [options.param];
|
|
29
|
+
|
|
30
|
+
for (const p of paramArray) {
|
|
31
|
+
const [key, ...valueParts] = p.split('=');
|
|
32
|
+
if (!key || valueParts.length === 0) {
|
|
33
|
+
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
let value = valueParts.join('='); // rejoin in case value contains '='
|
|
37
|
+
// Strip quotes if present
|
|
38
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
+
value = value.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
params[key.trim()] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Object.keys(params).length > 0) {
|
|
47
|
+
log.info('Parameters:');
|
|
48
|
+
for (const [k, v] of Object.entries(params)) {
|
|
49
|
+
log.dim(` ${k}: ${v}`);
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build URL with query params (always use GET for marketplace APIs)
|
|
55
|
+
let finalUrl = fullUrl;
|
|
56
|
+
if (Object.keys(params).length > 0) {
|
|
57
|
+
const queryString = new URLSearchParams(params).toString();
|
|
58
|
+
finalUrl = `${fullUrl}?${queryString}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const spinner = ora(`GET ${finalUrl}...`).start();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const fetchOptions = {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
},
|
|
69
|
+
signal: AbortSignal.timeout(30000),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const res = await fetch(finalUrl, fetchOptions);
|
|
73
|
+
|
|
74
|
+
spinner.stop();
|
|
75
|
+
|
|
76
|
+
// Handle 402 Payment Required
|
|
77
|
+
if (res.status === 402) {
|
|
78
|
+
console.log('');
|
|
79
|
+
log.warn(chalk.bold('Payment Required (HTTP 402)'));
|
|
80
|
+
console.log('');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const paymentInfo = await res.json();
|
|
84
|
+
|
|
85
|
+
if (paymentInfo.price) {
|
|
86
|
+
log.info(`Price: ${chalk.cyan(`${paymentInfo.price} USDC`)}`);
|
|
87
|
+
}
|
|
88
|
+
if (paymentInfo.paymentAddress) {
|
|
89
|
+
log.info(`Payment address: ${chalk.hex('#34D399')(paymentInfo.paymentAddress)}`);
|
|
90
|
+
}
|
|
91
|
+
if (paymentInfo.message) {
|
|
92
|
+
log.dim(` ${paymentInfo.message}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
log.separator();
|
|
97
|
+
console.log('');
|
|
98
|
+
log.info('To pay automatically, use the MCP server via Claude/Cursor.');
|
|
99
|
+
log.dim(' The MCP server handles x402 payments transparently.');
|
|
100
|
+
log.dim(' Install: npx x402-bazaar init');
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
} catch {
|
|
104
|
+
log.dim(' This endpoint requires payment.');
|
|
105
|
+
log.dim(' Use the MCP server for automatic payment handling.');
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle other errors
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
console.log('');
|
|
114
|
+
log.error(`HTTP ${res.status}: ${res.statusText}`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const errorBody = await res.text();
|
|
118
|
+
if (errorBody) {
|
|
119
|
+
console.log('');
|
|
120
|
+
log.dim('Response:');
|
|
121
|
+
console.log(chalk.red(errorBody));
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Ignore
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Success
|
|
132
|
+
console.log('');
|
|
133
|
+
log.success(`${chalk.bold(res.status)} ${res.statusText}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
const contentType = res.headers.get('content-type') || '';
|
|
137
|
+
|
|
138
|
+
if (contentType.includes('application/json')) {
|
|
139
|
+
const responseData = await res.json();
|
|
140
|
+
|
|
141
|
+
log.separator();
|
|
142
|
+
console.log('');
|
|
143
|
+
log.info('Response (JSON):');
|
|
144
|
+
console.log('');
|
|
145
|
+
|
|
146
|
+
// Pretty-print JSON with syntax highlighting
|
|
147
|
+
const jsonString = JSON.stringify(responseData, null, 2);
|
|
148
|
+
const highlighted = highlightJson(jsonString);
|
|
149
|
+
console.log(highlighted);
|
|
150
|
+
console.log('');
|
|
151
|
+
log.separator();
|
|
152
|
+
|
|
153
|
+
} else {
|
|
154
|
+
const responseText = await res.text();
|
|
155
|
+
|
|
156
|
+
log.separator();
|
|
157
|
+
console.log('');
|
|
158
|
+
log.info('Response:');
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(chalk.white(responseText));
|
|
161
|
+
console.log('');
|
|
162
|
+
log.separator();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
} catch (err) {
|
|
168
|
+
spinner.fail('Request failed');
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
if (err.name === 'AbortError') {
|
|
172
|
+
log.error('Request timeout (30s) — server may be slow or endpoint not responding');
|
|
173
|
+
log.dim(' Try again or check status: npx x402-bazaar status');
|
|
174
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
175
|
+
log.error('Cannot connect to server');
|
|
176
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
177
|
+
log.dim(' Check your internet connection or try: npx x402-bazaar status');
|
|
178
|
+
} else {
|
|
179
|
+
log.error(err.message);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Simple JSON syntax highlighting for terminal
|
|
189
|
+
*/
|
|
190
|
+
function highlightJson(jsonString) {
|
|
191
|
+
return jsonString
|
|
192
|
+
.replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
|
|
193
|
+
.replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
|
|
194
|
+
.replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
|
|
195
|
+
.replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
|
|
196
|
+
.replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
|
|
197
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -191,8 +191,8 @@ export async function initCommand(options) {
|
|
|
191
191
|
|
|
192
192
|
console.log('');
|
|
193
193
|
|
|
194
|
-
// ─── Step 3: Wallet Configuration
|
|
195
|
-
log.step(3, 'Configuring wallet...');
|
|
194
|
+
// ─── Step 3: Network & Wallet Configuration ────────────────────────
|
|
195
|
+
log.step(3, 'Configuring network and wallet...');
|
|
196
196
|
console.log('');
|
|
197
197
|
|
|
198
198
|
let walletMode = 'readonly';
|
|
@@ -207,6 +207,42 @@ export async function initCommand(options) {
|
|
|
207
207
|
log.info('Skipping wallet setup (--no-wallet)');
|
|
208
208
|
walletMode = 'readonly';
|
|
209
209
|
} else {
|
|
210
|
+
// Network & Budget first — needed for wallet funding instructions
|
|
211
|
+
const configOverrides = {};
|
|
212
|
+
if (options.network) configOverrides.network = options.network;
|
|
213
|
+
if (options.budget) configOverrides.maxBudget = options.budget;
|
|
214
|
+
|
|
215
|
+
const configAnswers = await promptOrDefault([
|
|
216
|
+
{
|
|
217
|
+
type: 'list',
|
|
218
|
+
name: 'network',
|
|
219
|
+
message: 'Which network?',
|
|
220
|
+
choices: [
|
|
221
|
+
{ name: 'Base Mainnet (real USDC)', value: 'mainnet' },
|
|
222
|
+
{ name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
|
|
223
|
+
{ name: 'SKALE Europa (zero gas fees — advanced users)', value: 'skale' },
|
|
224
|
+
],
|
|
225
|
+
default: 'mainnet',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'input',
|
|
229
|
+
name: 'maxBudget',
|
|
230
|
+
message: 'Max USDC budget per session (safety limit):',
|
|
231
|
+
default: '1.00',
|
|
232
|
+
validate: (v) => {
|
|
233
|
+
const n = parseFloat(v);
|
|
234
|
+
if (isNaN(n) || n <= 0) return 'Must be a positive number';
|
|
235
|
+
if (n > 100) return 'Maximum is 100 USDC per session';
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
], configOverrides);
|
|
240
|
+
|
|
241
|
+
network = configAnswers.network;
|
|
242
|
+
maxBudget = configAnswers.maxBudget;
|
|
243
|
+
|
|
244
|
+
console.log('');
|
|
245
|
+
|
|
210
246
|
const { mode } = await promptOrDefault([{
|
|
211
247
|
type: 'list',
|
|
212
248
|
name: 'mode',
|
|
@@ -247,19 +283,36 @@ export async function initCommand(options) {
|
|
|
247
283
|
).toString().trim();
|
|
248
284
|
console.log('');
|
|
249
285
|
log.info(`Wallet address: ${chalk.bold(walletAddress)}`);
|
|
250
|
-
|
|
286
|
+
if (network === 'skale') {
|
|
287
|
+
log.dim(` Explorer: https://elated-tan-skat.explorer.mainnet.skalenodes.com/address/${walletAddress}`);
|
|
288
|
+
} else {
|
|
289
|
+
log.dim(` BaseScan: https://basescan.org/address/${walletAddress}`);
|
|
290
|
+
}
|
|
251
291
|
console.log('');
|
|
252
292
|
log.separator();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
if (network === 'skale') {
|
|
294
|
+
log.info(chalk.bold('To activate payments, fund this wallet:'));
|
|
295
|
+
console.log('');
|
|
296
|
+
log.dim(` ${chalk.white('1.')} Bridge USDC to SKALE Europa`);
|
|
297
|
+
log.dim(` (Use https://portal.skale.space/bridge or any SKALE bridge)`);
|
|
298
|
+
log.dim(` ${chalk.white('2.')} Send ${chalk.bold('USDC')} to: ${chalk.hex('#34D399')(walletAddress)}`);
|
|
299
|
+
log.dim(` (Even $1 USDC is enough — each API call costs $0.005-$0.05)`);
|
|
300
|
+
log.dim(` ${chalk.white('3.')} Get free sFUEL for gas at https://www.sfuelstation.com/`);
|
|
301
|
+
log.dim(` (sFUEL is free — no ETH needed on SKALE!)`);
|
|
302
|
+
console.log('');
|
|
303
|
+
log.warn(`IMPORTANT: Send USDC on ${chalk.bold('SKALE Europa')} only — not Base or Ethereum!`);
|
|
304
|
+
} else {
|
|
305
|
+
log.info(chalk.bold('To activate payments, fund this wallet from MetaMask:'));
|
|
306
|
+
console.log('');
|
|
307
|
+
log.dim(` ${chalk.white('1.')} Open MetaMask and switch to the ${chalk.bold('Base')} network`);
|
|
308
|
+
log.dim(` (Chain ID: 8453 — add it via https://chainlist.org/chain/8453)`);
|
|
309
|
+
log.dim(` ${chalk.white('2.')} Send ${chalk.bold('USDC')} to: ${chalk.hex('#34D399')(walletAddress)}`);
|
|
310
|
+
log.dim(` (Even $1 USDC is enough to start — each API call costs $0.005-$0.05)`);
|
|
311
|
+
log.dim(` ${chalk.white('3.')} Send a tiny bit of ${chalk.bold('ETH')} to the same address for gas`);
|
|
312
|
+
log.dim(` (${chalk.white('~$0.01 of ETH on Base')} is enough for hundreds of transactions)`);
|
|
313
|
+
console.log('');
|
|
314
|
+
log.warn(`IMPORTANT: Send on the ${chalk.bold('Base')} network only — not Ethereum mainnet!`);
|
|
315
|
+
}
|
|
263
316
|
log.separator();
|
|
264
317
|
} catch {
|
|
265
318
|
log.info('Wallet address will be shown when you first use the MCP server.');
|
|
@@ -325,39 +378,6 @@ export async function initCommand(options) {
|
|
|
325
378
|
}
|
|
326
379
|
}
|
|
327
380
|
}
|
|
328
|
-
|
|
329
|
-
// Network & Budget — use CLI flags as overrides
|
|
330
|
-
const configOverrides = {};
|
|
331
|
-
if (options.network) configOverrides.network = options.network;
|
|
332
|
-
if (options.budget) configOverrides.maxBudget = options.budget;
|
|
333
|
-
|
|
334
|
-
const configAnswers = await promptOrDefault([
|
|
335
|
-
{
|
|
336
|
-
type: 'list',
|
|
337
|
-
name: 'network',
|
|
338
|
-
message: 'Which network?',
|
|
339
|
-
choices: [
|
|
340
|
-
{ name: 'Base Mainnet (real USDC)', value: 'mainnet' },
|
|
341
|
-
{ name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
|
|
342
|
-
],
|
|
343
|
-
default: 'mainnet',
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
type: 'input',
|
|
347
|
-
name: 'maxBudget',
|
|
348
|
-
message: 'Max USDC budget per session (safety limit):',
|
|
349
|
-
default: '1.00',
|
|
350
|
-
validate: (v) => {
|
|
351
|
-
const n = parseFloat(v);
|
|
352
|
-
if (isNaN(n) || n <= 0) return 'Must be a positive number';
|
|
353
|
-
if (n > 100) return 'Maximum is 100 USDC per session';
|
|
354
|
-
return true;
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
], configOverrides);
|
|
358
|
-
|
|
359
|
-
network = configAnswers.network;
|
|
360
|
-
maxBudget = configAnswers.maxBudget;
|
|
361
381
|
}
|
|
362
382
|
|
|
363
383
|
console.log('');
|
|
@@ -461,7 +481,7 @@ export async function initCommand(options) {
|
|
|
461
481
|
`Environment: ${targetEnv.label}`,
|
|
462
482
|
`Install dir: ${installDir}`,
|
|
463
483
|
`Server: ${serverUrl}`,
|
|
464
|
-
`Network: ${network === 'mainnet' ? 'Base Mainnet' : 'Base Sepolia'}`,
|
|
484
|
+
`Network: ${network === 'mainnet' ? 'Base Mainnet' : network === 'skale' ? 'SKALE Europa' : 'Base Sepolia'}`,
|
|
465
485
|
`Budget limit: ${maxBudget} USDC / session`,
|
|
466
486
|
`Wallet: ${walletLabel}`,
|
|
467
487
|
`Services: ${serviceCount > 0 ? serviceCount + ' available' : 'check with npx x402-bazaar status'}`,
|
|
@@ -470,7 +490,9 @@ export async function initCommand(options) {
|
|
|
470
490
|
'',
|
|
471
491
|
...(walletMode === 'generate' ? [
|
|
472
492
|
'Before your agent can pay for APIs:',
|
|
473
|
-
|
|
493
|
+
network === 'skale'
|
|
494
|
+
? ' 1. Bridge USDC to your wallet on SKALE Europa + get free sFUEL'
|
|
495
|
+
: ' 1. Send USDC + a little ETH to your wallet on Base',
|
|
474
496
|
' 2. Restart your IDE',
|
|
475
497
|
'',
|
|
476
498
|
] : []),
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
|
|
7
|
+
'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export async function listCommand(options) {
|
|
11
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
12
|
+
|
|
13
|
+
log.banner();
|
|
14
|
+
log.info(`Fetching services from ${chalk.bold('x402 Bazaar')}`);
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
// Build query params
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
if (options.chain) params.append('chain', options.chain);
|
|
20
|
+
if (options.category) params.append('category', options.category);
|
|
21
|
+
if (options.free) params.append('free', 'true');
|
|
22
|
+
|
|
23
|
+
const url = `${serverUrl}/api/services${params.toString() ? '?' + params.toString() : ''}`;
|
|
24
|
+
|
|
25
|
+
const spinner = ora('Loading services...').start();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
const services = data.services || data || [];
|
|
36
|
+
|
|
37
|
+
spinner.succeed(`Found ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
if (services.length === 0) {
|
|
41
|
+
log.warn('No services found with these filters.');
|
|
42
|
+
log.dim(' Try: x402-bazaar list --category ai');
|
|
43
|
+
console.log('');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Display services table
|
|
48
|
+
log.separator();
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
services.forEach((service, idx) => {
|
|
52
|
+
const name = service.name || service.title || 'Unnamed Service';
|
|
53
|
+
const price = parseFloat(service.price || 0);
|
|
54
|
+
const priceLabel = price === 0
|
|
55
|
+
? chalk.hex('#34D399').bold('FREE')
|
|
56
|
+
: chalk.cyan(`$${price.toFixed(3)} USDC`);
|
|
57
|
+
|
|
58
|
+
// Extract category from tags
|
|
59
|
+
let category = 'other';
|
|
60
|
+
if (service.tags && Array.isArray(service.tags)) {
|
|
61
|
+
const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
|
|
62
|
+
if (realCategories.length > 0) {
|
|
63
|
+
category = realCategories[0];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const chain = service.chain || 'base';
|
|
68
|
+
const chainLabel = chain === 'skale'
|
|
69
|
+
? chalk.hex('#9333EA')('SKALE')
|
|
70
|
+
: chalk.hex('#0052FF')('Base');
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
|
|
74
|
+
chalk.white.bold(name)
|
|
75
|
+
);
|
|
76
|
+
console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
|
|
77
|
+
|
|
78
|
+
if (service.description) {
|
|
79
|
+
const desc = service.description.length > 80
|
|
80
|
+
? service.description.substring(0, 77) + '...'
|
|
81
|
+
: service.description;
|
|
82
|
+
console.log(` ${chalk.hex('#6B7280')(desc)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (service.endpoint) {
|
|
86
|
+
console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
log.separator();
|
|
93
|
+
console.log('');
|
|
94
|
+
log.info(`Total: ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
|
|
95
|
+
|
|
96
|
+
// Show filter info
|
|
97
|
+
if (options.chain || options.category || options.free) {
|
|
98
|
+
console.log('');
|
|
99
|
+
log.dim(' Active filters:');
|
|
100
|
+
if (options.chain) log.dim(` Chain: ${options.chain}`);
|
|
101
|
+
if (options.category) log.dim(` Category: ${options.category}`);
|
|
102
|
+
if (options.free) log.dim(' Price: free only');
|
|
103
|
+
log.dim(' Run without options to see all services');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
} catch (err) {
|
|
109
|
+
spinner.fail('Failed to fetch services');
|
|
110
|
+
|
|
111
|
+
if (err.name === 'AbortError') {
|
|
112
|
+
log.error('Request timeout — server may be sleeping (Render free tier)');
|
|
113
|
+
log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
|
|
114
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
115
|
+
log.error('Cannot connect to server');
|
|
116
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
117
|
+
log.dim(' Check your internet connection or try: npx x402-bazaar status');
|
|
118
|
+
} else {
|
|
119
|
+
log.error(err.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
|
|
7
|
+
'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export async function searchCommand(query, options) {
|
|
11
|
+
if (!query || query.trim().length === 0) {
|
|
12
|
+
log.error('Search query is required');
|
|
13
|
+
log.dim(' Usage: x402-bazaar search <query>');
|
|
14
|
+
log.dim(' Example: x402-bazaar search "weather API"');
|
|
15
|
+
console.log('');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
20
|
+
|
|
21
|
+
log.banner();
|
|
22
|
+
log.info(`Searching for: ${chalk.bold(query)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
const url = `${serverUrl}/api/services?search=${encodeURIComponent(query)}`;
|
|
26
|
+
const spinner = ora('Searching...').start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
const services = data.services || data || [];
|
|
37
|
+
|
|
38
|
+
if (services.length === 0) {
|
|
39
|
+
spinner.fail('No results found');
|
|
40
|
+
console.log('');
|
|
41
|
+
log.warn(`No services match "${query}"`);
|
|
42
|
+
log.dim(' Tips:');
|
|
43
|
+
log.dim(' • Check your spelling');
|
|
44
|
+
log.dim(' • Try broader keywords (e.g., "AI" instead of "GPT-4")');
|
|
45
|
+
log.dim(' • Browse all services: x402-bazaar list');
|
|
46
|
+
console.log('');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
spinner.succeed(`Found ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
|
|
53
|
+
// Display results
|
|
54
|
+
log.separator();
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
services.forEach((service, idx) => {
|
|
58
|
+
const name = service.name || service.title || 'Unnamed Service';
|
|
59
|
+
const price = parseFloat(service.price || 0);
|
|
60
|
+
const priceLabel = price === 0
|
|
61
|
+
? chalk.hex('#34D399').bold('FREE')
|
|
62
|
+
: chalk.cyan(`$${price.toFixed(3)} USDC`);
|
|
63
|
+
|
|
64
|
+
// Extract category from tags
|
|
65
|
+
let category = 'other';
|
|
66
|
+
if (service.tags && Array.isArray(service.tags)) {
|
|
67
|
+
const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
|
|
68
|
+
if (realCategories.length > 0) {
|
|
69
|
+
category = realCategories[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chain = service.chain || 'base';
|
|
74
|
+
const chainLabel = chain === 'skale'
|
|
75
|
+
? chalk.hex('#9333EA')('SKALE')
|
|
76
|
+
: chalk.hex('#0052FF')('Base');
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
|
|
80
|
+
chalk.white.bold(name)
|
|
81
|
+
);
|
|
82
|
+
console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
|
|
83
|
+
|
|
84
|
+
if (service.description) {
|
|
85
|
+
const desc = service.description.length > 80
|
|
86
|
+
? service.description.substring(0, 77) + '...'
|
|
87
|
+
: service.description;
|
|
88
|
+
console.log(` ${chalk.hex('#6B7280')(desc)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (service.endpoint) {
|
|
92
|
+
console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
log.separator();
|
|
99
|
+
console.log('');
|
|
100
|
+
log.info(`Total: ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
spinner.fail('Search failed');
|
|
105
|
+
|
|
106
|
+
if (err.name === 'AbortError') {
|
|
107
|
+
log.error('Request timeout — server may be sleeping (Render free tier)');
|
|
108
|
+
log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
|
|
109
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
110
|
+
log.error('Cannot connect to server');
|
|
111
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
112
|
+
log.dim(' Check your internet connection or try: npx x402-bazaar status');
|
|
113
|
+
} else {
|
|
114
|
+
log.error(err.message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const BASE_RPC_URL = 'https://mainnet.base.org';
|
|
6
|
+
const USDC_CONTRACT_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
7
|
+
const BALANCE_OF_SELECTOR = '0x70a08231';
|
|
8
|
+
|
|
9
|
+
export async function walletCommand(options) {
|
|
10
|
+
log.banner();
|
|
11
|
+
|
|
12
|
+
if (!options.address) {
|
|
13
|
+
log.info('Check USDC balance for any wallet address on Base.');
|
|
14
|
+
console.log('');
|
|
15
|
+
log.dim(' Usage:');
|
|
16
|
+
log.dim(' x402-bazaar wallet --address 0xYourAddress');
|
|
17
|
+
console.log('');
|
|
18
|
+
log.dim(' Example:');
|
|
19
|
+
log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
|
|
20
|
+
console.log('');
|
|
21
|
+
log.dim(' To find your agent wallet address:');
|
|
22
|
+
log.dim(' 1. Run: npx x402-bazaar init');
|
|
23
|
+
log.dim(' 2. Or check your .env file: AGENT_PRIVATE_KEY');
|
|
24
|
+
log.dim(' 3. Or ask your AI agent: "What is my wallet address?"');
|
|
25
|
+
console.log('');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const address = options.address.trim();
|
|
30
|
+
|
|
31
|
+
// Validate address format
|
|
32
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
|
|
33
|
+
log.error('Invalid Ethereum address format');
|
|
34
|
+
log.dim(' Expected: 0x followed by 40 hexadecimal characters');
|
|
35
|
+
log.dim(` Got: ${address}`);
|
|
36
|
+
console.log('');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
log.info(`Checking wallet: ${chalk.bold(maskAddress(address))}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
const spinner = ora('Fetching USDC balance from Base...').start();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Encode balanceOf(address) call
|
|
47
|
+
// balanceOf selector: 0x70a08231
|
|
48
|
+
// Param: address (32 bytes, left-padded)
|
|
49
|
+
const paddedAddress = address.slice(2).padStart(64, '0');
|
|
50
|
+
const data = BALANCE_OF_SELECTOR + paddedAddress;
|
|
51
|
+
|
|
52
|
+
// Make RPC call
|
|
53
|
+
const rpcPayload = {
|
|
54
|
+
jsonrpc: '2.0',
|
|
55
|
+
id: 1,
|
|
56
|
+
method: 'eth_call',
|
|
57
|
+
params: [
|
|
58
|
+
{
|
|
59
|
+
to: USDC_CONTRACT_BASE,
|
|
60
|
+
data: data,
|
|
61
|
+
},
|
|
62
|
+
'latest',
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const res = await fetch(BASE_RPC_URL, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(rpcPayload),
|
|
70
|
+
signal: AbortSignal.timeout(15000),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rpcResponse = await res.json();
|
|
78
|
+
|
|
79
|
+
if (rpcResponse.error) {
|
|
80
|
+
throw new Error(rpcResponse.error.message || 'RPC error');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!rpcResponse.result) {
|
|
84
|
+
throw new Error('No result from RPC');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse balance (USDC has 6 decimals)
|
|
88
|
+
const balanceHex = rpcResponse.result;
|
|
89
|
+
const balanceRaw = BigInt(balanceHex);
|
|
90
|
+
const balanceUsdc = Number(balanceRaw) / 1_000_000;
|
|
91
|
+
|
|
92
|
+
spinner.succeed('Balance fetched');
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
log.separator();
|
|
96
|
+
console.log('');
|
|
97
|
+
|
|
98
|
+
log.info(`Address: ${chalk.hex('#34D399')(maskAddress(address))}`);
|
|
99
|
+
log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')} (Chain ID: 8453)`);
|
|
100
|
+
log.info(`Balance: ${chalk.cyan.bold(balanceUsdc.toFixed(6))} ${chalk.dim('USDC')}`);
|
|
101
|
+
|
|
102
|
+
console.log('');
|
|
103
|
+
log.separator();
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
// Show explorer link
|
|
107
|
+
log.dim(` Explorer: https://basescan.org/address/${address}`);
|
|
108
|
+
|
|
109
|
+
// Helpful tips based on balance
|
|
110
|
+
if (balanceUsdc === 0) {
|
|
111
|
+
console.log('');
|
|
112
|
+
log.warn('This wallet has no USDC.');
|
|
113
|
+
log.dim(' To fund it:');
|
|
114
|
+
log.dim(' 1. Open MetaMask and switch to Base network');
|
|
115
|
+
log.dim(' 2. Send USDC to this address');
|
|
116
|
+
log.dim(' 3. Send a tiny amount of ETH for gas (~$0.01)');
|
|
117
|
+
log.dim(' Get USDC: bridge from Ethereum or buy on Base DEX');
|
|
118
|
+
} else if (balanceUsdc < 0.1) {
|
|
119
|
+
console.log('');
|
|
120
|
+
log.warn('Low balance — consider adding more USDC.');
|
|
121
|
+
log.dim(' Most x402 Bazaar APIs cost $0.005-$0.05 per call.');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('');
|
|
124
|
+
log.success('Wallet is funded and ready!');
|
|
125
|
+
const estimatedCalls = Math.floor(balanceUsdc / 0.02);
|
|
126
|
+
log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.02/call)`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
} catch (err) {
|
|
132
|
+
spinner.fail('Failed to fetch balance');
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
if (err.name === 'AbortError') {
|
|
136
|
+
log.error('Request timeout — Base RPC may be slow');
|
|
137
|
+
log.dim(' Try again in a few seconds');
|
|
138
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
139
|
+
log.error('Cannot connect to Base RPC');
|
|
140
|
+
log.dim(' Check your internet connection');
|
|
141
|
+
} else {
|
|
142
|
+
log.error(err.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mask address for display: 0xabcd...1234
|
|
152
|
+
*/
|
|
153
|
+
function maskAddress(address) {
|
|
154
|
+
if (address.length < 12) return address;
|
|
155
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
156
|
+
}
|