x402-bazaar 2.0.0 → 3.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 CHANGED
@@ -25,7 +25,7 @@ const program = new Command();
25
25
  program
26
26
  .name('x402-bazaar')
27
27
  .description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
28
- .version('2.0.0');
28
+ .version('3.0.0');
29
29
 
30
30
  program
31
31
  .command('init')
@@ -70,23 +70,25 @@ program
70
70
 
71
71
  program
72
72
  .command('call <endpoint>')
73
- .description('Call a marketplace endpoint (testing/debugging)')
73
+ .description('Call a marketplace endpoint with automatic x402 payment')
74
74
  .option('--param <key=value>', 'Add parameter (can be used multiple times)', (value, previous) => {
75
75
  return previous ? [...previous, value] : [value];
76
76
  }, [])
77
+ .option('--key <privateKey>', 'Private key for auto-payment (or set X402_PRIVATE_KEY env)')
77
78
  .option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
78
79
  .action(callCommand);
79
80
 
80
81
  program
81
82
  .command('wallet')
82
- .description('Check USDC wallet balance on Base')
83
+ .description('Check USDC wallet balance or generate a new wallet')
83
84
  .option('--address <address>', 'Ethereum address to check')
85
+ .option('--setup', 'Generate a new wallet for auto-payment')
84
86
  .action(walletCommand);
85
87
 
86
88
  // Default: show help if no command given
