@mixrpay/agent-sdk 0.1.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.

Potentially problematic release.


This version of @mixrpay/agent-sdk might be problematic. Click here for more details.

package/dist/index.cjs ADDED
@@ -0,0 +1,982 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AgentWallet: () => AgentWallet,
24
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
25
+ DEFAULT_FACILITATOR_URL: () => DEFAULT_FACILITATOR_URL,
26
+ DEFAULT_TIMEOUT: () => DEFAULT_TIMEOUT,
27
+ InsufficientBalanceError: () => InsufficientBalanceError,
28
+ InvalidSessionKeyError: () => InvalidSessionKeyError,
29
+ MixrPayError: () => MixrPayError,
30
+ NETWORKS: () => NETWORKS,
31
+ PaymentFailedError: () => PaymentFailedError,
32
+ SDK_VERSION: () => SDK_VERSION,
33
+ SessionKey: () => SessionKey,
34
+ SessionKeyExpiredError: () => SessionKeyExpiredError,
35
+ SpendingLimitExceededError: () => SpendingLimitExceededError,
36
+ X402ProtocolError: () => X402ProtocolError,
37
+ buildTransferAuthorizationData: () => buildTransferAuthorizationData,
38
+ buildXPaymentHeader: () => buildXPaymentHeader,
39
+ generateNonce: () => generateNonce,
40
+ getAmountUsd: () => getAmountUsd,
41
+ getErrorMessage: () => getErrorMessage,
42
+ isMixrPayError: () => isMixrPayError,
43
+ isPaymentExpired: () => isPaymentExpired,
44
+ parse402Response: () => parse402Response,
45
+ validatePaymentAmount: () => validatePaymentAmount
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/session-key.ts
50
+ var import_accounts = require("viem/accounts");
51
+
52
+ // src/errors.ts
53
+ var MixrPayError = class extends Error {
54
+ /** Error code for programmatic handling */
55
+ code;
56
+ constructor(message, code = "MIXRPAY_ERROR") {
57
+ super(message);
58
+ this.name = "MixrPayError";
59
+ this.code = code;
60
+ if (Error.captureStackTrace) {
61
+ Error.captureStackTrace(this, this.constructor);
62
+ }
63
+ }
64
+ };
65
+ var InsufficientBalanceError = class extends MixrPayError {
66
+ /** Amount required for the payment in USD */
67
+ required;
68
+ /** Current available balance in USD */
69
+ available;
70
+ /** URL where the user can top up their wallet */
71
+ topUpUrl;
72
+ constructor(required, available) {
73
+ const shortage = required - available;
74
+ super(
75
+ `Insufficient balance: need $${required.toFixed(2)}, have $${available.toFixed(2)} (short $${shortage.toFixed(2)}). Top up your wallet to continue.`,
76
+ "INSUFFICIENT_BALANCE"
77
+ );
78
+ this.name = "InsufficientBalanceError";
79
+ this.required = required;
80
+ this.available = available;
81
+ this.topUpUrl = "/wallet";
82
+ }
83
+ };
84
+ var SessionKeyExpiredError = class extends MixrPayError {
85
+ /** When the session key expired */
86
+ expiredAt;
87
+ constructor(expiredAt) {
88
+ super(
89
+ `Session key expired at ${expiredAt}. Request a new session key from the wallet owner or create one at your MixrPay server /wallet/sessions`,
90
+ "SESSION_KEY_EXPIRED"
91
+ );
92
+ this.name = "SessionKeyExpiredError";
93
+ this.expiredAt = expiredAt;
94
+ }
95
+ };
96
+ var SpendingLimitExceededError = class extends MixrPayError {
97
+ /** Type of limit that was exceeded */
98
+ limitType;
99
+ /** The limit amount in USD */
100
+ limit;
101
+ /** The amount that was attempted in USD */
102
+ attempted;
103
+ constructor(limitType, limit, attempted) {
104
+ const limitNames = {
105
+ per_tx: "Per-transaction",
106
+ daily: "Daily",
107
+ total: "Total",
108
+ client_max: "Client-side"
109
+ };
110
+ const limitName = limitNames[limitType] || limitType;
111
+ const suggestion = limitType === "daily" ? "Wait until tomorrow or request a higher limit." : limitType === "client_max" ? "Increase maxPaymentUsd in your AgentWallet configuration." : "Request a new session key with a higher limit.";
112
+ super(
113
+ `${limitName} spending limit exceeded: limit is $${limit.toFixed(2)}, attempted $${attempted.toFixed(2)}. ${suggestion}`,
114
+ "SPENDING_LIMIT_EXCEEDED"
115
+ );
116
+ this.name = "SpendingLimitExceededError";
117
+ this.limitType = limitType;
118
+ this.limit = limit;
119
+ this.attempted = attempted;
120
+ }
121
+ };
122
+ var PaymentFailedError = class extends MixrPayError {
123
+ /** Detailed reason for the failure */
124
+ reason;
125
+ /** Transaction hash if the tx was submitted (for debugging) */
126
+ txHash;
127
+ constructor(reason, txHash) {
128
+ let message = `Payment failed: ${reason}`;
129
+ if (txHash) {
130
+ message += ` (tx: ${txHash} - check on basescan.org)`;
131
+ }
132
+ super(message, "PAYMENT_FAILED");
133
+ this.name = "PaymentFailedError";
134
+ this.reason = reason;
135
+ this.txHash = txHash;
136
+ }
137
+ };
138
+ var InvalidSessionKeyError = class extends MixrPayError {
139
+ /** Detailed reason why the key is invalid */
140
+ reason;
141
+ constructor(reason = "Invalid session key format") {
142
+ super(
143
+ `${reason}. Session keys should be in format: sk_live_<64 hex chars> or sk_test_<64 hex chars>. Get one from your MixrPay server /wallet/sessions`,
144
+ "INVALID_SESSION_KEY"
145
+ );
146
+ this.name = "InvalidSessionKeyError";
147
+ this.reason = reason;
148
+ }
149
+ };
150
+ var X402ProtocolError = class extends MixrPayError {
151
+ /** Detailed reason for the protocol error */
152
+ reason;
153
+ constructor(reason) {
154
+ super(
155
+ `x402 protocol error: ${reason}. This may indicate a server configuration issue. If the problem persists, contact the API provider.`,
156
+ "X402_PROTOCOL_ERROR"
157
+ );
158
+ this.name = "X402ProtocolError";
159
+ this.reason = reason;
160
+ }
161
+ };
162
+ function isMixrPayError(error) {
163
+ return error instanceof MixrPayError;
164
+ }
165
+ function getErrorMessage(error) {
166
+ if (error instanceof MixrPayError) {
167
+ return error.message;
168
+ }
169
+ if (error instanceof Error) {
170
+ return error.message;
171
+ }
172
+ return String(error);
173
+ }
174
+
175
+ // src/session-key.ts
176
+ var USDC_ADDRESSES = {
177
+ 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
178
+ // Base Mainnet
179
+ 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
180
+ // Base Sepolia
181
+ };
182
+ var USDC_EIP712_DOMAIN_BASE = {
183
+ name: "USD Coin",
184
+ version: "2"
185
+ };
186
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
187
+ TransferWithAuthorization: [
188
+ { name: "from", type: "address" },
189
+ { name: "to", type: "address" },
190
+ { name: "value", type: "uint256" },
191
+ { name: "validAfter", type: "uint256" },
192
+ { name: "validBefore", type: "uint256" },
193
+ { name: "nonce", type: "bytes32" }
194
+ ]
195
+ };
196
+ var SessionKey = class _SessionKey {
197
+ privateKey;
198
+ account;
199
+ address;
200
+ isTest;
201
+ constructor(privateKey, isTest) {
202
+ this.privateKey = privateKey;
203
+ this.account = (0, import_accounts.privateKeyToAccount)(privateKey);
204
+ this.address = this.account.address;
205
+ this.isTest = isTest;
206
+ }
207
+ /**
208
+ * Parse a session key string into a SessionKey object.
209
+ *
210
+ * @param sessionKey - Session key in format sk_live_... or sk_test_...
211
+ * @returns SessionKey object
212
+ * @throws InvalidSessionKeyError if the format is invalid
213
+ */
214
+ static fromString(sessionKey) {
215
+ const pattern = /^sk_(live|test)_([a-fA-F0-9]{64})$/;
216
+ const match = sessionKey.match(pattern);
217
+ if (!match) {
218
+ throw new InvalidSessionKeyError(
219
+ "Session key must be in format sk_live_{64_hex} or sk_test_{64_hex}"
220
+ );
221
+ }
222
+ const env = match[1];
223
+ const hexKey = match[2];
224
+ try {
225
+ const privateKey = `0x${hexKey}`;
226
+ return new _SessionKey(privateKey, env === "test");
227
+ } catch (e) {
228
+ throw new InvalidSessionKeyError(`Failed to decode session key: ${e}`);
229
+ }
230
+ }
231
+ /**
232
+ * Sign EIP-712 typed data for TransferWithAuthorization.
233
+ *
234
+ * @param domain - EIP-712 domain
235
+ * @param message - Transfer authorization message
236
+ * @returns Hex-encoded signature
237
+ */
238
+ async signTransferAuthorization(domain, message) {
239
+ return (0, import_accounts.signTypedData)({
240
+ privateKey: this.privateKey,
241
+ domain,
242
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
243
+ primaryType: "TransferWithAuthorization",
244
+ message
245
+ });
246
+ }
247
+ /**
248
+ * Get the chain ID based on whether this is a test key.
249
+ */
250
+ getDefaultChainId() {
251
+ return this.isTest ? 84532 : 8453;
252
+ }
253
+ };
254
+ function buildTransferAuthorizationData(params) {
255
+ const usdcAddress = USDC_ADDRESSES[params.chainId];
256
+ if (!usdcAddress) {
257
+ throw new Error(`USDC not supported on chain ${params.chainId}`);
258
+ }
259
+ const domain = {
260
+ ...USDC_EIP712_DOMAIN_BASE,
261
+ chainId: params.chainId,
262
+ verifyingContract: usdcAddress
263
+ };
264
+ const message = {
265
+ from: params.fromAddress,
266
+ to: params.toAddress,
267
+ value: params.value,
268
+ validAfter: params.validAfter,
269
+ validBefore: params.validBefore,
270
+ nonce: params.nonce
271
+ };
272
+ return { domain, message };
273
+ }
274
+ function generateNonce() {
275
+ const bytes = new Uint8Array(32);
276
+ crypto.getRandomValues(bytes);
277
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
278
+ }
279
+
280
+ // src/x402.ts
281
+ async function parse402Response(response) {
282
+ let paymentData = null;
283
+ const paymentHeader = response.headers.get("X-Payment-Required");
284
+ if (paymentHeader) {
285
+ try {
286
+ paymentData = JSON.parse(paymentHeader);
287
+ } catch {
288
+ }
289
+ }
290
+ if (!paymentData) {
291
+ const authHeader = response.headers.get("WWW-Authenticate");
292
+ if (authHeader?.startsWith("X-402 ")) {
293
+ try {
294
+ const encoded = authHeader.slice(6);
295
+ paymentData = JSON.parse(atob(encoded));
296
+ } catch {
297
+ }
298
+ }
299
+ }
300
+ if (!paymentData) {
301
+ try {
302
+ paymentData = await response.json();
303
+ } catch {
304
+ }
305
+ }
306
+ if (!paymentData) {
307
+ throw new X402ProtocolError("Could not parse payment requirements from 402 response");
308
+ }
309
+ if (!paymentData.recipient) {
310
+ throw new X402ProtocolError("Missing recipient in payment requirements");
311
+ }
312
+ if (!paymentData.amount) {
313
+ throw new X402ProtocolError("Missing amount in payment requirements");
314
+ }
315
+ const now = Math.floor(Date.now() / 1e3);
316
+ return {
317
+ recipient: paymentData.recipient,
318
+ amount: BigInt(paymentData.amount),
319
+ currency: paymentData.currency || "USDC",
320
+ chainId: paymentData.chainId || paymentData.chain_id || 8453,
321
+ facilitatorUrl: paymentData.facilitatorUrl || paymentData.facilitator_url || "https://x402.org/facilitator",
322
+ nonce: paymentData.nonce || generateNonce().slice(2),
323
+ // Remove 0x prefix for storage
324
+ expiresAt: paymentData.expiresAt || paymentData.expires_at || now + 300,
325
+ description: paymentData.description
326
+ };
327
+ }
328
+ async function buildXPaymentHeader(requirements, sessionKey, walletAddress) {
329
+ const nonceHex = requirements.nonce.length === 64 ? `0x${requirements.nonce}` : generateNonce();
330
+ const now = BigInt(Math.floor(Date.now() / 1e3));
331
+ const validAfter = now - 60n;
332
+ const validBefore = BigInt(requirements.expiresAt);
333
+ const { domain, message } = buildTransferAuthorizationData({
334
+ fromAddress: walletAddress,
335
+ toAddress: requirements.recipient,
336
+ value: requirements.amount,
337
+ validAfter,
338
+ validBefore,
339
+ nonce: nonceHex,
340
+ chainId: requirements.chainId
341
+ });
342
+ const signature = await sessionKey.signTransferAuthorization(domain, message);
343
+ const paymentPayload = {
344
+ x402Version: 1,
345
+ scheme: "exact",
346
+ network: requirements.chainId === 8453 ? "base" : "base-sepolia",
347
+ payload: {
348
+ signature,
349
+ authorization: {
350
+ from: walletAddress,
351
+ to: requirements.recipient,
352
+ value: requirements.amount.toString(),
353
+ validAfter: validAfter.toString(),
354
+ validBefore: validBefore.toString(),
355
+ nonce: nonceHex
356
+ }
357
+ }
358
+ };
359
+ return btoa(JSON.stringify(paymentPayload));
360
+ }
361
+ function isPaymentExpired(requirements) {
362
+ return Date.now() / 1e3 > requirements.expiresAt;
363
+ }
364
+ function getAmountUsd(requirements) {
365
+ return Number(requirements.amount) / 1e6;
366
+ }
367
+ function validatePaymentAmount(amountUsd, maxPaymentUsd) {
368
+ if (amountUsd <= 0) {
369
+ throw new X402ProtocolError(`Invalid payment amount: $${amountUsd.toFixed(2)}`);
370
+ }
371
+ if (maxPaymentUsd !== void 0 && amountUsd > maxPaymentUsd) {
372
+ throw new X402ProtocolError(
373
+ `Payment amount $${amountUsd.toFixed(2)} exceeds client limit $${maxPaymentUsd.toFixed(2)}`
374
+ );
375
+ }
376
+ }
377
+
378
+ // src/agent-wallet.ts
379
+ var SDK_VERSION = "0.1.0";
380
+ var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "http://localhost:3000";
381
+ var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
382
+ var DEFAULT_TIMEOUT = 3e4;
383
+ var NETWORKS = {
384
+ BASE_MAINNET: { chainId: 8453, name: "Base", isTestnet: false },
385
+ BASE_SEPOLIA: { chainId: 84532, name: "Base Sepolia", isTestnet: true }
386
+ };
387
+ var LOG_LEVELS = {
388
+ debug: 0,
389
+ info: 1,
390
+ warn: 2,
391
+ error: 3,
392
+ none: 4
393
+ };
394
+ var Logger = class {
395
+ level;
396
+ prefix;
397
+ constructor(level = "none", prefix = "[MixrPay]") {
398
+ this.level = level;
399
+ this.prefix = prefix;
400
+ }
401
+ setLevel(level) {
402
+ this.level = level;
403
+ }
404
+ shouldLog(level) {
405
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
406
+ }
407
+ debug(...args) {
408
+ if (this.shouldLog("debug")) {
409
+ console.log(`${this.prefix} \u{1F50D}`, ...args);
410
+ }
411
+ }
412
+ info(...args) {
413
+ if (this.shouldLog("info")) {
414
+ console.log(`${this.prefix} \u2139\uFE0F`, ...args);
415
+ }
416
+ }
417
+ warn(...args) {
418
+ if (this.shouldLog("warn")) {
419
+ console.warn(`${this.prefix} \u26A0\uFE0F`, ...args);
420
+ }
421
+ }
422
+ error(...args) {
423
+ if (this.shouldLog("error")) {
424
+ console.error(`${this.prefix} \u274C`, ...args);
425
+ }
426
+ }
427
+ payment(amount, recipient, description) {
428
+ if (this.shouldLog("info")) {
429
+ const desc = description ? ` for "${description}"` : "";
430
+ console.log(`${this.prefix} \u{1F4B8} Paid $${amount.toFixed(4)} to ${recipient.slice(0, 10)}...${desc}`);
431
+ }
432
+ }
433
+ };
434
+ var AgentWallet = class {
435
+ sessionKey;
436
+ walletAddress;
437
+ maxPaymentUsd;
438
+ onPayment;
439
+ baseUrl;
440
+ timeout;
441
+ logger;
442
+ // Payment tracking
443
+ payments = [];
444
+ totalSpentUsd = 0;
445
+ // Session key info cache
446
+ sessionKeyInfo;
447
+ sessionKeyInfoFetchedAt;
448
+ /**
449
+ * Create a new AgentWallet instance.
450
+ *
451
+ * @param config - Configuration options
452
+ * @throws {InvalidSessionKeyError} If the session key format is invalid
453
+ *
454
+ * @example Basic initialization
455
+ * ```typescript
456
+ * const wallet = new AgentWallet({
457
+ * sessionKey: 'sk_live_abc123...'
458
+ * });
459
+ * ```
460
+ *
461
+ * @example With all options
462
+ * ```typescript
463
+ * const wallet = new AgentWallet({
464
+ * sessionKey: 'sk_live_abc123...',
465
+ * maxPaymentUsd: 5.0, // Client-side safety limit
466
+ * onPayment: (p) => log(p), // Track payments
467
+ * logLevel: 'info', // Enable logging
468
+ * timeout: 60000, // 60s timeout
469
+ * });
470
+ * ```
471
+ */
472
+ constructor(config) {
473
+ this.validateConfig(config);
474
+ this.sessionKey = SessionKey.fromString(config.sessionKey);
475
+ this.walletAddress = config.walletAddress || this.sessionKey.address;
476
+ this.maxPaymentUsd = config.maxPaymentUsd;
477
+ this.onPayment = config.onPayment;
478
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
479
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
480
+ this.logger = new Logger(config.logLevel || "none");
481
+ this.logger.debug("AgentWallet initialized", {
482
+ walletAddress: this.walletAddress,
483
+ isTestnet: this.isTestnet(),
484
+ maxPaymentUsd: this.maxPaymentUsd
485
+ });
486
+ }
487
+ /**
488
+ * Validate the configuration before initialization.
489
+ */
490
+ validateConfig(config) {
491
+ if (!config.sessionKey) {
492
+ throw new InvalidSessionKeyError(
493
+ "Session key is required. Get one from the wallet owner or create one at your MixrPay server /wallet/sessions"
494
+ );
495
+ }
496
+ const key = config.sessionKey.trim();
497
+ if (!key.startsWith("sk_live_") && !key.startsWith("sk_test_")) {
498
+ throw new InvalidSessionKeyError(
499
+ `Invalid session key prefix. Expected 'sk_live_' (mainnet) or 'sk_test_' (testnet), got '${key.slice(0, 10)}...'`
500
+ );
501
+ }
502
+ const expectedLength = 8 + 64;
503
+ if (key.length !== expectedLength) {
504
+ throw new InvalidSessionKeyError(
505
+ `Invalid session key length. Expected ${expectedLength} characters, got ${key.length}. Make sure you copied the complete key.`
506
+ );
507
+ }
508
+ const hexPortion = key.slice(8);
509
+ if (!/^[0-9a-fA-F]+$/.test(hexPortion)) {
510
+ throw new InvalidSessionKeyError(
511
+ "Invalid session key format. The key should contain only hexadecimal characters after the prefix."
512
+ );
513
+ }
514
+ if (config.maxPaymentUsd !== void 0) {
515
+ if (config.maxPaymentUsd <= 0) {
516
+ throw new MixrPayError("maxPaymentUsd must be a positive number");
517
+ }
518
+ if (config.maxPaymentUsd > 1e4) {
519
+ this.logger?.warn("maxPaymentUsd is very high ($" + config.maxPaymentUsd + "). Consider using a lower limit for safety.");
520
+ }
521
+ }
522
+ }
523
+ // ===========================================================================
524
+ // Core Methods
525
+ // ===========================================================================
526
+ /**
527
+ * Make an HTTP request, automatically handling x402 payment if required.
528
+ *
529
+ * This is a drop-in replacement for the native `fetch()` function.
530
+ * If the server returns 402 Payment Required:
531
+ * 1. Parse payment requirements from response
532
+ * 2. Sign transferWithAuthorization using session key
533
+ * 3. Retry request with X-PAYMENT header
534
+ *
535
+ * @param url - Request URL
536
+ * @param init - Request options (same as native fetch)
537
+ * @returns Response from the server
538
+ *
539
+ * @throws {InsufficientBalanceError} If wallet doesn't have enough USDC
540
+ * @throws {SessionKeyExpiredError} If session key has expired
541
+ * @throws {SpendingLimitExceededError} If payment would exceed limits
542
+ * @throws {PaymentFailedError} If payment transaction failed
543
+ *
544
+ * @example GET request
545
+ * ```typescript
546
+ * const response = await wallet.fetch('https://api.example.com/data');
547
+ * const data = await response.json();
548
+ * ```
549
+ *
550
+ * @example POST request with JSON
551
+ * ```typescript
552
+ * const response = await wallet.fetch('https://api.example.com/generate', {
553
+ * method: 'POST',
554
+ * headers: { 'Content-Type': 'application/json' },
555
+ * body: JSON.stringify({ prompt: 'Hello world' })
556
+ * });
557
+ * ```
558
+ */
559
+ async fetch(url, init) {
560
+ this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
561
+ const controller = new AbortController();
562
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
563
+ try {
564
+ let response = await fetch(url, {
565
+ ...init,
566
+ signal: controller.signal
567
+ });
568
+ this.logger.debug(`Initial response: ${response.status}`);
569
+ if (response.status === 402) {
570
+ this.logger.info(`Payment required for ${url}`);
571
+ response = await this.handlePaymentRequired(url, init, response);
572
+ }
573
+ return response;
574
+ } catch (error) {
575
+ if (error instanceof Error && error.name === "AbortError") {
576
+ throw new MixrPayError(`Request timeout after ${this.timeout}ms`);
577
+ }
578
+ throw error;
579
+ } finally {
580
+ clearTimeout(timeoutId);
581
+ }
582
+ }
583
+ /**
584
+ * Handle a 402 Payment Required response.
585
+ */
586
+ async handlePaymentRequired(url, init, response) {
587
+ let requirements;
588
+ try {
589
+ requirements = await parse402Response(response);
590
+ this.logger.debug("Payment requirements:", {
591
+ amount: `$${getAmountUsd(requirements).toFixed(4)}`,
592
+ recipient: requirements.recipient,
593
+ description: requirements.description
594
+ });
595
+ } catch (e) {
596
+ this.logger.error("Failed to parse payment requirements:", e);
597
+ throw new PaymentFailedError(
598
+ `Failed to parse payment requirements: ${e}. The server may not be properly configured for x402 payments.`
599
+ );
600
+ }
601
+ if (isPaymentExpired(requirements)) {
602
+ throw new PaymentFailedError(
603
+ "Payment requirements have expired. This usually means the request took too long. Try again."
604
+ );
605
+ }
606
+ const amountUsd = getAmountUsd(requirements);
607
+ if (this.maxPaymentUsd !== void 0 && amountUsd > this.maxPaymentUsd) {
608
+ throw new SpendingLimitExceededError(
609
+ "client_max",
610
+ this.maxPaymentUsd,
611
+ amountUsd
612
+ );
613
+ }
614
+ let xPayment;
615
+ try {
616
+ this.logger.debug("Signing payment authorization...");
617
+ xPayment = await buildXPaymentHeader(
618
+ requirements,
619
+ this.sessionKey,
620
+ this.walletAddress
621
+ );
622
+ } catch (e) {
623
+ this.logger.error("Failed to sign payment:", e);
624
+ throw new PaymentFailedError(
625
+ `Failed to sign payment: ${e}. This may indicate an issue with the session key.`
626
+ );
627
+ }
628
+ this.logger.debug("Retrying request with payment...");
629
+ const headers = new Headers(init?.headers);
630
+ headers.set("X-PAYMENT", xPayment);
631
+ const retryResponse = await fetch(url, {
632
+ ...init,
633
+ headers
634
+ });
635
+ if (retryResponse.status !== 402) {
636
+ const payment = {
637
+ amountUsd,
638
+ recipient: requirements.recipient,
639
+ txHash: retryResponse.headers.get("X-Payment-TxHash"),
640
+ timestamp: /* @__PURE__ */ new Date(),
641
+ description: requirements.description,
642
+ url
643
+ };
644
+ this.payments.push(payment);
645
+ this.totalSpentUsd += amountUsd;
646
+ this.logger.payment(amountUsd, requirements.recipient, requirements.description);
647
+ if (this.onPayment) {
648
+ this.onPayment(payment);
649
+ }
650
+ } else {
651
+ await this.handlePaymentError(retryResponse);
652
+ }
653
+ return retryResponse;
654
+ }
655
+ /**
656
+ * Handle payment-specific errors from the response.
657
+ */
658
+ async handlePaymentError(response) {
659
+ let errorData = {};
660
+ try {
661
+ errorData = await response.json();
662
+ } catch {
663
+ }
664
+ const errorCode = errorData.error_code || "";
665
+ const errorMessage = errorData.error || errorData.message || "Payment required";
666
+ this.logger.error("Payment failed:", { errorCode, errorMessage, errorData });
667
+ if (errorCode === "insufficient_balance") {
668
+ const required = errorData.required || 0;
669
+ const available = errorData.available || 0;
670
+ throw new InsufficientBalanceError(required, available);
671
+ } else if (errorCode === "session_key_expired") {
672
+ throw new SessionKeyExpiredError(errorData.expired_at || "unknown");
673
+ } else if (errorCode === "spending_limit_exceeded") {
674
+ throw new SpendingLimitExceededError(
675
+ errorData.limit_type || "unknown",
676
+ errorData.limit || 0,
677
+ errorData.attempted || 0
678
+ );
679
+ } else {
680
+ throw new PaymentFailedError(errorMessage);
681
+ }
682
+ }
683
+ // ===========================================================================
684
+ // Wallet Information
685
+ // ===========================================================================
686
+ /**
687
+ * Get the wallet address.
688
+ *
689
+ * @returns The Ethereum address of the smart wallet
690
+ */
691
+ getWalletAddress() {
692
+ return this.walletAddress;
693
+ }
694
+ /**
695
+ * Check if using testnet session key.
696
+ *
697
+ * @returns true if using Base Sepolia (testnet), false if using Base Mainnet
698
+ */
699
+ isTestnet() {
700
+ return this.sessionKey.isTest;
701
+ }
702
+ /**
703
+ * Get the network information.
704
+ *
705
+ * @returns Network details including chain ID and name
706
+ */
707
+ getNetwork() {
708
+ return this.isTestnet() ? NETWORKS.BASE_SEPOLIA : NETWORKS.BASE_MAINNET;
709
+ }
710
+ /**
711
+ * Get current USDC balance of the wallet.
712
+ *
713
+ * @returns USDC balance in USD
714
+ *
715
+ * @example
716
+ * ```typescript
717
+ * const balance = await wallet.getBalance();
718
+ * console.log(`Balance: $${balance.toFixed(2)}`);
719
+ * ```
720
+ */
721
+ async getBalance() {
722
+ this.logger.debug("Fetching wallet balance...");
723
+ try {
724
+ const response = await fetch(`${this.baseUrl}/v1/wallet/balance`, {
725
+ headers: {
726
+ "X-Session-Key": this.sessionKey.address
727
+ }
728
+ });
729
+ if (response.ok) {
730
+ const data = await response.json();
731
+ const balance = data.balance_usd || data.balanceUsd || 0;
732
+ this.logger.debug(`Balance: $${balance}`);
733
+ return balance;
734
+ }
735
+ } catch (error) {
736
+ this.logger.warn("Failed to fetch balance:", error);
737
+ }
738
+ this.logger.debug("Using estimated balance based on tracking");
739
+ return Math.max(0, 100 - this.totalSpentUsd);
740
+ }
741
+ /**
742
+ * Get information about the session key.
743
+ *
744
+ * @param refresh - Force refresh from server (default: use cache if < 60s old)
745
+ * @returns Session key details including limits and expiration
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * const info = await wallet.getSessionKeyInfo();
750
+ * console.log(`Daily limit: $${info.limits.dailyUsd}`);
751
+ * console.log(`Expires: ${info.expiresAt}`);
752
+ * ```
753
+ */
754
+ async getSessionKeyInfo(refresh = false) {
755
+ const cacheAge = this.sessionKeyInfoFetchedAt ? Date.now() - this.sessionKeyInfoFetchedAt : Infinity;
756
+ if (!refresh && this.sessionKeyInfo && cacheAge < 6e4) {
757
+ return this.sessionKeyInfo;
758
+ }
759
+ this.logger.debug("Fetching session key info...");
760
+ try {
761
+ const response = await fetch(`${this.baseUrl}/v1/session-key/info`, {
762
+ headers: {
763
+ "X-Session-Key": this.sessionKey.address
764
+ }
765
+ });
766
+ if (response.ok) {
767
+ const data = await response.json();
768
+ this.sessionKeyInfo = {
769
+ address: this.sessionKey.address,
770
+ isValid: data.is_valid ?? data.isValid ?? true,
771
+ limits: {
772
+ perTxUsd: data.per_tx_limit_usd ?? data.perTxLimitUsd ?? null,
773
+ dailyUsd: data.daily_limit_usd ?? data.dailyLimitUsd ?? null,
774
+ totalUsd: data.total_limit_usd ?? data.totalLimitUsd ?? null
775
+ },
776
+ usage: {
777
+ todayUsd: data.today_spent_usd ?? data.todaySpentUsd ?? 0,
778
+ totalUsd: data.total_spent_usd ?? data.totalSpentUsd ?? 0,
779
+ txCount: data.tx_count ?? data.txCount ?? 0
780
+ },
781
+ expiresAt: data.expires_at ? new Date(data.expires_at) : null,
782
+ createdAt: data.created_at ? new Date(data.created_at) : null,
783
+ name: data.name
784
+ };
785
+ this.sessionKeyInfoFetchedAt = Date.now();
786
+ return this.sessionKeyInfo;
787
+ }
788
+ } catch (error) {
789
+ this.logger.warn("Failed to fetch session key info:", error);
790
+ }
791
+ return {
792
+ address: this.sessionKey.address,
793
+ isValid: true,
794
+ limits: { perTxUsd: null, dailyUsd: null, totalUsd: null },
795
+ usage: { todayUsd: this.totalSpentUsd, totalUsd: this.totalSpentUsd, txCount: this.payments.length },
796
+ expiresAt: null,
797
+ createdAt: null
798
+ };
799
+ }
800
+ /**
801
+ * Get spending stats for this session key.
802
+ *
803
+ * @returns Spending statistics
804
+ *
805
+ * @example
806
+ * ```typescript
807
+ * const stats = await wallet.getSpendingStats();
808
+ * console.log(`Spent: $${stats.totalSpentUsd.toFixed(2)}`);
809
+ * console.log(`Remaining daily: $${stats.remainingDailyUsd?.toFixed(2) ?? 'unlimited'}`);
810
+ * ```
811
+ */
812
+ async getSpendingStats() {
813
+ this.logger.debug("Fetching spending stats...");
814
+ try {
815
+ const response = await fetch(`${this.baseUrl}/v1/session-key/stats`, {
816
+ headers: {
817
+ "X-Session-Key": this.sessionKey.address
818
+ }
819
+ });
820
+ if (response.ok) {
821
+ const data = await response.json();
822
+ return {
823
+ totalSpentUsd: data.total_spent_usd || data.totalSpentUsd || this.totalSpentUsd,
824
+ txCount: data.tx_count || data.txCount || this.payments.length,
825
+ remainingDailyUsd: data.remaining_daily_usd || data.remainingDailyUsd || null,
826
+ remainingTotalUsd: data.remaining_total_usd || data.remainingTotalUsd || null,
827
+ expiresAt: data.expires_at ? new Date(data.expires_at) : null
828
+ };
829
+ }
830
+ } catch (error) {
831
+ this.logger.warn("Failed to fetch spending stats:", error);
832
+ }
833
+ return {
834
+ totalSpentUsd: this.totalSpentUsd,
835
+ txCount: this.payments.length,
836
+ remainingDailyUsd: null,
837
+ remainingTotalUsd: null,
838
+ expiresAt: null
839
+ };
840
+ }
841
+ /**
842
+ * Get list of payments made in this session.
843
+ *
844
+ * @returns Array of payment events
845
+ */
846
+ getPaymentHistory() {
847
+ return [...this.payments];
848
+ }
849
+ /**
850
+ * Get the total amount spent in this session.
851
+ *
852
+ * @returns Total spent in USD
853
+ */
854
+ getTotalSpent() {
855
+ return this.totalSpentUsd;
856
+ }
857
+ // ===========================================================================
858
+ // Diagnostics
859
+ // ===========================================================================
860
+ /**
861
+ * Run diagnostics to verify the wallet is properly configured.
862
+ *
863
+ * This is useful for debugging integration issues.
864
+ *
865
+ * @returns Diagnostic results with status and any issues found
866
+ *
867
+ * @example
868
+ * ```typescript
869
+ * const diagnostics = await wallet.runDiagnostics();
870
+ * if (diagnostics.healthy) {
871
+ * console.log('✅ Wallet is ready to use');
872
+ * } else {
873
+ * console.log('❌ Issues found:');
874
+ * diagnostics.issues.forEach(issue => console.log(` - ${issue}`));
875
+ * }
876
+ * ```
877
+ */
878
+ async runDiagnostics() {
879
+ this.logger.info("Running diagnostics...");
880
+ const issues = [];
881
+ const checks = {};
882
+ checks.sessionKeyFormat = true;
883
+ try {
884
+ const response = await fetch(`${this.baseUrl}/health`, {
885
+ method: "GET",
886
+ signal: AbortSignal.timeout(5e3)
887
+ });
888
+ checks.apiConnectivity = response.ok;
889
+ if (!response.ok) {
890
+ issues.push(`API server returned ${response.status}. Check baseUrl configuration.`);
891
+ }
892
+ } catch {
893
+ checks.apiConnectivity = false;
894
+ issues.push("Cannot connect to MixrPay API. Check your network connection and baseUrl.");
895
+ }
896
+ try {
897
+ const info = await this.getSessionKeyInfo(true);
898
+ checks.sessionKeyValid = info.isValid;
899
+ if (!info.isValid) {
900
+ issues.push("Session key is invalid or has been revoked.");
901
+ }
902
+ if (info.expiresAt && info.expiresAt < /* @__PURE__ */ new Date()) {
903
+ checks.sessionKeyValid = false;
904
+ issues.push(`Session key expired on ${info.expiresAt.toISOString()}`);
905
+ }
906
+ } catch {
907
+ checks.sessionKeyValid = false;
908
+ issues.push("Could not verify session key validity.");
909
+ }
910
+ try {
911
+ const balance = await this.getBalance();
912
+ checks.hasBalance = balance > 0;
913
+ if (balance <= 0) {
914
+ issues.push("Wallet has no USDC balance. Top up at your MixrPay server /wallet");
915
+ } else if (balance < 1) {
916
+ issues.push(`Low balance: $${balance.toFixed(2)}. Consider topping up.`);
917
+ }
918
+ } catch {
919
+ checks.hasBalance = false;
920
+ issues.push("Could not fetch wallet balance.");
921
+ }
922
+ const healthy = issues.length === 0;
923
+ this.logger.info("Diagnostics complete:", { healthy, issues });
924
+ return {
925
+ healthy,
926
+ issues,
927
+ checks,
928
+ sdkVersion: SDK_VERSION,
929
+ network: this.getNetwork().name,
930
+ walletAddress: this.walletAddress
931
+ };
932
+ }
933
+ /**
934
+ * Enable or disable debug logging.
935
+ *
936
+ * @param enable - true to enable debug logging, false to disable
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * wallet.setDebug(true); // Enable detailed logs
941
+ * await wallet.fetch(...);
942
+ * wallet.setDebug(false); // Disable logs
943
+ * ```
944
+ */
945
+ setDebug(enable) {
946
+ this.logger.setLevel(enable ? "debug" : "none");
947
+ }
948
+ /**
949
+ * Set the log level.
950
+ *
951
+ * @param level - 'debug' | 'info' | 'warn' | 'error' | 'none'
952
+ */
953
+ setLogLevel(level) {
954
+ this.logger.setLevel(level);
955
+ }
956
+ };
957
+ // Annotate the CommonJS export names for ESM import in node:
958
+ 0 && (module.exports = {
959
+ AgentWallet,
960
+ DEFAULT_BASE_URL,
961
+ DEFAULT_FACILITATOR_URL,
962
+ DEFAULT_TIMEOUT,
963
+ InsufficientBalanceError,
964
+ InvalidSessionKeyError,
965
+ MixrPayError,
966
+ NETWORKS,
967
+ PaymentFailedError,
968
+ SDK_VERSION,
969
+ SessionKey,
970
+ SessionKeyExpiredError,
971
+ SpendingLimitExceededError,
972
+ X402ProtocolError,
973
+ buildTransferAuthorizationData,
974
+ buildXPaymentHeader,
975
+ generateNonce,
976
+ getAmountUsd,
977
+ getErrorMessage,
978
+ isMixrPayError,
979
+ isPaymentExpired,
980
+ parse402Response,
981
+ validatePaymentAmount
982
+ });