x402-bazaar 3.0.0 → 3.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.
@@ -1,305 +1,419 @@
1
- import ora from 'ora';
2
- import chalk from 'chalk';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { log } from '../utils/logger.js';
6
-
7
- export async function callCommand(endpoint, options) {
8
- if (!endpoint || endpoint.trim().length === 0) {
9
- log.error('Endpoint is required');
10
- log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
11
- log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
12
- log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
13
- console.log('');
14
- process.exit(1);
15
- }
16
-
17
- const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
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
- for (const p of paramArray) {
30
- const [key, ...valueParts] = p.split('=');
31
- if (!key || valueParts.length === 0) {
32
- log.warn(`Invalid param format: ${p} (expected key=value)`);
33
- continue;
34
- }
35
- let value = valueParts.join('=');
36
- if ((value.startsWith('"') && value.endsWith('"')) ||
37
- (value.startsWith("'") && value.endsWith("'"))) {
38
- value = value.slice(1, -1);
39
- }
40
- params[key.trim()] = value;
41
- }
42
- }
43
-
44
- if (Object.keys(params).length > 0) {
45
- log.info('Parameters:');
46
- for (const [k, v] of Object.entries(params)) {
47
- log.dim(` ${k}: ${v}`);
48
- }
49
- console.log('');
50
- }
51
-
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
67
- let finalUrl = fullUrl;
68
- if (Object.keys(params).length > 0) {
69
- const queryString = new URLSearchParams(params).toString();
70
- finalUrl = `${fullUrl}?${queryString}`;
71
- }
72
-
73
- const spinner = ora(`GET ${finalUrl}...`).start();
74
-
75
- try {
76
- const fetchOptions = {
77
- method: 'GET',
78
- headers: { 'Content-Type': 'application/json' },
79
- signal: AbortSignal.timeout(30000),
80
- };
81
-
82
- const res = await fetch(finalUrl, fetchOptions);
83
- spinner.stop();
84
-
85
- // Handle 402 Payment Required
86
- if (res.status === 402) {
87
- console.log('');
88
- log.warn(chalk.bold('Payment Required (HTTP 402)'));
89
- console.log('');
90
-
91
- let paymentInfo;
92
- try {
93
- paymentInfo = await res.json();
94
- } catch {
95
- log.error('Could not parse payment details');
96
- process.exit(1);
97
- }
98
-
99
- const price = paymentInfo.payment_details?.amount || paymentInfo.price;
100
- const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
101
-
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('');
109
-
110
- // Auto-pay if key is available
111
- if (autoPay && price && payTo) {
112
- await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
113
- return;
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('');
134
- return;
135
- }
136
-
137
- // Handle other errors
138
- if (!res.ok) {
139
- console.log('');
140
- log.error(`HTTP ${res.status}: ${res.statusText}`);
141
- try {
142
- const errorBody = await res.text();
143
- if (errorBody) {
144
- console.log('');
145
- log.dim('Response:');
146
- console.log(chalk.red(errorBody));
147
- }
148
- } catch { /* ignore */ }
149
- console.log('');
150
- process.exit(1);
151
- }
152
-
153
- // Success
154
- await displayResponse(res);
155
-
156
- } catch (err) {
157
- spinner.fail('Request failed');
158
- console.log('');
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
-
170
- console.log('');
171
- process.exit(1);
172
- }
173
- }
174
-
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
- }
182
-
183
- if (process.env.X402_PRIVATE_KEY) {
184
- return normalizeKey(process.env.X402_PRIVATE_KEY);
185
- }
186
-
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 */ }
198
-
199
- return null;
200
- }
201
-
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
- }
208
-
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) {
237
- console.log('');
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 */ }
243
- console.log('');
244
- process.exit(1);
245
- }
246
-
247
- await displayResponse(retryRes);
248
-
249
- } catch (err) {
250
- spinner.fail('Payment failed');
251
- console.log('');
252
-
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>');
257
- } else {
258
- log.error(err.message);
259
- }
260
-
261
- console.log('');
262
- process.exit(1);
263
- }
264
- }
265
-
266
- /**
267
- * Display API response with JSON highlighting
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
-
298
- function highlightJson(jsonString) {
299
- return jsonString
300
- .replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
301
- .replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
302
- .replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
303
- .replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
304
- .replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
305
- }
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { log } from '../utils/logger.js';
6
+
7
+ export async function callCommand(endpoint, options) {
8
+ if (!endpoint || endpoint.trim().length === 0) {
9
+ log.error('Endpoint is required');
10
+ log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
11
+ log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
12
+ log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
13
+ console.log('');
14
+ process.exit(1);
15
+ }
16
+
17
+ const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
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
+ for (const p of paramArray) {
30
+ const [key, ...valueParts] = p.split('=');
31
+ if (!key || valueParts.length === 0) {
32
+ log.warn(`Invalid param format: ${p} (expected key=value)`);
33
+ continue;
34
+ }
35
+ let value = valueParts.join('=');
36
+ if ((value.startsWith('"') && value.endsWith('"')) ||
37
+ (value.startsWith("'") && value.endsWith("'"))) {
38
+ value = value.slice(1, -1);
39
+ }
40
+ params[key.trim()] = value;
41
+ }
42
+ }
43
+
44
+ if (Object.keys(params).length > 0) {
45
+ log.info('Parameters:');
46
+ for (const [k, v] of Object.entries(params)) {
47
+ log.dim(` ${k}: ${v}`);
48
+ }
49
+ console.log('');
50
+ }
51
+
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
67
+ let finalUrl = fullUrl;
68
+ if (Object.keys(params).length > 0) {
69
+ const queryString = new URLSearchParams(params).toString();
70
+ finalUrl = `${fullUrl}?${queryString}`;
71
+ }
72
+
73
+ const spinner = ora(`GET ${finalUrl}...`).start();
74
+
75
+ try {
76
+ const fetchOptions = {
77
+ method: 'GET',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ signal: AbortSignal.timeout(30000),
80
+ };
81
+
82
+ const res = await fetch(finalUrl, fetchOptions);
83
+ spinner.stop();
84
+
85
+ // Handle 402 Payment Required
86
+ if (res.status === 402) {
87
+ console.log('');
88
+ log.warn(chalk.bold('Payment Required (HTTP 402)'));
89
+ console.log('');
90
+
91
+ let paymentInfo;
92
+ try {
93
+ paymentInfo = await res.json();
94
+ } catch {
95
+ log.error('Could not parse payment details');
96
+ process.exit(1);
97
+ }
98
+
99
+ const price = paymentInfo.payment_details?.amount || paymentInfo.price;
100
+ const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
101
+
102
+ // Split-native mode: provider_wallet is present in payment_details
103
+ const providerWallet = paymentInfo.payment_details?.provider_wallet || null;
104
+ const serverSplit = paymentInfo.payment_details?.split || null;
105
+ const isSplitMode = !!(providerWallet);
106
+
107
+ if (price) {
108
+ log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
109
+ }
110
+ if (isSplitMode) {
111
+ log.dim(` Mode: split native (95% provider / 5% platform)`);
112
+ log.dim(` Provider wallet: ${providerWallet}`);
113
+ log.dim(` Platform wallet: ${payTo}`);
114
+ } else if (payTo) {
115
+ log.dim(` Pay to: ${payTo}`);
116
+ }
117
+ console.log('');
118
+
119
+ // Auto-pay if key is available
120
+ if (autoPay && price && payTo) {
121
+ if (isSplitMode) {
122
+ await handleSplitAutoPayment(
123
+ privateKey, price, providerWallet, payTo, serverSplit, finalUrl, fetchOptions
124
+ );
125
+ } else {
126
+ await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
127
+ }
128
+ return;
129
+ }
130
+
131
+ // No auto-pay show instructions
132
+ log.separator();
133
+ console.log('');
134
+ log.info('To pay automatically, provide your private key:');
135
+ console.log('');
136
+ log.dim(' Option 1: Environment variable (recommended)');
137
+ log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
138
+ log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
139
+ console.log('');
140
+ log.dim(' Option 2: Generate a wallet file');
141
+ log.dim(' npx x402-bazaar wallet --setup');
142
+ console.log('');
143
+ log.dim(' Option 3: --key flag (reads from wallet.json file)');
144
+ log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key ~/.x402-bazaar/wallet.json');
145
+ console.log('');
146
+ log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
147
+ log.dim(' npx x402-bazaar init');
148
+ console.log('');
149
+ return;
150
+ }
151
+
152
+ // Handle other errors
153
+ if (!res.ok) {
154
+ console.log('');
155
+ log.error(`HTTP ${res.status}: ${res.statusText}`);
156
+ try {
157
+ const errorBody = await res.text();
158
+ if (errorBody) {
159
+ console.log('');
160
+ log.dim('Response:');
161
+ console.log(chalk.red(errorBody));
162
+ }
163
+ } catch { /* ignore */ }
164
+ console.log('');
165
+ process.exit(1);
166
+ }
167
+
168
+ // Success
169
+ await displayResponse(res);
170
+
171
+ } catch (err) {
172
+ spinner.fail('Request failed');
173
+ console.log('');
174
+
175
+ if (err.name === 'AbortError') {
176
+ log.error('Request timeout (30s)');
177
+ log.dim(' Try again or check status: npx x402-bazaar status');
178
+ } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
179
+ log.error('Cannot connect to server');
180
+ log.dim(` Server URL: ${serverUrl}`);
181
+ } else {
182
+ log.error(err.message);
183
+ }
184
+
185
+ console.log('');
186
+ process.exit(1);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
192
+ */
193
+ function resolvePrivateKey(options) {
194
+ if (options.key) {
195
+ const raw = options.key.trim();
196
+ // Warn if a raw private key is passed directly as CLI argument (visible in ps aux, shell history)
197
+ const hex = raw.startsWith('0x') ? raw.slice(2) : raw;
198
+ if (/^[0-9a-fA-F]{64}$/.test(hex)) {
199
+ console.log('');
200
+ console.warn(chalk.yellow('⚠️ Warning: Using --key on the command line exposes your private key in shell history.'));
201
+ console.warn(chalk.yellow('⚠️ Prefer setting the X402_PRIVATE_KEY environment variable instead.'));
202
+ console.log('');
203
+ }
204
+ return normalizeKey(options.key);
205
+ }
206
+
207
+ if (process.env.X402_PRIVATE_KEY) {
208
+ return normalizeKey(process.env.X402_PRIVATE_KEY);
209
+ }
210
+
211
+ // Try local wallet file
212
+ try {
213
+ const home = process.env.HOME || process.env.USERPROFILE;
214
+ const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
215
+ if (fs.existsSync(walletPath)) {
216
+ const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
217
+ if (data.privateKey) {
218
+ return normalizeKey(data.privateKey);
219
+ }
220
+ }
221
+ } catch { /* ignore */ }
222
+
223
+ return null;
224
+ }
225
+
226
+ function normalizeKey(key) {
227
+ key = key.trim();
228
+ if (!key.startsWith('0x')) key = '0x' + key;
229
+ if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
230
+ return key;
231
+ }
232
+
233
+ /**
234
+ * Handle split native payment (95% to provider, 5% to platform) and retry.
235
+ *
236
+ * On success the retry request carries two separate tx-hash headers:
237
+ * X-Payment-TxHash-Provider — hash of the 95% transfer to the provider
238
+ * X-Payment-TxHash-Platform — hash of the 5% transfer to the platform
239
+ *
240
+ * @param {string} privateKey - Agent private key (hex, with 0x)
241
+ * @param {number} totalPrice - Full price in USDC
242
+ * @param {string} providerWallet - Provider wallet address (95% recipient)
243
+ * @param {string} platformWallet - Platform wallet address (5% recipient)
244
+ * @param {object|null} serverSplit - Optional split amounts from 402 payment_details.split
245
+ * @param {string} url - API endpoint URL
246
+ * @param {object} fetchOptions - Fetch options passed to the retry request
247
+ */
248
+ async function handleSplitAutoPayment(
249
+ privateKey, totalPrice, providerWallet, platformWallet, serverSplit, url, fetchOptions
250
+ ) {
251
+ const spinner = ora(
252
+ `Sending ${totalPrice} USDC (split: 95% provider / 5% platform)...`
253
+ ).start();
254
+
255
+ try {
256
+ const { sendSplitUsdcPayment } = await import('../lib/payment.js');
257
+
258
+ const result = await sendSplitUsdcPayment(privateKey, {
259
+ totalAmountUsdc: totalPrice,
260
+ providerWallet,
261
+ platformWallet,
262
+ serverSplit,
263
+ });
264
+
265
+ spinner.succeed(
266
+ `Split payment confirmed: ` +
267
+ chalk.hex('#34D399').bold(`${result.providerAmountUsdc.toFixed(6)} USDC`) +
268
+ ` to provider + ` +
269
+ chalk.hex('#34D399').bold(`${result.platformAmountUsdc.toFixed(6)} USDC`) +
270
+ ` to platform`
271
+ );
272
+ log.dim(` Provider tx: ${result.explorerProvider}`);
273
+ log.dim(` Platform tx: ${result.explorerPlatform}`);
274
+ console.log('');
275
+
276
+ // Retry with both payment proofs
277
+ const retrySpinner = ora('Retrying with split payment proof...').start();
278
+
279
+ const retryRes = await fetch(url, {
280
+ ...fetchOptions,
281
+ headers: {
282
+ ...fetchOptions.headers,
283
+ 'X-Payment-TxHash-Provider': result.txHashProvider,
284
+ 'X-Payment-TxHash-Platform': result.txHashPlatform,
285
+ },
286
+ });
287
+
288
+ retrySpinner.stop();
289
+
290
+ if (!retryRes.ok) {
291
+ console.log('');
292
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
293
+ try {
294
+ const body = await retryRes.text();
295
+ if (body) console.log(chalk.red(body));
296
+ } catch { /* ignore */ }
297
+ console.log('');
298
+ process.exit(1);
299
+ }
300
+
301
+ await displayResponse(retryRes);
302
+
303
+ } catch (err) {
304
+ spinner.fail('Split payment failed');
305
+ console.log('');
306
+
307
+ if (err.message.includes('Insufficient USDC')) {
308
+ log.error(err.message);
309
+ log.dim(' Fund your wallet with USDC on Base.');
310
+ log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
311
+ } else if (err.message.includes('Amount too small')) {
312
+ log.error(err.message);
313
+ log.dim(' The service price is too low for a split payment (minimum 0.0001 USDC).');
314
+ } else {
315
+ log.error(err.message);
316
+ }
317
+
318
+ console.log('');
319
+ process.exit(1);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Handle automatic x402 payment and retry
325
+ */
326
+ async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
327
+ const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
328
+
329
+ try {
330
+ const { sendUsdcPayment } = await import('../lib/payment.js');
331
+ const payment = await sendUsdcPayment(privateKey, payTo, price);
332
+
333
+ spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
334
+ log.dim(` Tx: ${payment.explorer}`);
335
+ console.log('');
336
+
337
+ // Retry with payment proof
338
+ const retrySpinner = ora('Retrying with payment proof...').start();
339
+
340
+ const retryRes = await fetch(url, {
341
+ ...fetchOptions,
342
+ headers: {
343
+ ...fetchOptions.headers,
344
+ 'X-Payment-TxHash': payment.txHash,
345
+ },
346
+ });
347
+
348
+ retrySpinner.stop();
349
+
350
+ if (!retryRes.ok) {
351
+ console.log('');
352
+ log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
353
+ try {
354
+ const body = await retryRes.text();
355
+ if (body) console.log(chalk.red(body));
356
+ } catch { /* ignore */ }
357
+ console.log('');
358
+ process.exit(1);
359
+ }
360
+
361
+ await displayResponse(retryRes);
362
+
363
+ } catch (err) {
364
+ spinner.fail('Payment failed');
365
+ console.log('');
366
+
367
+ if (err.message.includes('Insufficient USDC')) {
368
+ log.error(err.message);
369
+ log.dim(' Fund your wallet with USDC on Base.');
370
+ log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
371
+ } else {
372
+ log.error(err.message);
373
+ }
374
+
375
+ console.log('');
376
+ process.exit(1);
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Display API response with JSON highlighting
382
+ */
383
+ async function displayResponse(res) {
384
+ console.log('');
385
+ log.success(`${chalk.bold(res.status)} ${res.statusText}`);
386
+ console.log('');
387
+
388
+ const contentType = res.headers.get('content-type') || '';
389
+
390
+ if (contentType.includes('application/json')) {
391
+ const responseData = await res.json();
392
+ log.separator();
393
+ console.log('');
394
+ log.info('Response (JSON):');
395
+ console.log('');
396
+ console.log(highlightJson(JSON.stringify(responseData, null, 2)));
397
+ console.log('');
398
+ log.separator();
399
+ } else {
400
+ const responseText = await res.text();
401
+ log.separator();
402
+ console.log('');
403
+ log.info('Response:');
404
+ console.log('');
405
+ console.log(chalk.white(responseText));
406
+ console.log('');
407
+ log.separator();
408
+ }
409
+ console.log('');
410
+ }
411
+
412
+ function highlightJson(jsonString) {
413
+ return jsonString
414
+ .replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
415
+ .replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
416
+ .replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
417
+ .replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
418
+ .replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
419
+ }