87
89
  if (process.argv.length <= 2) {
88
90
  console.log('');
89
- console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v2'));
91
+ console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v3'));
90
92
  console.log('');
91
93
  console.log(' Setup commands:');
92
94
  console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
@@ -96,8 +98,8 @@ if (process.argv.length <= 2) {
96
98
  console.log(' Marketplace commands:');
97
99
  console.log(chalk.cyan(' npx x402-bazaar list') + chalk.dim(' Browse all services'));
98
100
  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'));
101
+ console.log(chalk.cyan(' npx x402-bazaar call <endpoint>') + chalk.dim(' Call API with auto-payment'));
102
+ console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check balance / setup wallet'));
101
103
  console.log('');
102
104
  console.log(chalk.dim(' Run any command with --help for detailed options'));
103
105
  console.log('');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "x402-bazaar",
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.",
3
+ "version": "3.0.0",
4
+ "description": "CLI for x402 Bazaar AI Agent Marketplace. Browse, call, and auto-pay APIs with USDC on Base. One command to connect your agent.",
5
5
  "type": "module",
6
6
  "main": "bin/cli.js",
7
7
  "bin": {
@@ -44,6 +44,7 @@
44
44
  "chalk": "^5.3.0",
45
45
  "commander": "^12.1.0",
46
46
  "inquirer": "^9.3.0",
47
- "ora": "^8.0.1"
47
+ "ora": "^8.0.1",
48
+ "viem": "^2.45.3"
48
49
  }
49
50
  }
@@ -1,5 +1,7 @@
1
1
  import ora from 'ora';
2
2
  import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
3
5
  import { log } from '../utils/logger.js';
4
6
 
5
7
  export async function callCommand(endpoint, options) {
@@ -7,14 +9,12 @@ export async function callCommand(endpoint, options) {
7
9
  log.error('Endpoint is required');
8
10
  log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
9
11
  log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
10
- log.dim(' Example: x402-bazaar call /api/search --param q="AI agents"');
12
+ log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
11
13
  console.log('');
12
14
  process.exit(1);
13
15
  }
14
16
 
15
17
  const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
16
-
17
- // Normalize endpoint (add leading slash if missing)
18
18
  const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
19
19
  const fullUrl = `${serverUrl}${normalizedEndpoint}`;
20
20
 
@@ -26,15 +26,13 @@ export async function callCommand(endpoint, options) {
26
26
  const params = {};
27
27
  if (options.param) {
28
28
  const paramArray = Array.isArray(options.param) ? options.param : [options.param];
29
-
30
29
  for (const p of paramArray) {
31
30
  const [key, ...valueParts] = p.split('=');
32
31
  if (!key || valueParts.length === 0) {
33
32
  log.warn(`Invalid param format: ${p} (expected key=value)`);
34
33
  continue;
35
34
  }
36
- let value = valueParts.join('='); // rejoin in case value contains '='
37
- // Strip quotes if present
35
+ let value = valueParts.join('=');
38
36
  if ((value.startsWith('"') && value.endsWith('"')) ||
39
37
  (value.startsWith("'") && value.endsWith("'"))) {
40
38
  value = value.slice(1, -1);
@@ -51,7 +49,21 @@ export async function callCommand(endpoint, options) {
51
49
  console.log('');
52
50
  }
53
51
 
54
- // Build URL with query params (always use GET for marketplace APIs)
52
+ // Resolve private key for auto-payment
53
+ const privateKey = resolvePrivateKey(options);
54
+ const autoPay = !!privateKey;
55
+
56
+ if (autoPay) {
57
+ log.info(`Auto-payment: ${chalk.hex('#34D399').bold('enabled')}`);
58
+ try {
59
+ const { getAddressFromKey } = await import('../lib/payment.js');
60
+ const address = getAddressFromKey(privateKey);
61
+ log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
62
+ } catch { /* ignore display errors */ }
63
+ console.log('');
64
+ }
65
+
66
+ // Build URL with query params
55
67
  let finalUrl = fullUrl;
56
68
  if (Object.keys(params).length > 0) {
57
69
  const queryString = new URLSearchParams(params).toString();
@@ -63,14 +75,11 @@ export async function callCommand(endpoint, options) {
63
75
  try {
64
76
  const fetchOptions = {
65
77
  method: 'GET',
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- },
78
+ headers: { 'Content-Type': 'application/json' },
69
79
  signal: AbortSignal.timeout(30000),
70
80
  };
71
81
 
72
82
  const res = await fetch(finalUrl, fetchOptions);
73
-
74
83
  spinner.stop();
75
84
 
76
85
  // Handle 402 Payment Required
@@ -79,32 +88,49 @@ export async function callCommand(endpoint, options) {
79
88
  log.warn(chalk.bold('Payment Required (HTTP 402)'));
80
89
  console.log('');
81
90
 
91
+ let paymentInfo;
82
92
  try {
83
- const paymentInfo = await res.json();
93
+ paymentInfo = await res.json();
94
+ } catch {
95
+ log.error('Could not parse payment details');
96
+ process.exit(1);
97
+ }
84
98
 
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
- }
99
+ const price = paymentInfo.payment_details?.amount || paymentInfo.price;
100
+ const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
94
101
 
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
+ if (price) {
103
+ log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
104
+ }
105
+ if (payTo) {
106
+ log.dim(` Pay to: ${payTo}`);
107
+ }
108
+ console.log('');
102
109
 
103
- } catch {
104
- log.dim(' This endpoint requires payment.');
105
- log.dim(' Use the MCP server for automatic payment handling.');
106
- console.log('');
110
+ // Auto-pay if key is available
111
+ if (autoPay && price && payTo) {
112
+ await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
113
+ return;
107
114
  }
115
+
116
+ // No auto-pay — show instructions
117
+ log.separator();
118
+ console.log('');
119
+ log.info('To pay automatically, provide your private key:');
120
+ console.log('');
121
+ log.dim(' Option 1: Environment variable');
122
+ log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
123
+ log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
124
+ console.log('');
125
+ log.dim(' Option 2: --key flag');
126
+ log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key 0xYourKey');
127
+ console.log('');
128
+ log.dim(' Option 3: Generate a new wallet');
129
+ log.dim(' npx x402-bazaar wallet --setup');
130
+ console.log('');
131
+ log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
132
+ log.dim(' npx x402-bazaar init');
133
+ console.log('');
108
134
  return;
109
135
  }
110
136
 
@@ -112,7 +138,6 @@ export async function callCommand(endpoint, options) {
112
138
  if (!res.ok) {
113
139
  console.log('');
114
140
  log.error(`HTTP ${res.status}: ${res.statusText}`);
115
-
116
141
  try {
117
142
  const errorBody = await res.text();
118
143
  if (errorBody) {
@@ -120,61 +145,115 @@ export async function callCommand(endpoint, options) {
120
145
  log.dim('Response:');
121
146
  console.log(chalk.red(errorBody));
122
147
  }
123
- } catch {
124
- // Ignore
125
- }
126
-
148
+ } catch { /* ignore */ }
127
149
  console.log('');
128
150
  process.exit(1);
129
151
  }
130
152
 
131
153
  // Success
154
+ await displayResponse(res);
155
+
156
+ } catch (err) {
157
+ spinner.fail('Request failed');
132
158
  console.log('');
133
- log.success(`${chalk.bold(res.status)} ${res.statusText}`);
159
+
160
+ if (err.name === 'AbortError') {
161
+ log.error('Request timeout (30s)');
162
+ log.dim(' Try again or check status: npx x402-bazaar status');
163
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
164
+ log.error('Cannot connect to server');
165
+ log.dim(` Server URL: ${serverUrl}`);
166
+ } else {
167
+ log.error(err.message);
168
+ }
169
+
134
170
  console.log('');
171
+ process.exit(1);
172
+ }
173
+ }
135
174
 
136
- const contentType = res.headers.get('content-type') || '';
175
+ /**
176
+ * Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
177
+ */
178
+ function resolvePrivateKey(options) {
179
+ if (options.key) {
180
+ return normalizeKey(options.key);
181
+ }
137
182
 
138
- if (contentType.includes('application/json')) {
139
- const responseData = await res.json();
183
+ if (process.env.X402_PRIVATE_KEY) {
184
+ return normalizeKey(process.env.X402_PRIVATE_KEY);
185
+ }
140
186
 
141
- log.separator();
142
- console.log('');
143
- log.info('Response (JSON):');
144
- console.log('');
187
+ // Try local wallet file
188
+ try {
189
+ const home = process.env.HOME || process.env.USERPROFILE;
190
+ const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
191
+ if (fs.existsSync(walletPath)) {
192
+ const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
193
+ if (data.privateKey) {
194
+ return normalizeKey(data.privateKey);
195
+ }
196
+ }
197
+ } catch { /* ignore */ }
145
198
 
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();
199
+ return null;
200
+ }
152
201
 
153
- } else {
154
- const responseText = await res.text();
202
+ function normalizeKey(key) {
203
+ key = key.trim();
204
+ if (!key.startsWith('0x')) key = '0x' + key;
205
+ if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
206
+ return key;
207
+ }
155
208
 
156
- log.separator();
157
- console.log('');
158
- log.info('Response:');
209
+ /**
210
+ * Handle automatic x402 payment and retry
211
+ */
212
+ async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
213
+ const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
214
+
215
+ try {
216
+ const { sendUsdcPayment } = await import('../lib/payment.js');
217
+ const payment = await sendUsdcPayment(privateKey, payTo, price);
218
+
219
+ spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
220
+ log.dim(` Tx: ${payment.explorer}`);
221
+ console.log('');
222
+
223
+ // Retry with payment proof
224
+ const retrySpinner = ora('Retrying with payment proof...').start();
225
+
226
+ const retryRes = await fetch(url, {
227
+ ...fetchOptions,
228
+ headers: {
229
+ ...fetchOptions.headers,
230
+ 'X-Payment-TxHash': payment.txHash,
231
+ },
232
+ });
233
+
234
+ retrySpinner.stop();
235
+
236
+ if (!retryRes.ok) {
159
237
  console.log('');
160
- console.log(chalk.white(responseText));
238
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
239
+ try {
240
+ const body = await retryRes.text();
241
+ if (body) console.log(chalk.red(body));
242
+ } catch { /* ignore */ }
161
243
  console.log('');
162
- log.separator();
244
+ process.exit(1);
163
245
  }
164
246
 
165
- console.log('');
247
+ await displayResponse(retryRes);
166
248
 
167
249
  } catch (err) {
168
- spinner.fail('Request failed');
250
+ spinner.fail('Payment failed');
169
251
  console.log('');
170
252
 
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');
253
+ if (err.message.includes('Insufficient USDC')) {
254
+ log.error(err.message);
255
+ log.dim(' Fund your wallet with USDC on Base.');
256
+ log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
178
257
  } else {
179
258
  log.error(err.message);
180
259
  }
@@ -185,8 +264,37 @@ export async function callCommand(endpoint, options) {
185
264
  }
186
265
 
187
266
  /**
188
- * Simple JSON syntax highlighting for terminal
267
+ * Display API response with JSON highlighting
189
268
  */
269
+ async function displayResponse(res) {
270
+ console.log('');
271
+ log.success(`${chalk.bold(res.status)} ${res.statusText}`);
272
+ console.log('');
273
+
274
+ const contentType = res.headers.get('content-type') || '';
275
+
276
+ if (contentType.includes('application/json')) {
277
+ const responseData = await res.json();
278
+ log.separator();
279
+ console.log('');
280
+ log.info('Response (JSON):');
281
+ console.log('');
282
+ console.log(highlightJson(JSON.stringify(responseData, null, 2)));
283
+ console.log('');
284
+ log.separator();
285
+ } else {
286
+ const responseText = await res.text();
287
+ log.separator();
288
+ console.log('');
289
+ log.info('Response:');
290
+ console.log('');
291
+ console.log(chalk.white(responseText));
292
+ console.log('');
293
+ log.separator();
294
+ }
295
+ console.log('');
296
+ }
297
+
190
298
  function highlightJson(jsonString) {
191
299
  return jsonString
192
300
  .replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
@@ -1,5 +1,7 @@
1
1
  import ora from 'ora';
2
2
  import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
3
5
  import { log } from '../utils/logger.js';
4
6
 
5
7
  const BASE_RPC_URL = 'https://mainnet.base.org';
@@ -9,26 +11,28 @@ const BALANCE_OF_SELECTOR = '0x70a08231';
9
11
  export async function walletCommand(options) {
10
12
  log.banner();
11
13
 
14
+ // Handle --setup: generate a new wallet
15
+ if (options.setup) {
16
+ await setupWallet();
17
+ return;
18
+ }
19
+
12
20
  if (!options.address) {
13
- log.info('Check USDC balance for any wallet address on Base.');
21
+ log.info('Check USDC balance or generate a new wallet.');
14
22
  console.log('');
15
23
  log.dim(' Usage:');
16
24
  log.dim(' x402-bazaar wallet --address 0xYourAddress');
25
+ log.dim(' x402-bazaar wallet --setup');
17
26
  console.log('');
18
- log.dim(' Example:');
27
+ log.dim(' Examples:');
19
28
  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?"');
29
+ log.dim(' x402-bazaar wallet --setup (generate a new wallet for auto-payment)');
25
30
  console.log('');
26
31
  return;
27
32
  }
28
33
 
29
34
  const address = options.address.trim();
30
35
 
31
- // Validate address format
32
36
  if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
33
37
  log.error('Invalid Ethereum address format');
34
38
  log.dim(' Expected: 0x followed by 40 hexadecimal characters');
@@ -43,24 +47,14 @@ export async function walletCommand(options) {
43
47
  const spinner = ora('Fetching USDC balance from Base...').start();
44
48
 
45
49
  try {
46
- // Encode balanceOf(address) call
47
- // balanceOf selector: 0x70a08231
48
- // Param: address (32 bytes, left-padded)
49
50
  const paddedAddress = address.slice(2).padStart(64, '0');
50
51
  const data = BALANCE_OF_SELECTOR + paddedAddress;
51
52
 
52
- // Make RPC call
53
53
  const rpcPayload = {
54
54
  jsonrpc: '2.0',
55
55
  id: 1,
56
56
  method: 'eth_call',
57
- params: [
58
- {
59
- to: USDC_CONTRACT_BASE,
60
- data: data,
61
- },
62
- 'latest',
63
- ],
57
+ params: [{ to: USDC_CONTRACT_BASE, data }, 'latest'],
64
58
  };
65
59
 
66
60
  const res = await fetch(BASE_RPC_URL, {
@@ -70,28 +64,17 @@ export async function walletCommand(options) {
70
64
  signal: AbortSignal.timeout(15000),
71
65
  });
72
66
 
73
- if (!res.ok) {
74
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
75
- }
67
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
76
68
 
77
69
  const rpcResponse = await res.json();
70
+ if (rpcResponse.error) throw new Error(rpcResponse.error.message || 'RPC error');
71
+ if (!rpcResponse.result) throw new Error('No result from RPC');
78
72
 
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);
73
+ const balanceRaw = BigInt(rpcResponse.result);
90
74
  const balanceUsdc = Number(balanceRaw) / 1_000_000;
91
75
 
92
76
  spinner.succeed('Balance fetched');
93
77
  console.log('');
94
-
95
78
  log.separator();
96
79
  console.log('');
97
80
 
@@ -102,30 +85,22 @@ export async function walletCommand(options) {
102
85
  console.log('');
103
86
  log.separator();
104
87
  console.log('');
105
-
106
- // Show explorer link
107
88
  log.dim(` Explorer: https://basescan.org/address/${address}`);
108
89
 
109
- // Helpful tips based on balance
110
90
  if (balanceUsdc === 0) {
111
91
  console.log('');
112
92
  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');
93
+ log.dim(' Send USDC on Base to this address to start using paid APIs.');
118
94
  } else if (balanceUsdc < 0.1) {
119
95
  console.log('');
120
96
  log.warn('Low balance — consider adding more USDC.');
121
- log.dim(' Most x402 Bazaar APIs cost $0.005-$0.05 per call.');
97
+ log.dim(' Most x402 Bazaar APIs cost $0.001-$0.05 per call.');
122
98
  } else {
123
99
  console.log('');
124
100
  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)`);
101
+ const estimatedCalls = Math.floor(balanceUsdc / 0.005);
102
+ log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.005/call)`);
127
103
  }
128
-
129
104
  console.log('');
130
105
 
131
106
  } catch (err) {
@@ -134,22 +109,107 @@ export async function walletCommand(options) {
134
109
 
135
110
  if (err.name === 'AbortError') {
136
111
  log.error('Request timeout — Base RPC may be slow');
137
- log.dim(' Try again in a few seconds');
138
112
  } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
139
113
  log.error('Cannot connect to Base RPC');
140
- log.dim(' Check your internet connection');
141
114
  } else {
142
115
  log.error(err.message);
143
116
  }
144
-
145
117
  console.log('');
146
118
  process.exit(1);
147
119
  }
148
120
  }
149
121
 
150
122
  /**
151
- * Mask address for display: 0xabcd...1234
123
+ * Generate a new wallet and save to ~/.x402-bazaar/wallet.json
152
124
  */
125
+ async function setupWallet() {
126
+ log.info('Generating a new wallet for x402 Bazaar auto-payment...');
127
+ console.log('');
128
+
129
+ const home = process.env.HOME || process.env.USERPROFILE;
130
+ const dir = path.join(home, '.x402-bazaar');
131
+ const walletPath = path.join(dir, 'wallet.json');
132
+
133
+ // Check if wallet already exists
134
+ if (fs.existsSync(walletPath)) {
135
+ try {
136
+ const existing = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
137
+ if (existing.privateKey) {
138
+ const { getAddressFromKey } = await import('../lib/payment.js');
139
+ const address = getAddressFromKey(existing.privateKey.startsWith('0x') ? existing.privateKey : '0x' + existing.privateKey);
140
+ console.log('');
141
+ log.warn('A wallet already exists!');
142
+ log.info(`Address: ${chalk.hex('#34D399')(address)}`);
143
+ log.dim(` File: ${walletPath}`);
144
+ console.log('');
145
+ log.dim(' To use a different wallet, delete the file and run --setup again.');
146
+ log.dim(' To check balance: npx x402-bazaar wallet --address ' + address);
147
+ console.log('');
148
+ return;
149
+ }
150
+ } catch { /* ignore parse errors, will overwrite */ }
151
+ }
152
+
153
+ const spinner = ora('Generating cryptographic key pair...').start();
154
+
155
+ try {
156
+ const { generatePrivateKey, privateKeyToAccount } = await import('viem/accounts');
157
+ const privateKey = generatePrivateKey();
158
+ const account = privateKeyToAccount(privateKey);
159
+
160
+ // Create directory
161
+ if (!fs.existsSync(dir)) {
162
+ fs.mkdirSync(dir, { recursive: true });
163
+ }
164
+
165
+ // Save wallet (private key stored locally)
166
+ fs.writeFileSync(walletPath, JSON.stringify({
167
+ address: account.address,
168
+ privateKey: privateKey,
169
+ network: 'base',
170
+ created: new Date().toISOString(),
171
+ }, null, 2), 'utf-8');
172
+
173
+ spinner.succeed('Wallet generated!');
174
+ console.log('');
175
+ log.separator();
176
+ console.log('');
177
+
178
+ log.info(`Address: ${chalk.hex('#34D399').bold(account.address)}`);
179
+ log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')}`);
180
+ log.info(`Saved to: ${chalk.dim(walletPath)}`);
181
+
182
+ console.log('');
183
+ log.separator();
184
+ console.log('');
185
+
186
+ log.info('Next steps:');
187
+ console.log('');
188
+ log.dim(' 1. Fund this wallet with USDC on Base:');
189
+ log.dim(` Send USDC to ${chalk.hex('#34D399')(account.address)}`);
190
+ log.dim(' + a tiny amount of ETH for gas (~$0.01)');
191
+ console.log('');
192
+ log.dim(' 2. Call paid APIs automatically:');
193
+ log.dim(` ${chalk.cyan('npx x402-bazaar call /api/weather --param city=Paris')}`);
194
+ log.dim(' (auto-payment will use your saved wallet)');
195
+ console.log('');
196
+ log.dim(' 3. Or set the env variable for any tool:');
197
+ log.dim(` export X402_PRIVATE_KEY=${privateKey}`);
198
+ console.log('');
199
+
200
+ log.warn('Keep your private key safe! Anyone with it can spend your funds.');
201
+ log.dim(` Backup: ${walletPath}`);
202
+ console.log('');
203
+
204
+ } catch (err) {
205
+ spinner.fail('Wallet generation failed');
206
+ console.log('');
207
+ log.error(err.message);
208
+ console.log('');
209
+ process.exit(1);
210
+ }
211
+ }
212
+
153
213
  function maskAddress(address) {
154
214
  if (address.length < 12) return address;
155
215
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
@@ -0,0 +1,106 @@
1
+ import { createWalletClient, createPublicClient, http, parseUnits, encodeFunctionData } from 'viem';
2
+ import { base } from 'viem/chains';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+
5
+ const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
6
+ const USDC_ABI = [
7
+ {
8
+ name: 'transfer',
9
+ type: 'function',
10
+ inputs: [
11
+ { name: 'to', type: 'address' },
12
+ { name: 'amount', type: 'uint256' },
13
+ ],
14
+ outputs: [{ type: 'bool' }],
15
+ },
16
+ {
17
+ name: 'balanceOf',
18
+ type: 'function',
19
+ inputs: [{ name: 'account', type: 'address' }],
20
+ outputs: [{ type: 'uint256' }],
21
+ stateMutability: 'view',
22
+ },
23
+ ];
24
+
25
+ /**
26
+ * Send USDC payment on Base mainnet
27
+ * @param {string} privateKey - Hex private key (with 0x prefix)
28
+ * @param {string} toAddress - Recipient wallet address
29
+ * @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
30
+ * @returns {{ txHash: string, explorer: string }}
31
+ */
32
+ export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
33
+ const account = privateKeyToAccount(privateKey);
34
+
35
+ const walletClient = createWalletClient({
36
+ account,
37
+ chain: base,
38
+ transport: http('https://mainnet.base.org'),
39
+ });
40
+
41
+ const publicClient = createPublicClient({
42
+ chain: base,
43
+ transport: http('https://mainnet.base.org'),
44
+ });
45
+
46
+ // Convert USDC amount to 6-decimal units
47
+ const amount = parseUnits(amountUsdc.toString(), 6);
48
+
49
+ // Check balance first
50
+ const balance = await publicClient.readContract({
51
+ address: USDC_CONTRACT,
52
+ abi: USDC_ABI,
53
+ functionName: 'balanceOf',
54
+ args: [account.address],
55
+ });
56
+
57
+ if (balance < amount) {
58
+ const balanceUsdc = Number(balance) / 1_000_000;
59
+ throw new Error(`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${amountUsdc} USDC)`);
60
+ }
61
+
62
+ // Send the USDC transfer
63
+ const txHash = await walletClient.writeContract({
64
+ address: USDC_CONTRACT,
65
+ abi: USDC_ABI,
66
+ functionName: 'transfer',
67
+ args: [toAddress, amount],
68
+ });
69
+
70
+ // Wait for confirmation
71
+ await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
72
+
73
+ return {
74
+ txHash,
75
+ explorer: `https://basescan.org/tx/${txHash}`,
76
+ from: account.address,
77
+ amount: amountUsdc,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Get the wallet address from a private key
83
+ */
84
+ export function getAddressFromKey(privateKey) {
85
+ const account = privateKeyToAccount(privateKey);
86
+ return account.address;
87
+ }
88
+
89
+ /**
90
+ * Get USDC balance for an address
91
+ */
92
+ export async function getUsdcBalance(address) {
93
+ const publicClient = createPublicClient({
94
+ chain: base,
95
+ transport: http('https://mainnet.base.org'),
96
+ });
97
+
98
+ const balance = await publicClient.readContract({
99
+ address: USDC_CONTRACT,
100
+ abi: USDC_ABI,
101
+ functionName: 'balanceOf',
102
+ args: [address],
103
+ });
104
+
105
+ return Number(balance) / 1_000_000;
106
+ }