@mixrpay/agent-sdk 0.5.0 → 0.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/dist/index.js CHANGED
@@ -9,14 +9,43 @@ import {
9
9
  var MixrPayError = class extends Error {
10
10
  /** Error code for programmatic handling */
11
11
  code;
12
- constructor(message, code = "MIXRPAY_ERROR") {
12
+ /** Optional hint for how long to wait before retrying (in milliseconds) */
13
+ retryAfterMs;
14
+ constructor(message, code = "MIXRPAY_ERROR", retryAfterMs) {
13
15
  super(message);
14
16
  this.name = "MixrPayError";
15
17
  this.code = code;
18
+ this.retryAfterMs = retryAfterMs;
16
19
  if (Error.captureStackTrace) {
17
20
  Error.captureStackTrace(this, this.constructor);
18
21
  }
19
22
  }
23
+ /**
24
+ * Check if this error is retryable.
25
+ *
26
+ * Returns true if the operation might succeed on retry (e.g., transient network issues).
27
+ * Returns false if the error requires user action to resolve (e.g., insufficient balance).
28
+ *
29
+ * @returns true if the operation should be retried, false otherwise
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * try {
34
+ * await wallet.fetch(...);
35
+ * } catch (error) {
36
+ * if (error instanceof MixrPayError && error.isRetryable()) {
37
+ * // Retry the operation
38
+ * await retry();
39
+ * } else {
40
+ * // Handle permanent failure
41
+ * throw error;
42
+ * }
43
+ * }
44
+ * ```
45
+ */
46
+ isRetryable() {
47
+ return false;
48
+ }
20
49
  };
21
50
  var InsufficientBalanceError = class extends MixrPayError {
22
51
  /** Amount required for the payment in USD */
@@ -74,6 +103,14 @@ var SpendingLimitExceededError = class extends MixrPayError {
74
103
  this.limit = limit;
75
104
  this.attempted = attempted;
76
105
  }
106
+ /**
107
+ * Daily limits reset at midnight, so those are retryable (after waiting).
108
+ * Other limit types require user action (new session key, config change).
109
+ * @returns true only for daily limits
110
+ */
111
+ isRetryable() {
112
+ return this.limitType === "daily";
113
+ }
77
114
  };
78
115
  var PaymentFailedError = class extends MixrPayError {
79
116
  /** Detailed reason for the failure */
@@ -90,6 +127,13 @@ var PaymentFailedError = class extends MixrPayError {
90
127
  this.reason = reason;
91
128
  this.txHash = txHash;
92
129
  }
130
+ /**
131
+ * Payment failures are often transient (network issues, temporary server errors).
132
+ * @returns true - payment failures should generally be retried
133
+ */
134
+ isRetryable() {
135
+ return true;
136
+ }
93
137
  };
94
138
  var InvalidSessionKeyError = class extends MixrPayError {
95
139
  /** Detailed reason why the key is invalid */
@@ -114,6 +158,13 @@ var X402ProtocolError = class extends MixrPayError {
114
158
  this.name = "X402ProtocolError";
115
159
  this.reason = reason;
116
160
  }
161
+ /**
162
+ * Protocol errors may be caused by temporary server issues.
163
+ * @returns true - worth retrying in case server recovers
164
+ */
165
+ isRetryable() {
166
+ return true;
167
+ }
117
168
  };
118
169
  var SessionExpiredError = class extends MixrPayError {
119
170
  /** ID of the expired session */
@@ -181,6 +232,15 @@ var SessionRevokedError = class extends MixrPayError {
181
232
  function isMixrPayError(error) {
182
233
  return error instanceof MixrPayError;
183
234
  }
235
+ function getErrorMessage(error) {
236
+ if (error instanceof MixrPayError) {
237
+ return error.message;
238
+ }
239
+ if (error instanceof Error) {
240
+ return error.message;
241
+ }
242
+ return String(error);
243
+ }
184
244
 
185
245
  // src/session-key.ts
186
246
  var USDC_ADDRESSES = {
@@ -411,7 +471,7 @@ function getAmountUsd(requirements) {
411
471
  }
412
472
 
413
473
  // src/agent-wallet.ts
414
- var SDK_VERSION = "0.5.0";
474
+ var SDK_VERSION = "0.6.1";
415
475
  var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
416
476
  var DEFAULT_TIMEOUT = 3e4;
417
477
  var NETWORKS = {
@@ -603,7 +663,19 @@ var AgentWallet = class {
603
663
  });
604
664
  if (!registerResponse.ok) {
605
665
  const error = await registerResponse.json().catch(() => ({}));
606
- throw new MixrPayError(error.error || `Registration failed: ${registerResponse.status}`);
666
+ const errorMessage = error.error || `Registration failed with status ${registerResponse.status}`;
667
+ const requestId = error.request_id;
668
+ const errorCode = error.code;
669
+ let helpText = "";
670
+ if (registerResponse.status === 503) {
671
+ helpText = " The service may be temporarily unavailable. Please try again later.";
672
+ } else if (registerResponse.status === 500) {
673
+ helpText = " This is a server error. Please contact support with the request ID.";
674
+ } else if (errorCode === "MISSING_CHALLENGE" || errorCode === "MISSING_SIGNATURE") {
675
+ helpText = " This may indicate an SDK bug. Please update to the latest version.";
676
+ }
677
+ const fullMessage = requestId ? `${errorMessage} (request_id: ${requestId})${helpText}` : `${errorMessage}${helpText}`;
678
+ throw new MixrPayError(fullMessage);
607
679
  }
608
680
  const data = await registerResponse.json();
609
681
  return {
@@ -611,6 +683,52 @@ var AgentWallet = class {
611
683
  depositAddress: data.deposit_address
612
684
  };
613
685
  }
686
+ /**
687
+ * Check if the MixrPay server is properly configured for agent registration.
688
+ *
689
+ * Use this to diagnose registration issues before attempting to register.
690
+ *
691
+ * @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
692
+ * @returns Server health status including agent registration availability
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * const status = await AgentWallet.checkServerHealth();
697
+ * if (!status.agentRegistrationAvailable) {
698
+ * console.error('Agent registration is not available:', status);
699
+ * }
700
+ * ```
701
+ */
702
+ static async checkServerHealth(baseUrl) {
703
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
704
+ try {
705
+ const response = await fetch(`${url}/api/health/ready?details=true`);
706
+ if (!response.ok) {
707
+ return {
708
+ healthy: false,
709
+ database: "unknown",
710
+ agentRegistrationAvailable: false,
711
+ privyConfigured: false,
712
+ error: `Health check failed with status ${response.status}`
713
+ };
714
+ }
715
+ const data = await response.json();
716
+ return {
717
+ healthy: data.status === "ready",
718
+ database: data.database || "unknown",
719
+ agentRegistrationAvailable: data.services?.agentRegistration?.available ?? false,
720
+ privyConfigured: data.services?.privy?.configured ?? false
721
+ };
722
+ } catch (error) {
723
+ return {
724
+ healthy: false,
725
+ database: "unreachable",
726
+ agentRegistrationAvailable: false,
727
+ privyConfigured: false,
728
+ error: error instanceof Error ? error.message : "Failed to reach server"
729
+ };
730
+ }
731
+ }
614
732
  /**
615
733
  * Get a session key for an already-registered agent.
616
734
  *
@@ -785,6 +903,67 @@ var AgentWallet = class {
785
903
  }
786
904
  return true;
787
905
  }
906
+ /**
907
+ * Withdraw USDC from agent's MixrPay wallet to their external wallet.
908
+ *
909
+ * SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
910
+ * (the wallet used during `register()`). This prevents prompt injection
911
+ * attacks where a compromised agent might be tricked into withdrawing
912
+ * to an attacker's address.
913
+ *
914
+ * @param options - Withdrawal options
915
+ * @returns Withdrawal result with transaction hash
916
+ * @throws {MixrPayError} If withdrawal fails
917
+ *
918
+ * @example
919
+ * ```typescript
920
+ * const result = await AgentWallet.withdraw({
921
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
922
+ * amountUsd: 50.00,
923
+ * });
924
+ *
925
+ * console.log(`Withdrew $${result.amountUsd}`);
926
+ * console.log(`Transaction: ${result.txHash}`);
927
+ * console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
928
+ * ```
929
+ */
930
+ static async withdraw(options) {
931
+ const { privateKey, amountUsd } = options;
932
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
933
+ const account = privateKeyToAccount2(privateKey);
934
+ const walletAddress = account.address;
935
+ const challengeResponse = await fetch(
936
+ `${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=withdraw`
937
+ );
938
+ if (!challengeResponse.ok) {
939
+ const error = await challengeResponse.json().catch(() => ({}));
940
+ throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
941
+ }
942
+ const { challenge, message } = await challengeResponse.json();
943
+ const signature = await signMessage({ message, privateKey });
944
+ const withdrawResponse = await fetch(`${baseUrl}/api/v1/agent/withdraw`, {
945
+ method: "POST",
946
+ headers: { "Content-Type": "application/json" },
947
+ body: JSON.stringify({
948
+ challenge,
949
+ external_wallet: walletAddress,
950
+ signature,
951
+ to_address: walletAddress,
952
+ // Always withdraw to self
953
+ amount_usd: amountUsd
954
+ })
955
+ });
956
+ if (!withdrawResponse.ok) {
957
+ const error = await withdrawResponse.json().catch(() => ({}));
958
+ throw new MixrPayError(error.error || `Withdrawal failed: ${withdrawResponse.status}`);
959
+ }
960
+ const data = await withdrawResponse.json();
961
+ return {
962
+ txHash: data.tx_hash,
963
+ amountUsd: data.amount_usd,
964
+ remainingBalanceUsd: data.remaining_balance_usd
965
+ };
966
+ }
788
967
  // ===========================================================================
789
968
  // Core Methods
790
969
  // ===========================================================================
@@ -823,6 +1002,8 @@ var AgentWallet = class {
823
1002
  */
824
1003
  async fetch(url, init) {
825
1004
  this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
1005
+ const requestId = crypto.randomUUID();
1006
+ const correlationId = this.extractCorrelationId(init?.headers);
826
1007
  const controller = new AbortController();
827
1008
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
828
1009
  try {
@@ -833,7 +1014,7 @@ var AgentWallet = class {
833
1014
  this.logger.debug(`Initial response: ${response.status}`);
834
1015
  if (response.status === 402) {
835
1016
  this.logger.info(`Payment required for ${url}`);
836
- response = await this.handlePaymentRequired(url, init, response);
1017
+ response = await this.handlePaymentRequired(url, init, response, requestId, correlationId);
837
1018
  }
838
1019
  return response;
839
1020
  } catch (error) {
@@ -845,10 +1026,27 @@ var AgentWallet = class {
845
1026
  clearTimeout(timeoutId);
846
1027
  }
847
1028
  }
1029
+ /**
1030
+ * Extract correlation ID from request headers.
1031
+ */
1032
+ extractCorrelationId(headers) {
1033
+ if (!headers) return void 0;
1034
+ if (headers instanceof Headers) {
1035
+ return headers.get("X-Correlation-Id") || headers.get("x-correlation-id") || void 0;
1036
+ }
1037
+ if (Array.isArray(headers)) {
1038
+ const entry = headers.find(
1039
+ ([key]) => key.toLowerCase() === "x-correlation-id"
1040
+ );
1041
+ return entry ? entry[1] : void 0;
1042
+ }
1043
+ const record = headers;
1044
+ return record["X-Correlation-Id"] || record["x-correlation-id"] || void 0;
1045
+ }
848
1046
  /**
849
1047
  * Handle a 402 Payment Required response.
850
1048
  */
851
- async handlePaymentRequired(url, init, response) {
1049
+ async handlePaymentRequired(url, init, response, requestId, correlationId) {
852
1050
  let requirements;
853
1051
  try {
854
1052
  requirements = await parse402Response(response);
@@ -904,7 +1102,9 @@ var AgentWallet = class {
904
1102
  txHash: retryResponse.headers.get("X-Payment-TxHash"),
905
1103
  timestamp: /* @__PURE__ */ new Date(),
906
1104
  description: requirements.description,
907
- url
1105
+ url,
1106
+ requestId,
1107
+ correlationId
908
1108
  };
909
1109
  this.payments.push(payment);
910
1110
  this.totalSpentUsd += amountUsd;
@@ -1003,6 +1203,36 @@ var AgentWallet = class {
1003
1203
  this.logger.debug("Using estimated balance based on tracking");
1004
1204
  return Math.max(0, 100 - this.totalSpentUsd);
1005
1205
  }
1206
+ /**
1207
+ * Check if the wallet can afford a specific amount.
1208
+ *
1209
+ * This is a convenience method to check balance before making a request
1210
+ * when you know the expected cost.
1211
+ *
1212
+ * @param amountUsd - Amount to check in USD
1213
+ * @returns Object with affordability information
1214
+ *
1215
+ * @example
1216
+ * ```typescript
1217
+ * const check = await wallet.canAfford(5.00);
1218
+ * if (check.canAfford) {
1219
+ * console.log(`Can afford! Will have $${check.remainingAfter.toFixed(2)} left`);
1220
+ * await wallet.fetch(url);
1221
+ * } else {
1222
+ * console.log(`Need $${check.shortfall.toFixed(2)} more`);
1223
+ * }
1224
+ * ```
1225
+ */
1226
+ async canAfford(amountUsd) {
1227
+ const balance = await this.getBalance();
1228
+ const canAfford = balance >= amountUsd;
1229
+ return {
1230
+ canAfford,
1231
+ balance,
1232
+ shortfall: canAfford ? 0 : amountUsd - balance,
1233
+ remainingAfter: canAfford ? balance - amountUsd : 0
1234
+ };
1235
+ }
1006
1236
  /**
1007
1237
  * Get information about the session key.
1008
1238
  *
@@ -1143,56 +1373,100 @@ var AgentWallet = class {
1143
1373
  async runDiagnostics() {
1144
1374
  this.logger.info("Running diagnostics...");
1145
1375
  const issues = [];
1376
+ const recommendations = [];
1146
1377
  const checks = {};
1378
+ let latencyMs;
1379
+ let sessionLimits;
1147
1380
  checks.sessionKeyFormat = true;
1148
1381
  try {
1382
+ const startTime = Date.now();
1149
1383
  const response = await fetch(`${this.baseUrl}/health`, {
1150
1384
  method: "GET",
1151
1385
  signal: AbortSignal.timeout(5e3)
1152
1386
  });
1387
+ latencyMs = Date.now() - startTime;
1153
1388
  checks.apiConnectivity = response.ok;
1154
1389
  if (!response.ok) {
1155
1390
  issues.push(`API server returned ${response.status}. Check baseUrl configuration.`);
1391
+ recommendations.push("Verify the baseUrl configuration points to a valid MixrPay server.");
1392
+ }
1393
+ if (latencyMs > 2e3) {
1394
+ issues.push(`High API latency: ${latencyMs}ms. This may cause timeouts.`);
1395
+ recommendations.push("Consider using a server closer to your region or check network connectivity.");
1156
1396
  }
1157
1397
  } catch {
1158
1398
  checks.apiConnectivity = false;
1159
1399
  issues.push("Cannot connect to MixrPay API. Check your network connection and baseUrl.");
1400
+ recommendations.push("Verify network connectivity and that the MixrPay server is running.");
1160
1401
  }
1161
1402
  try {
1162
1403
  const info = await this.getSessionKeyInfo(true);
1163
1404
  checks.sessionKeyValid = info.isValid;
1164
1405
  if (!info.isValid) {
1165
1406
  issues.push("Session key is invalid or has been revoked.");
1407
+ recommendations.push("Request a new session key from the wallet owner or create one at /wallet/sessions.");
1408
+ }
1409
+ const now = /* @__PURE__ */ new Date();
1410
+ let expiresInHours = null;
1411
+ if (info.expiresAt) {
1412
+ expiresInHours = (info.expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60);
1413
+ if (info.expiresAt < now) {
1414
+ checks.sessionKeyValid = false;
1415
+ issues.push(`Session key expired on ${info.expiresAt.toISOString()}`);
1416
+ recommendations.push("Create a new session key to continue making payments.");
1417
+ } else if (expiresInHours < 24) {
1418
+ issues.push(`Session key expires in ${expiresInHours.toFixed(1)} hours.`);
1419
+ recommendations.push("Consider creating a new session key before the current one expires.");
1420
+ }
1166
1421
  }
1167
- if (info.expiresAt && info.expiresAt < /* @__PURE__ */ new Date()) {
1168
- checks.sessionKeyValid = false;
1169
- issues.push(`Session key expired on ${info.expiresAt.toISOString()}`);
1422
+ const stats = await this.getSpendingStats();
1423
+ sessionLimits = {
1424
+ remainingDailyUsd: stats.remainingDailyUsd,
1425
+ remainingTotalUsd: stats.remainingTotalUsd,
1426
+ expiresAt: info.expiresAt,
1427
+ expiresInHours
1428
+ };
1429
+ if (stats.remainingDailyUsd !== null && stats.remainingDailyUsd < 1) {
1430
+ issues.push(`Daily limit nearly exhausted: $${stats.remainingDailyUsd.toFixed(2)} remaining.`);
1431
+ recommendations.push("Wait until tomorrow for daily limit to reset, or request a higher daily limit.");
1432
+ }
1433
+ if (stats.remainingTotalUsd !== null && stats.remainingTotalUsd < 1) {
1434
+ issues.push(`Total limit nearly exhausted: $${stats.remainingTotalUsd.toFixed(2)} remaining.`);
1435
+ recommendations.push("Request a new session key with a higher total limit.");
1170
1436
  }
1171
1437
  } catch {
1172
1438
  checks.sessionKeyValid = false;
1173
1439
  issues.push("Could not verify session key validity.");
1440
+ recommendations.push("Check network connectivity and try again.");
1174
1441
  }
1442
+ let balance = 0;
1175
1443
  try {
1176
- const balance = await this.getBalance();
1444
+ balance = await this.getBalance();
1177
1445
  checks.hasBalance = balance > 0;
1178
1446
  if (balance <= 0) {
1179
1447
  issues.push("Wallet has no USDC balance. Top up at your MixrPay server /wallet");
1448
+ recommendations.push("Deposit USDC to your wallet address to enable payments.");
1180
1449
  } else if (balance < 1) {
1181
1450
  issues.push(`Low balance: $${balance.toFixed(2)}. Consider topping up.`);
1451
+ recommendations.push("Top up your wallet balance to avoid payment failures.");
1182
1452
  }
1183
1453
  } catch {
1184
1454
  checks.hasBalance = false;
1185
1455
  issues.push("Could not fetch wallet balance.");
1456
+ recommendations.push("Check network connectivity and try again.");
1186
1457
  }
1187
1458
  const healthy = issues.length === 0;
1188
- this.logger.info("Diagnostics complete:", { healthy, issues });
1459
+ this.logger.info("Diagnostics complete:", { healthy, issues, latencyMs });
1189
1460
  return {
1190
1461
  healthy,
1191
1462
  issues,
1192
1463
  checks,
1193
1464
  sdkVersion: SDK_VERSION,
1194
1465
  network: this.getNetwork().name,
1195
- walletAddress: this.walletAddress
1466
+ walletAddress: this.walletAddress,
1467
+ sessionLimits,
1468
+ latencyMs,
1469
+ recommendations
1196
1470
  };
1197
1471
  }
1198
1472
  /**
@@ -1601,7 +1875,9 @@ var AgentWallet = class {
1601
1875
  txHash: response.headers.get("X-Payment-TxHash"),
1602
1876
  timestamp: /* @__PURE__ */ new Date(),
1603
1877
  description: feature || "API call",
1604
- url
1878
+ url,
1879
+ requestId: crypto.randomUUID(),
1880
+ correlationId: this.extractCorrelationId(customHeaders)
1605
1881
  };
1606
1882
  this.payments.push(payment);
1607
1883
  this.totalSpentUsd += amountUsd;
@@ -1800,7 +2076,8 @@ Timestamp: ${timestamp}`;
1800
2076
  txHash: mixrpay.txHash,
1801
2077
  timestamp: /* @__PURE__ */ new Date(),
1802
2078
  description: `MCP: ${toolName}`,
1803
- url: `${this.baseUrl}/api/mcp`
2079
+ url: `${this.baseUrl}/api/mcp`,
2080
+ requestId: crypto.randomUUID()
1804
2081
  };
1805
2082
  this.payments.push(payment);
1806
2083
  this.totalSpentUsd += mixrpay.chargedUsd;
@@ -1917,7 +2194,8 @@ Timestamp: ${timestamp}`;
1917
2194
  txHash: data.tx_hash,
1918
2195
  timestamp: /* @__PURE__ */ new Date(),
1919
2196
  description: `Agent run: ${data.run_id}`,
1920
- url: `${this.baseUrl}/api/v2/agent/run`
2197
+ url: `${this.baseUrl}/api/v2/agent/run`,
2198
+ requestId: idempotencyKey || crypto.randomUUID()
1921
2199
  };
1922
2200
  this.payments.push(payment);
1923
2201
  this.totalSpentUsd += data.cost.total_usd;
@@ -2009,7 +2287,8 @@ Timestamp: ${timestamp}`;
2009
2287
  txHash: data.tx_hash,
2010
2288
  timestamp: /* @__PURE__ */ new Date(),
2011
2289
  description: `Agent run: ${data.run_id}`,
2012
- url: `${this.baseUrl}/api/v2/agent/run`
2290
+ url: `${this.baseUrl}/api/v2/agent/run`,
2291
+ requestId: body.idempotency_key || crypto.randomUUID()
2013
2292
  };
2014
2293
  this.payments.push(payment);
2015
2294
  this.totalSpentUsd += data.total_cost_usd;
@@ -2185,7 +2464,8 @@ Timestamp: ${timestamp}`;
2185
2464
  txHash: mixrpay.txHash,
2186
2465
  timestamp: /* @__PURE__ */ new Date(),
2187
2466
  description: `MCP: ${toolName}`,
2188
- url: `${this.baseUrl}/api/mcp`
2467
+ url: `${this.baseUrl}/api/mcp`,
2468
+ requestId: crypto.randomUUID()
2189
2469
  };
2190
2470
  this.payments.push(payment);
2191
2471
  this.totalSpentUsd += mixrpay.chargedUsd;
@@ -2215,5 +2495,7 @@ export {
2215
2495
  SessionNotFoundError,
2216
2496
  SessionRevokedError,
2217
2497
  SpendingLimitExceededError,
2498
+ X402ProtocolError,
2499
+ getErrorMessage,
2218
2500
  isMixrPayError
2219
2501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mixrpay/agent-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "MixrPay Agent SDK - Enable AI agents to make x402 payments with session keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",