x402-bazaar 3.5.0 → 3.6.1
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 +50 -50
- package/src/commands/call.js +629 -511
- package/src/commands/init.js +805 -644
package/src/commands/call.js
CHANGED
|
@@ -1,511 +1,629 @@
|
|
|
1
|
-
import ora from
|
|
2
|
-
import chalk from
|
|
3
|
-
import fs from
|
|
4
|
-
import path from
|
|
5
|
-
import { log } from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
log.warn(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
/**
|
|
8
|
+
* Parse an array of "key=value" strings into a params object.
|
|
9
|
+
* Splits on the first `=` only; strips surrounding single or double quotes from values.
|
|
10
|
+
*
|
|
11
|
+
* @param {string[]} paramArray
|
|
12
|
+
* @returns {Record<string, string>}
|
|
13
|
+
*/
|
|
14
|
+
export function parseParams(paramArray) {
|
|
15
|
+
const params = {};
|
|
16
|
+
if (!paramArray || paramArray.length === 0) return params;
|
|
17
|
+
|
|
18
|
+
for (const p of paramArray) {
|
|
19
|
+
const [key, ...valueParts] = p.split("=");
|
|
20
|
+
if (!key || valueParts.length === 0) {
|
|
21
|
+
continue; // Skip invalid params
|
|
22
|
+
}
|
|
23
|
+
let value = valueParts.join("=");
|
|
24
|
+
if (
|
|
25
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
26
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
27
|
+
) {
|
|
28
|
+
value = value.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
params[key.trim()] = value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return params;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a full URL from a base URL, an endpoint path, and optional query params.
|
|
38
|
+
* Ensures the endpoint always starts with `/`.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} baseUrl
|
|
41
|
+
* @param {string} endpoint
|
|
42
|
+
* @param {Record<string, string>} params
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function constructUrl(baseUrl, endpoint, params) {
|
|
46
|
+
const normalizedEndpoint = endpoint.startsWith("/")
|
|
47
|
+
? endpoint
|
|
48
|
+
: `/${endpoint}`;
|
|
49
|
+
let url = `${baseUrl}${normalizedEndpoint}`;
|
|
50
|
+
|
|
51
|
+
if (Object.keys(params).length > 0) {
|
|
52
|
+
const queryString = new URLSearchParams(params).toString();
|
|
53
|
+
url = `${url}?${queryString}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function callCommand(endpoint, options) {
|
|
60
|
+
if (!endpoint || endpoint.trim().length === 0) {
|
|
61
|
+
log.error("Endpoint is required");
|
|
62
|
+
log.dim(" Usage: x402-bazaar call <endpoint> [--param key=value...]");
|
|
63
|
+
log.dim(" Example: x402-bazaar call /api/weather --param city=Paris");
|
|
64
|
+
log.dim(
|
|
65
|
+
" Example: x402-bazaar call /api/hash --param text=hello --key 0x...",
|
|
66
|
+
);
|
|
67
|
+
console.log("");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const serverUrl = options.serverUrl || "https://x402-api.onrender.com";
|
|
72
|
+
const normalizedEndpoint = endpoint.startsWith("/")
|
|
73
|
+
? endpoint
|
|
74
|
+
: `/${endpoint}`;
|
|
75
|
+
|
|
76
|
+
log.banner();
|
|
77
|
+
log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
|
|
78
|
+
console.log("");
|
|
79
|
+
|
|
80
|
+
// Parse params — warn on invalid format before delegating to parseParams()
|
|
81
|
+
const rawParamArray = options.param
|
|
82
|
+
? Array.isArray(options.param)
|
|
83
|
+
? options.param
|
|
84
|
+
: [options.param]
|
|
85
|
+
: [];
|
|
86
|
+
for (const p of rawParamArray) {
|
|
87
|
+
if (!p.includes("="))
|
|
88
|
+
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
89
|
+
}
|
|
90
|
+
const params = parseParams(rawParamArray);
|
|
91
|
+
|
|
92
|
+
if (Object.keys(params).length > 0) {
|
|
93
|
+
log.info("Parameters:");
|
|
94
|
+
for (const [k, v] of Object.entries(params)) {
|
|
95
|
+
log.dim(` ${k}: ${v}`);
|
|
96
|
+
}
|
|
97
|
+
console.log("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Resolve private key for auto-payment
|
|
101
|
+
const privateKey = resolvePrivateKey(options);
|
|
102
|
+
const autoPay = !!privateKey;
|
|
103
|
+
|
|
104
|
+
if (autoPay) {
|
|
105
|
+
log.info(`Auto-payment: ${chalk.hex("#34D399").bold("enabled")}`);
|
|
106
|
+
try {
|
|
107
|
+
const { getAddressFromKey } = await import("../lib/payment.js");
|
|
108
|
+
const address = getAddressFromKey(privateKey);
|
|
109
|
+
log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore display errors */
|
|
112
|
+
}
|
|
113
|
+
console.log("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const finalUrl = constructUrl(serverUrl, normalizedEndpoint, params);
|
|
117
|
+
|
|
118
|
+
const spinner = ora(`GET ${finalUrl}...`).start();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const fetchOptions = {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
signal: AbortSignal.timeout(30000),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const res = await fetch(finalUrl, fetchOptions);
|
|
128
|
+
spinner.stop();
|
|
129
|
+
|
|
130
|
+
// Handle 402 Payment Required
|
|
131
|
+
if (res.status === 402) {
|
|
132
|
+
console.log("");
|
|
133
|
+
log.warn(chalk.bold("Payment Required (HTTP 402)"));
|
|
134
|
+
console.log("");
|
|
135
|
+
|
|
136
|
+
let paymentInfo;
|
|
137
|
+
try {
|
|
138
|
+
paymentInfo = await res.json();
|
|
139
|
+
} catch {
|
|
140
|
+
log.error("Could not parse payment details");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const price = paymentInfo.payment_details?.amount || paymentInfo.price;
|
|
145
|
+
const payTo =
|
|
146
|
+
paymentInfo.payment_details?.recipient ||
|
|
147
|
+
paymentInfo.payment_details?.walletAddress ||
|
|
148
|
+
paymentInfo.paymentAddress;
|
|
149
|
+
|
|
150
|
+
// Split-native mode: provider_wallet is present in payment_details
|
|
151
|
+
const providerWallet =
|
|
152
|
+
paymentInfo.payment_details?.provider_wallet || null;
|
|
153
|
+
const serverSplit = paymentInfo.payment_details?.split || null;
|
|
154
|
+
const isSplitMode = !!providerWallet;
|
|
155
|
+
|
|
156
|
+
// Facilitator mode (Polygon Phase 2): payment_mode === 'fee_splitter'
|
|
157
|
+
const paymentMode = paymentInfo.payment_details?.payment_mode || null;
|
|
158
|
+
const facilitatorUrl =
|
|
159
|
+
paymentInfo.payment_details?.facilitator ||
|
|
160
|
+
paymentInfo.facilitator ||
|
|
161
|
+
null;
|
|
162
|
+
const isFacilitatorMode =
|
|
163
|
+
paymentMode === "fee_splitter" && !!facilitatorUrl;
|
|
164
|
+
|
|
165
|
+
if (price) {
|
|
166
|
+
log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
|
|
167
|
+
}
|
|
168
|
+
if (isFacilitatorMode) {
|
|
169
|
+
log.dim(` Mode: fee_splitter via Polygon facilitator (gas-free)`);
|
|
170
|
+
log.dim(` Facilitator: ${facilitatorUrl}`);
|
|
171
|
+
log.dim(` Recipient: ${payTo}`);
|
|
172
|
+
} else if (isSplitMode) {
|
|
173
|
+
log.dim(` Mode: split native (95% provider / 5% platform)`);
|
|
174
|
+
log.dim(` Provider wallet: ${providerWallet}`);
|
|
175
|
+
log.dim(` Platform wallet: ${payTo}`);
|
|
176
|
+
} else if (payTo) {
|
|
177
|
+
log.dim(` Pay to: ${payTo}`);
|
|
178
|
+
}
|
|
179
|
+
console.log("");
|
|
180
|
+
|
|
181
|
+
// Auto-pay if key is available
|
|
182
|
+
if (autoPay && price && payTo) {
|
|
183
|
+
if (isFacilitatorMode) {
|
|
184
|
+
await handleFacilitatorPayment(
|
|
185
|
+
privateKey,
|
|
186
|
+
price,
|
|
187
|
+
facilitatorUrl,
|
|
188
|
+
paymentInfo.payment_details,
|
|
189
|
+
finalUrl,
|
|
190
|
+
fetchOptions,
|
|
191
|
+
);
|
|
192
|
+
} else if (isSplitMode) {
|
|
193
|
+
await handleSplitAutoPayment(
|
|
194
|
+
privateKey,
|
|
195
|
+
price,
|
|
196
|
+
providerWallet,
|
|
197
|
+
payTo,
|
|
198
|
+
serverSplit,
|
|
199
|
+
finalUrl,
|
|
200
|
+
fetchOptions,
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
await handleAutoPayment(
|
|
204
|
+
privateKey,
|
|
205
|
+
payTo,
|
|
206
|
+
price,
|
|
207
|
+
finalUrl,
|
|
208
|
+
fetchOptions,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// No auto-pay — show instructions
|
|
215
|
+
log.separator();
|
|
216
|
+
console.log("");
|
|
217
|
+
log.info("To pay automatically, provide your private key:");
|
|
218
|
+
console.log("");
|
|
219
|
+
log.dim(" Option 1: Environment variable (recommended)");
|
|
220
|
+
log.dim(" export X402_PRIVATE_KEY=0xYourPrivateKey");
|
|
221
|
+
log.dim(" npx x402-bazaar call /api/weather --param city=Paris");
|
|
222
|
+
console.log("");
|
|
223
|
+
log.dim(" Option 2: Generate a wallet file");
|
|
224
|
+
log.dim(" npx x402-bazaar wallet --setup");
|
|
225
|
+
console.log("");
|
|
226
|
+
log.dim(" Option 3: --key flag (reads from wallet.json file)");
|
|
227
|
+
log.dim(
|
|
228
|
+
" npx x402-bazaar call /api/weather --param city=Paris --key ~/.x402-bazaar/wallet.json",
|
|
229
|
+
);
|
|
230
|
+
console.log("");
|
|
231
|
+
log.dim(" Option 4: Use the MCP server (via Claude/Cursor)");
|
|
232
|
+
log.dim(" npx x402-bazaar init");
|
|
233
|
+
console.log("");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle other errors
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
console.log("");
|
|
240
|
+
log.error(`HTTP ${res.status}: ${res.statusText}`);
|
|
241
|
+
try {
|
|
242
|
+
const errorBody = await res.text();
|
|
243
|
+
if (errorBody) {
|
|
244
|
+
console.log("");
|
|
245
|
+
log.dim("Response:");
|
|
246
|
+
console.log(chalk.red(errorBody));
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
/* ignore */
|
|
250
|
+
}
|
|
251
|
+
console.log("");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Success
|
|
256
|
+
await displayResponse(res);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
spinner.fail("Request failed");
|
|
259
|
+
console.log("");
|
|
260
|
+
|
|
261
|
+
if (err.name === "AbortError") {
|
|
262
|
+
log.error("Request timeout (30s)");
|
|
263
|
+
log.dim(" Try again or check status: npx x402-bazaar status");
|
|
264
|
+
} else if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
|
|
265
|
+
log.error("Cannot connect to server");
|
|
266
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
267
|
+
} else {
|
|
268
|
+
log.error(err.message);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log("");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
|
|
278
|
+
*/
|
|
279
|
+
function resolvePrivateKey(options) {
|
|
280
|
+
if (options.key) {
|
|
281
|
+
const raw = options.key.trim();
|
|
282
|
+
// Warn if a raw private key is passed directly as CLI argument (visible in ps aux, shell history)
|
|
283
|
+
const hex = raw.startsWith("0x") ? raw.slice(2) : raw;
|
|
284
|
+
if (/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
285
|
+
console.log("");
|
|
286
|
+
console.warn(
|
|
287
|
+
chalk.yellow(
|
|
288
|
+
"⚠️ Warning: Using --key on the command line exposes your private key in shell history.",
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
console.warn(
|
|
292
|
+
chalk.yellow(
|
|
293
|
+
"⚠️ Prefer setting the X402_PRIVATE_KEY environment variable instead.",
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
console.log("");
|
|
297
|
+
}
|
|
298
|
+
return normalizeKey(options.key);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (process.env.X402_PRIVATE_KEY) {
|
|
302
|
+
return normalizeKey(process.env.X402_PRIVATE_KEY);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Try local wallet file
|
|
306
|
+
try {
|
|
307
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
308
|
+
const walletPath = path.join(home, ".x402-bazaar", "wallet.json");
|
|
309
|
+
if (fs.existsSync(walletPath)) {
|
|
310
|
+
const data = JSON.parse(fs.readFileSync(walletPath, "utf-8"));
|
|
311
|
+
if (data.privateKey) {
|
|
312
|
+
return normalizeKey(data.privateKey);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
/* ignore */
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeKey(key) {
|
|
323
|
+
key = key.trim();
|
|
324
|
+
if (!key.startsWith("0x")) key = "0x" + key;
|
|
325
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
|
|
326
|
+
return key;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Handle Polygon facilitator payment (EIP-3009 gas-free, fee_splitter mode) and retry.
|
|
331
|
+
*
|
|
332
|
+
* Flow:
|
|
333
|
+
* 1. Sign EIP-3009 TransferWithAuthorization off-chain ($0 gas for user)
|
|
334
|
+
* 2. POST to facilitator /settle — facilitator executes on-chain
|
|
335
|
+
* 3. Retry the API call with the txHash as proof (X-Payment-TxHash header)
|
|
336
|
+
*
|
|
337
|
+
* Falls back to standard sendUsdcPayment on the same recipient if the facilitator fails.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} privateKey - Agent private key (hex, with 0x)
|
|
340
|
+
* @param {number} price - Full price in USDC
|
|
341
|
+
* @param {string} facilitatorUrl - Facilitator base URL
|
|
342
|
+
* @param {object} details - payment_details from the 402 response
|
|
343
|
+
* @param {string} url - API endpoint URL
|
|
344
|
+
* @param {object} fetchOptions - Fetch options for the retry request
|
|
345
|
+
*/
|
|
346
|
+
async function handleFacilitatorPayment(
|
|
347
|
+
privateKey,
|
|
348
|
+
price,
|
|
349
|
+
facilitatorUrl,
|
|
350
|
+
details,
|
|
351
|
+
url,
|
|
352
|
+
fetchOptions,
|
|
353
|
+
) {
|
|
354
|
+
const spinner = ora(
|
|
355
|
+
`Signing EIP-3009 permit and settling via Polygon facilitator (gas-free)...`,
|
|
356
|
+
).start();
|
|
357
|
+
|
|
358
|
+
let txHash;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const { sendViaFacilitator } = await import("../lib/payment.js");
|
|
362
|
+
txHash = await sendViaFacilitator(privateKey, facilitatorUrl, details, url);
|
|
363
|
+
|
|
364
|
+
spinner.succeed(
|
|
365
|
+
`Facilitator settlement confirmed: ${chalk.hex("#34D399").bold(`${price} USDC`)} ` +
|
|
366
|
+
`(gas-free via Polygon facilitator)`,
|
|
367
|
+
);
|
|
368
|
+
log.dim(` Tx: https://polygonscan.com/tx/${txHash}`);
|
|
369
|
+
console.log("");
|
|
370
|
+
} catch (facilitatorErr) {
|
|
371
|
+
spinner.warn(
|
|
372
|
+
`Facilitator payment failed — falling back to direct transfer`,
|
|
373
|
+
);
|
|
374
|
+
log.dim(` Reason: ${facilitatorErr.message}`);
|
|
375
|
+
console.log("");
|
|
376
|
+
|
|
377
|
+
// Fallback: direct USDC transfer on Polygon (standard sendUsdcPayment on Base
|
|
378
|
+
// is intentionally NOT used here; we log a clear message and exit so the user
|
|
379
|
+
// knows they need MATIC gas or can retry manually)
|
|
380
|
+
log.error(
|
|
381
|
+
"Fallback direct transfer not available for Polygon in facilitator mode.",
|
|
382
|
+
);
|
|
383
|
+
log.dim(" Ensure POLYGON_FACILITATOR_URL is reachable or retry later.");
|
|
384
|
+
log.dim(
|
|
385
|
+
" You can also send USDC manually and provide --key with sufficient MATIC for gas.",
|
|
386
|
+
);
|
|
387
|
+
console.log("");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Retry with facilitator payment proof
|
|
392
|
+
const retrySpinner = ora(
|
|
393
|
+
"Retrying with facilitator payment proof...",
|
|
394
|
+
).start();
|
|
395
|
+
|
|
396
|
+
const retryRes = await fetch(url, {
|
|
397
|
+
...fetchOptions,
|
|
398
|
+
headers: {
|
|
399
|
+
...fetchOptions.headers,
|
|
400
|
+
"X-Payment-TxHash": txHash,
|
|
401
|
+
"X-Payment-Chain": "polygon",
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
retrySpinner.stop();
|
|
406
|
+
|
|
407
|
+
if (!retryRes.ok) {
|
|
408
|
+
console.log("");
|
|
409
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
410
|
+
try {
|
|
411
|
+
const body = await retryRes.text();
|
|
412
|
+
if (body) console.log(chalk.red(body));
|
|
413
|
+
} catch {
|
|
414
|
+
/* ignore */
|
|
415
|
+
}
|
|
416
|
+
console.log("");
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
await displayResponse(retryRes);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handle split native payment (95% to provider, 5% to platform) and retry.
|
|
425
|
+
*
|
|
426
|
+
* On success the retry request carries two separate tx-hash headers:
|
|
427
|
+
* X-Payment-TxHash-Provider — hash of the 95% transfer to the provider
|
|
428
|
+
* X-Payment-TxHash-Platform — hash of the 5% transfer to the platform
|
|
429
|
+
*
|
|
430
|
+
* @param {string} privateKey - Agent private key (hex, with 0x)
|
|
431
|
+
* @param {number} totalPrice - Full price in USDC
|
|
432
|
+
* @param {string} providerWallet - Provider wallet address (95% recipient)
|
|
433
|
+
* @param {string} platformWallet - Platform wallet address (5% recipient)
|
|
434
|
+
* @param {object|null} serverSplit - Optional split amounts from 402 payment_details.split
|
|
435
|
+
* @param {string} url - API endpoint URL
|
|
436
|
+
* @param {object} fetchOptions - Fetch options passed to the retry request
|
|
437
|
+
*/
|
|
438
|
+
async function handleSplitAutoPayment(
|
|
439
|
+
privateKey,
|
|
440
|
+
totalPrice,
|
|
441
|
+
providerWallet,
|
|
442
|
+
platformWallet,
|
|
443
|
+
serverSplit,
|
|
444
|
+
url,
|
|
445
|
+
fetchOptions,
|
|
446
|
+
) {
|
|
447
|
+
const spinner = ora(
|
|
448
|
+
`Sending ${totalPrice} USDC (split: 95% provider / 5% platform)...`,
|
|
449
|
+
).start();
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const { sendSplitUsdcPayment } = await import("../lib/payment.js");
|
|
453
|
+
|
|
454
|
+
const result = await sendSplitUsdcPayment(privateKey, {
|
|
455
|
+
totalAmountUsdc: totalPrice,
|
|
456
|
+
providerWallet,
|
|
457
|
+
platformWallet,
|
|
458
|
+
serverSplit,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
spinner.succeed(
|
|
462
|
+
`Split payment confirmed: ` +
|
|
463
|
+
chalk
|
|
464
|
+
.hex("#34D399")
|
|
465
|
+
.bold(`${result.providerAmountUsdc.toFixed(6)} USDC`) +
|
|
466
|
+
` to provider + ` +
|
|
467
|
+
chalk
|
|
468
|
+
.hex("#34D399")
|
|
469
|
+
.bold(`${result.platformAmountUsdc.toFixed(6)} USDC`) +
|
|
470
|
+
` to platform`,
|
|
471
|
+
);
|
|
472
|
+
log.dim(` Provider tx: ${result.explorerProvider}`);
|
|
473
|
+
log.dim(` Platform tx: ${result.explorerPlatform}`);
|
|
474
|
+
console.log("");
|
|
475
|
+
|
|
476
|
+
// Retry with both payment proofs
|
|
477
|
+
const retrySpinner = ora("Retrying with split payment proof...").start();
|
|
478
|
+
|
|
479
|
+
const retryRes = await fetch(url, {
|
|
480
|
+
...fetchOptions,
|
|
481
|
+
headers: {
|
|
482
|
+
...fetchOptions.headers,
|
|
483
|
+
"X-Payment-TxHash-Provider": result.txHashProvider,
|
|
484
|
+
"X-Payment-TxHash-Platform": result.txHashPlatform,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
retrySpinner.stop();
|
|
489
|
+
|
|
490
|
+
if (!retryRes.ok) {
|
|
491
|
+
console.log("");
|
|
492
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
493
|
+
try {
|
|
494
|
+
const body = await retryRes.text();
|
|
495
|
+
if (body) console.log(chalk.red(body));
|
|
496
|
+
} catch {
|
|
497
|
+
/* ignore */
|
|
498
|
+
}
|
|
499
|
+
console.log("");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
await displayResponse(retryRes);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
spinner.fail("Split payment failed");
|
|
506
|
+
console.log("");
|
|
507
|
+
|
|
508
|
+
if (err.message.includes("Insufficient USDC")) {
|
|
509
|
+
log.error(err.message);
|
|
510
|
+
log.dim(" Fund your wallet with USDC on Base.");
|
|
511
|
+
log.dim(
|
|
512
|
+
" Check balance: npx x402-bazaar wallet --address <your-address>",
|
|
513
|
+
);
|
|
514
|
+
} else if (err.message.includes("Amount too small")) {
|
|
515
|
+
log.error(err.message);
|
|
516
|
+
log.dim(
|
|
517
|
+
" The service price is too low for a split payment (minimum 0.0001 USDC).",
|
|
518
|
+
);
|
|
519
|
+
} else {
|
|
520
|
+
log.error(err.message);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
console.log("");
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Handle automatic x402 payment and retry
|
|
530
|
+
*/
|
|
531
|
+
async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
|
|
532
|
+
const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const { sendUsdcPayment } = await import("../lib/payment.js");
|
|
536
|
+
const payment = await sendUsdcPayment(privateKey, payTo, price);
|
|
537
|
+
|
|
538
|
+
spinner.succeed(
|
|
539
|
+
`Payment confirmed: ${chalk.hex("#34D399").bold(`${price} USDC`)}`,
|
|
540
|
+
);
|
|
541
|
+
log.dim(` Tx: ${payment.explorer}`);
|
|
542
|
+
console.log("");
|
|
543
|
+
|
|
544
|
+
// Retry with payment proof
|
|
545
|
+
const retrySpinner = ora("Retrying with payment proof...").start();
|
|
546
|
+
|
|
547
|
+
const retryRes = await fetch(url, {
|
|
548
|
+
...fetchOptions,
|
|
549
|
+
headers: {
|
|
550
|
+
...fetchOptions.headers,
|
|
551
|
+
"X-Payment-TxHash": payment.txHash,
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
retrySpinner.stop();
|
|
556
|
+
|
|
557
|
+
if (!retryRes.ok) {
|
|
558
|
+
console.log("");
|
|
559
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
560
|
+
try {
|
|
561
|
+
const body = await retryRes.text();
|
|
562
|
+
if (body) console.log(chalk.red(body));
|
|
563
|
+
} catch {
|
|
564
|
+
/* ignore */
|
|
565
|
+
}
|
|
566
|
+
console.log("");
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
await displayResponse(retryRes);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
spinner.fail("Payment failed");
|
|
573
|
+
console.log("");
|
|
574
|
+
|
|
575
|
+
if (err.message.includes("Insufficient USDC")) {
|
|
576
|
+
log.error(err.message);
|
|
577
|
+
log.dim(" Fund your wallet with USDC on Base.");
|
|
578
|
+
log.dim(
|
|
579
|
+
" Check balance: npx x402-bazaar wallet --address <your-address>",
|
|
580
|
+
);
|
|
581
|
+
} else {
|
|
582
|
+
log.error(err.message);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
console.log("");
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Display API response with JSON highlighting
|
|
592
|
+
*/
|
|
593
|
+
async function displayResponse(res) {
|
|
594
|
+
console.log("");
|
|
595
|
+
log.success(`${chalk.bold(res.status)} ${res.statusText}`);
|
|
596
|
+
console.log("");
|
|
597
|
+
|
|
598
|
+
const contentType = res.headers.get("content-type") || "";
|
|
599
|
+
|
|
600
|
+
if (contentType.includes("application/json")) {
|
|
601
|
+
const responseData = await res.json();
|
|
602
|
+
log.separator();
|
|
603
|
+
console.log("");
|
|
604
|
+
log.info("Response (JSON):");
|
|
605
|
+
console.log("");
|
|
606
|
+
console.log(highlightJson(JSON.stringify(responseData, null, 2)));
|
|
607
|
+
console.log("");
|
|
608
|
+
log.separator();
|
|
609
|
+
} else {
|
|
610
|
+
const responseText = await res.text();
|
|
611
|
+
log.separator();
|
|
612
|
+
console.log("");
|
|
613
|
+
log.info("Response:");
|
|
614
|
+
console.log("");
|
|
615
|
+
console.log(chalk.white(responseText));
|
|
616
|
+
console.log("");
|
|
617
|
+
log.separator();
|
|
618
|
+
}
|
|
619
|
+
console.log("");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function highlightJson(jsonString) {
|
|
623
|
+
return jsonString
|
|
624
|
+
.replace(/"([^"]+)":/g, chalk.hex("#60A5FA")('"$1"') + ":")
|
|
625
|
+
.replace(/: "([^"]+)"/g, ": " + chalk.hex("#34D399")('"$1"'))
|
|
626
|
+
.replace(/: (\d+\.?\d*)/g, ": " + chalk.hex("#FBBF24")("$1"))
|
|
627
|
+
.replace(/: (true|false)/g, ": " + chalk.hex("#9333EA")("$1"))
|
|
628
|
+
.replace(/: null/g, ": " + chalk.hex("#6B7280")("null"));
|
|
629
|
+
}
|