@mixrpay/agent-sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,9 +41,13 @@ await wallet.fetch(url, options);
41
41
  // Get wallet balance
42
42
  const balance = await wallet.getBalance();
43
43
 
44
- // Track payments
45
- const payments = wallet.getPaymentHistory();
46
- const total = wallet.getTotalSpent();
44
+ // Pre-flight balance check
45
+ const check = await wallet.canAfford(0.50);
46
+ if (check.canAfford) { /* proceed */ }
47
+
48
+ // Run diagnostics
49
+ const diag = await wallet.runDiagnostics();
50
+ console.log(diag.recommendations);
47
51
  ```
48
52
 
49
53
  ## Configuration
@@ -63,6 +67,7 @@ import {
63
67
  AgentWallet,
64
68
  InsufficientBalanceError,
65
69
  SessionLimitExceededError,
70
+ MixrPayError,
66
71
  } from '@mixrpay/agent-sdk';
67
72
 
68
73
  try {
@@ -71,12 +76,27 @@ try {
71
76
  if (error instanceof InsufficientBalanceError) {
72
77
  console.log(`Need $${error.required}, have $${error.available}`);
73
78
  }
79
+
80
+ // Check if error is retryable
81
+ if (error instanceof MixrPayError && error.isRetryable()) {
82
+ const delay = error.retryAfterMs || 1000;
83
+ await sleep(delay);
84
+ return retry();
85
+ }
74
86
  }
75
87
  ```
76
88
 
89
+ ## What's New in v0.6.0
90
+
91
+ - `canAfford(amountUsd)` - Pre-flight balance check
92
+ - `isRetryable()` on all errors - Determine if operation should be retried
93
+ - `retryAfterMs` on errors - Suggested retry delay
94
+ - Enhanced `runDiagnostics()` - Session limits, latency, recommendations
95
+ - `requestId` and `correlationId` in payment events
96
+
77
97
  ## Documentation
78
98
 
79
- [mixrpay.com/build](https://www.mixrpay.com/build)
99
+ [mixrpay.com/docs](https://www.mixrpay.com/docs)
80
100
 
81
101
  ## License
82
102
 
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  AgentWallet: () => AgentWallet,
24
24
  InsufficientBalanceError: () => InsufficientBalanceError,
25
25
  InvalidSessionKeyError: () => InvalidSessionKeyError,
26
+ MerchantNotAllowedError: () => MerchantNotAllowedError,
26
27
  MixrPayError: () => MixrPayError,
27
28
  PaymentFailedError: () => PaymentFailedError,
28
29
  SDK_VERSION: () => SDK_VERSION,
@@ -265,6 +266,23 @@ var SessionRevokedError = class extends MixrPayError {
265
266
  this.reason = reason;
266
267
  }
267
268
  };
269
+ var MerchantNotAllowedError = class extends MixrPayError {
270
+ /** The merchant/tool that was attempted */
271
+ attempted;
272
+ /** The patterns that are allowed by the session */
273
+ allowedPatterns;
274
+ constructor(attempted, allowedPatterns) {
275
+ const patternsPreview = allowedPatterns.slice(0, 3).join(", ");
276
+ const suffix = allowedPatterns.length > 3 ? "..." : "";
277
+ super(
278
+ `Payment to "${attempted}" not allowed. Session allowlist: ${patternsPreview}${suffix}. Update the session allowlist or create a new session.`,
279
+ "MERCHANT_NOT_ALLOWED"
280
+ );
281
+ this.name = "MerchantNotAllowedError";
282
+ this.attempted = attempted;
283
+ this.allowedPatterns = allowedPatterns;
284
+ }
285
+ };
268
286
  function isMixrPayError(error) {
269
287
  return error instanceof MixrPayError;
270
288
  }
@@ -507,7 +525,7 @@ function getAmountUsd(requirements) {
507
525
  }
508
526
 
509
527
  // src/agent-wallet.ts
510
- var SDK_VERSION = "0.6.0";
528
+ var SDK_VERSION = "0.6.1";
511
529
  var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
512
530
  var DEFAULT_TIMEOUT = 3e4;
513
531
  var NETWORKS = {
@@ -575,6 +593,9 @@ var AgentWallet = class {
575
593
  // Session key info cache
576
594
  sessionKeyInfo;
577
595
  sessionKeyInfoFetchedAt;
596
+ // Merchant allowlist (fetched from server)
597
+ allowlist;
598
+ allowlistFetchedAt;
578
599
  /**
579
600
  * Create a new AgentWallet instance.
580
601
  *
@@ -651,6 +672,126 @@ var AgentWallet = class {
651
672
  }
652
673
  }
653
674
  // ===========================================================================
675
+ // Merchant Allowlist Validation
676
+ // ===========================================================================
677
+ /**
678
+ * Fetch the merchant allowlist from the server.
679
+ * Caches the result for 5 minutes to avoid excessive API calls.
680
+ */
681
+ async fetchAllowlist() {
682
+ const CACHE_TTL_MS = 5 * 60 * 1e3;
683
+ if (this.allowlist !== void 0 && this.allowlistFetchedAt) {
684
+ const age = Date.now() - this.allowlistFetchedAt;
685
+ if (age < CACHE_TTL_MS) {
686
+ return this.allowlist;
687
+ }
688
+ }
689
+ try {
690
+ const info = await this.getSessionKeyInfo();
691
+ this.allowlist = info.allowedMerchants || [];
692
+ this.allowlistFetchedAt = Date.now();
693
+ this.logger.debug("Fetched allowlist", {
694
+ patterns: this.allowlist.length,
695
+ allowAll: this.allowlist.length === 0
696
+ });
697
+ return this.allowlist;
698
+ } catch (error) {
699
+ this.logger.warn("Failed to fetch allowlist, allowing all merchants", { error });
700
+ this.allowlist = [];
701
+ this.allowlistFetchedAt = Date.now();
702
+ return this.allowlist;
703
+ }
704
+ }
705
+ /**
706
+ * Validate that a target is allowed by the session's allowlist.
707
+ *
708
+ * @param target - URL, domain, or tool name to check
709
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
710
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
711
+ */
712
+ async validateMerchantAllowed(target, type) {
713
+ const allowlist = await this.fetchAllowlist();
714
+ if (allowlist.length === 0) {
715
+ return;
716
+ }
717
+ const isAllowed = this.matchesAllowlist(target, allowlist, type);
718
+ if (!isAllowed) {
719
+ throw new MerchantNotAllowedError(target, allowlist);
720
+ }
721
+ }
722
+ /**
723
+ * Check if a target matches any pattern in the allowlist.
724
+ */
725
+ matchesAllowlist(target, allowlist, type) {
726
+ for (const pattern of allowlist) {
727
+ if (this.matchPattern(target, pattern, type)) {
728
+ return true;
729
+ }
730
+ }
731
+ return false;
732
+ }
733
+ /**
734
+ * Check if a target matches a single pattern.
735
+ */
736
+ matchPattern(target, pattern, type) {
737
+ if (type === "url") {
738
+ return this.matchUrlPattern(target, pattern);
739
+ } else {
740
+ return this.matchToolPattern(target, pattern);
741
+ }
742
+ }
743
+ /**
744
+ * Match a URL or domain against a pattern.
745
+ */
746
+ matchUrlPattern(target, pattern) {
747
+ let hostname;
748
+ try {
749
+ if (target.includes("://")) {
750
+ const url = new URL(target);
751
+ hostname = url.hostname.toLowerCase();
752
+ } else {
753
+ hostname = target.toLowerCase();
754
+ }
755
+ } catch {
756
+ return false;
757
+ }
758
+ const normalizedPattern = pattern.toLowerCase().trim();
759
+ let patternDomain = normalizedPattern;
760
+ if (patternDomain.includes("://")) {
761
+ try {
762
+ patternDomain = new URL(patternDomain).hostname;
763
+ } catch {
764
+ }
765
+ }
766
+ if (hostname === patternDomain) {
767
+ return true;
768
+ }
769
+ if (patternDomain.startsWith("*.")) {
770
+ const baseDomain = patternDomain.substring(2);
771
+ if (hostname.endsWith(`.${baseDomain}`) && hostname !== baseDomain) {
772
+ return true;
773
+ }
774
+ }
775
+ return false;
776
+ }
777
+ /**
778
+ * Match an MCP tool name against a pattern.
779
+ */
780
+ matchToolPattern(toolName, pattern) {
781
+ const normalizedTool = toolName.toLowerCase().trim();
782
+ const normalizedPattern = pattern.toLowerCase().trim();
783
+ if (normalizedTool === normalizedPattern) {
784
+ return true;
785
+ }
786
+ if (normalizedPattern.endsWith("/*")) {
787
+ const baseProvider = normalizedPattern.substring(0, normalizedPattern.length - 2);
788
+ if (normalizedTool.startsWith(`${baseProvider}/`)) {
789
+ return true;
790
+ }
791
+ }
792
+ return false;
793
+ }
794
+ // ===========================================================================
654
795
  // Static Agent Registration Methods
655
796
  // ===========================================================================
656
797
  /**
@@ -699,7 +840,19 @@ var AgentWallet = class {
699
840
  });
700
841
  if (!registerResponse.ok) {
701
842
  const error = await registerResponse.json().catch(() => ({}));
702
- throw new MixrPayError(error.error || `Registration failed: ${registerResponse.status}`);
843
+ const errorMessage = error.error || `Registration failed with status ${registerResponse.status}`;
844
+ const requestId = error.request_id;
845
+ const errorCode = error.code;
846
+ let helpText = "";
847
+ if (registerResponse.status === 503) {
848
+ helpText = " The service may be temporarily unavailable. Please try again later.";
849
+ } else if (registerResponse.status === 500) {
850
+ helpText = " This is a server error. Please contact support with the request ID.";
851
+ } else if (errorCode === "MISSING_CHALLENGE" || errorCode === "MISSING_SIGNATURE") {
852
+ helpText = " This may indicate an SDK bug. Please update to the latest version.";
853
+ }
854
+ const fullMessage = requestId ? `${errorMessage} (request_id: ${requestId})${helpText}` : `${errorMessage}${helpText}`;
855
+ throw new MixrPayError(fullMessage);
703
856
  }
704
857
  const data = await registerResponse.json();
705
858
  return {
@@ -707,6 +860,52 @@ var AgentWallet = class {
707
860
  depositAddress: data.deposit_address
708
861
  };
709
862
  }
863
+ /**
864
+ * Check if the MixrPay server is properly configured for agent registration.
865
+ *
866
+ * Use this to diagnose registration issues before attempting to register.
867
+ *
868
+ * @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
869
+ * @returns Server health status including agent registration availability
870
+ *
871
+ * @example
872
+ * ```typescript
873
+ * const status = await AgentWallet.checkServerHealth();
874
+ * if (!status.agentRegistrationAvailable) {
875
+ * console.error('Agent registration is not available:', status);
876
+ * }
877
+ * ```
878
+ */
879
+ static async checkServerHealth(baseUrl) {
880
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
881
+ try {
882
+ const response = await fetch(`${url}/api/health/ready?details=true`);
883
+ if (!response.ok) {
884
+ return {
885
+ healthy: false,
886
+ database: "unknown",
887
+ agentRegistrationAvailable: false,
888
+ privyConfigured: false,
889
+ error: `Health check failed with status ${response.status}`
890
+ };
891
+ }
892
+ const data = await response.json();
893
+ return {
894
+ healthy: data.status === "ready",
895
+ database: data.database || "unknown",
896
+ agentRegistrationAvailable: data.services?.agentRegistration?.available ?? false,
897
+ privyConfigured: data.services?.privy?.configured ?? false
898
+ };
899
+ } catch (error) {
900
+ return {
901
+ healthy: false,
902
+ database: "unreachable",
903
+ agentRegistrationAvailable: false,
904
+ privyConfigured: false,
905
+ error: error instanceof Error ? error.message : "Failed to reach server"
906
+ };
907
+ }
908
+ }
710
909
  /**
711
910
  * Get a session key for an already-registered agent.
712
911
  *
@@ -881,6 +1080,67 @@ var AgentWallet = class {
881
1080
  }
882
1081
  return true;
883
1082
  }
1083
+ /**
1084
+ * Withdraw USDC from agent's MixrPay wallet to their external wallet.
1085
+ *
1086
+ * SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
1087
+ * (the wallet used during `register()`). This prevents prompt injection
1088
+ * attacks where a compromised agent might be tricked into withdrawing
1089
+ * to an attacker's address.
1090
+ *
1091
+ * @param options - Withdrawal options
1092
+ * @returns Withdrawal result with transaction hash
1093
+ * @throws {MixrPayError} If withdrawal fails
1094
+ *
1095
+ * @example
1096
+ * ```typescript
1097
+ * const result = await AgentWallet.withdraw({
1098
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
1099
+ * amountUsd: 50.00,
1100
+ * });
1101
+ *
1102
+ * console.log(`Withdrew $${result.amountUsd}`);
1103
+ * console.log(`Transaction: ${result.txHash}`);
1104
+ * console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
1105
+ * ```
1106
+ */
1107
+ static async withdraw(options) {
1108
+ const { privateKey, amountUsd } = options;
1109
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1110
+ const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
1111
+ const walletAddress = account.address;
1112
+ const challengeResponse = await fetch(
1113
+ `${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=withdraw`
1114
+ );
1115
+ if (!challengeResponse.ok) {
1116
+ const error = await challengeResponse.json().catch(() => ({}));
1117
+ throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
1118
+ }
1119
+ const { challenge, message } = await challengeResponse.json();
1120
+ const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
1121
+ const withdrawResponse = await fetch(`${baseUrl}/api/v1/agent/withdraw`, {
1122
+ method: "POST",
1123
+ headers: { "Content-Type": "application/json" },
1124
+ body: JSON.stringify({
1125
+ challenge,
1126
+ external_wallet: walletAddress,
1127
+ signature,
1128
+ to_address: walletAddress,
1129
+ // Always withdraw to self
1130
+ amount_usd: amountUsd
1131
+ })
1132
+ });
1133
+ if (!withdrawResponse.ok) {
1134
+ const error = await withdrawResponse.json().catch(() => ({}));
1135
+ throw new MixrPayError(error.error || `Withdrawal failed: ${withdrawResponse.status}`);
1136
+ }
1137
+ const data = await withdrawResponse.json();
1138
+ return {
1139
+ txHash: data.tx_hash,
1140
+ amountUsd: data.amount_usd,
1141
+ remainingBalanceUsd: data.remaining_balance_usd
1142
+ };
1143
+ }
884
1144
  // ===========================================================================
885
1145
  // Core Methods
886
1146
  // ===========================================================================
@@ -919,6 +1179,7 @@ var AgentWallet = class {
919
1179
  */
920
1180
  async fetch(url, init) {
921
1181
  this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
1182
+ await this.validateMerchantAllowed(url, "url");
922
1183
  const requestId = crypto.randomUUID();
923
1184
  const correlationId = this.extractCorrelationId(init?.headers);
924
1185
  const controller = new AbortController();
@@ -1192,7 +1453,8 @@ var AgentWallet = class {
1192
1453
  },
1193
1454
  expiresAt: data.expires_at ? new Date(data.expires_at) : null,
1194
1455
  createdAt: data.created_at ? new Date(data.created_at) : null,
1195
- name: data.name
1456
+ name: data.name,
1457
+ allowedMerchants: data.allowed_merchants ?? data.allowedMerchants ?? []
1196
1458
  };
1197
1459
  this.sessionKeyInfoFetchedAt = Date.now();
1198
1460
  return this.sessionKeyInfo;
@@ -1758,6 +2020,7 @@ var AgentWallet = class {
1758
2020
  feature
1759
2021
  } = options;
1760
2022
  this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
2023
+ await this.validateMerchantAllowed(url, "url");
1761
2024
  if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1762
2025
  throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1763
2026
  }
@@ -1965,6 +2228,7 @@ Timestamp: ${timestamp}`;
1965
2228
  */
1966
2229
  async callMCPTool(toolName, args = {}) {
1967
2230
  this.logger.debug("callMCPTool", { toolName, args });
2231
+ await this.validateMerchantAllowed(toolName, "tool");
1968
2232
  const authHeaders = await this.getMCPAuthHeaders();
1969
2233
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
1970
2234
  method: "POST",
@@ -2354,6 +2618,7 @@ Timestamp: ${timestamp}`;
2354
2618
  */
2355
2619
  async callMCPToolWithSession(sessionId, toolName, args = {}) {
2356
2620
  this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
2621
+ await this.validateMerchantAllowed(toolName, "tool");
2357
2622
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2358
2623
  method: "POST",
2359
2624
  headers: {
@@ -2404,6 +2669,7 @@ Timestamp: ${timestamp}`;
2404
2669
  AgentWallet,
2405
2670
  InsufficientBalanceError,
2406
2671
  InvalidSessionKeyError,
2672
+ MerchantNotAllowedError,
2407
2673
  MixrPayError,
2408
2674
  PaymentFailedError,
2409
2675
  SDK_VERSION,
package/dist/index.d.cts CHANGED
@@ -158,6 +158,8 @@ interface SessionKeyInfo {
158
158
  createdAt: Date | null;
159
159
  /** Optional name given to this session key */
160
160
  name?: string;
161
+ /** Allowed merchants/tools (empty = allow all) */
162
+ allowedMerchants?: string[];
161
163
  }
162
164
  /**
163
165
  * Spending statistics for the current session.
@@ -378,7 +380,7 @@ interface SessionStats {
378
380
  */
379
381
 
380
382
  /** Current SDK version */
381
- declare const SDK_VERSION = "0.6.0";
383
+ declare const SDK_VERSION = "0.6.1";
382
384
  /** Supported networks */
383
385
  declare const NETWORKS: {
384
386
  readonly BASE_MAINNET: {
@@ -487,6 +489,28 @@ interface AgentRevokeSessionKeyOptions {
487
489
  /** MixrPay API base URL (default: https://www.mixrpay.com) */
488
490
  baseUrl?: string;
489
491
  }
492
+ /**
493
+ * Options for withdrawing funds
494
+ */
495
+ interface AgentWithdrawOptions {
496
+ /** The agent's external wallet private key (used for signing, NOT transmitted) */
497
+ privateKey: `0x${string}`;
498
+ /** Amount to withdraw in USD */
499
+ amountUsd: number;
500
+ /** MixrPay API base URL (default: https://www.mixrpay.com) */
501
+ baseUrl?: string;
502
+ }
503
+ /**
504
+ * Result from withdrawal
505
+ */
506
+ interface AgentWithdrawResult {
507
+ /** On-chain transaction hash */
508
+ txHash: string;
509
+ /** Amount withdrawn in USD */
510
+ amountUsd: number;
511
+ /** Remaining balance after withdrawal */
512
+ remainingBalanceUsd: number;
513
+ }
490
514
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
491
515
  /**
492
516
  * A wallet wrapper for AI agents that handles x402 payments automatically.
@@ -539,6 +563,8 @@ declare class AgentWallet {
539
563
  private totalSpentUsd;
540
564
  private sessionKeyInfo?;
541
565
  private sessionKeyInfoFetchedAt?;
566
+ private allowlist?;
567
+ private allowlistFetchedAt?;
542
568
  /**
543
569
  * Create a new AgentWallet instance.
544
570
  *
@@ -568,6 +594,35 @@ declare class AgentWallet {
568
594
  * Validate the configuration before initialization.
569
595
  */
570
596
  private validateConfig;
597
+ /**
598
+ * Fetch the merchant allowlist from the server.
599
+ * Caches the result for 5 minutes to avoid excessive API calls.
600
+ */
601
+ private fetchAllowlist;
602
+ /**
603
+ * Validate that a target is allowed by the session's allowlist.
604
+ *
605
+ * @param target - URL, domain, or tool name to check
606
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
607
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
608
+ */
609
+ private validateMerchantAllowed;
610
+ /**
611
+ * Check if a target matches any pattern in the allowlist.
612
+ */
613
+ private matchesAllowlist;
614
+ /**
615
+ * Check if a target matches a single pattern.
616
+ */
617
+ private matchPattern;
618
+ /**
619
+ * Match a URL or domain against a pattern.
620
+ */
621
+ private matchUrlPattern;
622
+ /**
623
+ * Match an MCP tool name against a pattern.
624
+ */
625
+ private matchToolPattern;
571
626
  /**
572
627
  * Register a new agent with MixrPay.
573
628
  *
@@ -589,6 +644,29 @@ declare class AgentWallet {
589
644
  * ```
590
645
  */
591
646
  static register(options: AgentRegisterOptions): Promise<AgentRegisterResult>;
647
+ /**
648
+ * Check if the MixrPay server is properly configured for agent registration.
649
+ *
650
+ * Use this to diagnose registration issues before attempting to register.
651
+ *
652
+ * @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
653
+ * @returns Server health status including agent registration availability
654
+ *
655
+ * @example
656
+ * ```typescript
657
+ * const status = await AgentWallet.checkServerHealth();
658
+ * if (!status.agentRegistrationAvailable) {
659
+ * console.error('Agent registration is not available:', status);
660
+ * }
661
+ * ```
662
+ */
663
+ static checkServerHealth(baseUrl?: string): Promise<{
664
+ healthy: boolean;
665
+ database: string;
666
+ agentRegistrationAvailable: boolean;
667
+ privyConfigured: boolean;
668
+ error?: string;
669
+ }>;
592
670
  /**
593
671
  * Get a session key for an already-registered agent.
594
672
  *
@@ -657,6 +735,31 @@ declare class AgentWallet {
657
735
  * ```
658
736
  */
659
737
  static revokeSessionKey(options: AgentRevokeSessionKeyOptions): Promise<boolean>;
738
+ /**
739
+ * Withdraw USDC from agent's MixrPay wallet to their external wallet.
740
+ *
741
+ * SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
742
+ * (the wallet used during `register()`). This prevents prompt injection
743
+ * attacks where a compromised agent might be tricked into withdrawing
744
+ * to an attacker's address.
745
+ *
746
+ * @param options - Withdrawal options
747
+ * @returns Withdrawal result with transaction hash
748
+ * @throws {MixrPayError} If withdrawal fails
749
+ *
750
+ * @example
751
+ * ```typescript
752
+ * const result = await AgentWallet.withdraw({
753
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
754
+ * amountUsd: 50.00,
755
+ * });
756
+ *
757
+ * console.log(`Withdrew $${result.amountUsd}`);
758
+ * console.log(`Transaction: ${result.txHash}`);
759
+ * console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
760
+ * ```
761
+ */
762
+ static withdraw(options: AgentWithdrawOptions): Promise<AgentWithdrawResult>;
660
763
  /**
661
764
  * Make an HTTP request, automatically handling x402 payment if required.
662
765
  *
@@ -1713,6 +1816,34 @@ declare class SessionRevokedError extends MixrPayError {
1713
1816
  readonly reason?: string;
1714
1817
  constructor(sessionId: string, reason?: string);
1715
1818
  }
1819
+ /**
1820
+ * Thrown when a payment is attempted to a merchant/tool not in the session's allowlist.
1821
+ *
1822
+ * This error indicates the session key has a configured allowlist that doesn't
1823
+ * include the target merchant or tool.
1824
+ *
1825
+ * ## Resolution
1826
+ * 1. Update the session's allowlist to include the target
1827
+ * 2. Create a new session with the correct allowlist
1828
+ * 3. Use a session without allowlist restrictions
1829
+ *
1830
+ * @example
1831
+ * ```typescript
1832
+ * catch (error) {
1833
+ * if (error instanceof MerchantNotAllowedError) {
1834
+ * console.log(`Cannot pay "${error.attempted}"`);
1835
+ * console.log(`Allowed patterns: ${error.allowedPatterns.join(', ')}`);
1836
+ * }
1837
+ * }
1838
+ * ```
1839
+ */
1840
+ declare class MerchantNotAllowedError extends MixrPayError {
1841
+ /** The merchant/tool that was attempted */
1842
+ readonly attempted: string;
1843
+ /** The patterns that are allowed by the session */
1844
+ readonly allowedPatterns: string[];
1845
+ constructor(attempted: string, allowedPatterns: string[]);
1846
+ }
1716
1847
  /**
1717
1848
  * Check if an error is a MixrPay SDK error.
1718
1849
  *
@@ -1749,4 +1880,4 @@ declare function isMixrPayError(error: unknown): error is MixrPayError;
1749
1880
  */
1750
1881
  declare function getErrorMessage(error: unknown): string;
1751
1882
 
1752
- export { type AgentMessage, type AgentRunConfig, type AgentRunEvent, type AgentRunOptions, type AgentRunResult, type AgentRunStatusResult, AgentWallet, type AgentWalletConfig, type CallMerchantApiOptions, type ChargeResult, type DiagnosticsResult, InsufficientBalanceError, InvalidSessionKeyError, MixrPayError, type PaymentEvent, PaymentFailedError, SDK_VERSION, type SessionAuthorization, SessionExpiredError, SessionKeyExpiredError, type SessionKeyInfo, SessionLimitExceededError, SessionNotFoundError, SessionRevokedError, type SessionStats, SpendingLimitExceededError, type SpendingStats, X402ProtocolError, getErrorMessage, isMixrPayError };
1883
+ export { type AgentMessage, type AgentRunConfig, type AgentRunEvent, type AgentRunOptions, type AgentRunResult, type AgentRunStatusResult, AgentWallet, type AgentWalletConfig, type CallMerchantApiOptions, type ChargeResult, type DiagnosticsResult, InsufficientBalanceError, InvalidSessionKeyError, MerchantNotAllowedError, MixrPayError, type PaymentEvent, PaymentFailedError, SDK_VERSION, type SessionAuthorization, SessionExpiredError, SessionKeyExpiredError, type SessionKeyInfo, SessionLimitExceededError, SessionNotFoundError, SessionRevokedError, type SessionStats, SpendingLimitExceededError, type SpendingStats, X402ProtocolError, getErrorMessage, isMixrPayError };
package/dist/index.d.ts CHANGED
@@ -158,6 +158,8 @@ interface SessionKeyInfo {
158
158
  createdAt: Date | null;
159
159
  /** Optional name given to this session key */
160
160
  name?: string;
161
+ /** Allowed merchants/tools (empty = allow all) */
162
+ allowedMerchants?: string[];
161
163
  }
162
164
  /**
163
165
  * Spending statistics for the current session.
@@ -378,7 +380,7 @@ interface SessionStats {
378
380
  */
379
381
 
380
382
  /** Current SDK version */
381
- declare const SDK_VERSION = "0.6.0";
383
+ declare const SDK_VERSION = "0.6.1";
382
384
  /** Supported networks */
383
385
  declare const NETWORKS: {
384
386
  readonly BASE_MAINNET: {
@@ -487,6 +489,28 @@ interface AgentRevokeSessionKeyOptions {
487
489
  /** MixrPay API base URL (default: https://www.mixrpay.com) */
488
490
  baseUrl?: string;
489
491
  }
492
+ /**
493
+ * Options for withdrawing funds
494
+ */
495
+ interface AgentWithdrawOptions {
496
+ /** The agent's external wallet private key (used for signing, NOT transmitted) */
497
+ privateKey: `0x${string}`;
498
+ /** Amount to withdraw in USD */
499
+ amountUsd: number;
500
+ /** MixrPay API base URL (default: https://www.mixrpay.com) */
501
+ baseUrl?: string;
502
+ }
503
+ /**
504
+ * Result from withdrawal
505
+ */
506
+ interface AgentWithdrawResult {
507
+ /** On-chain transaction hash */
508
+ txHash: string;
509
+ /** Amount withdrawn in USD */
510
+ amountUsd: number;
511
+ /** Remaining balance after withdrawal */
512
+ remainingBalanceUsd: number;
513
+ }
490
514
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
491
515
  /**
492
516
  * A wallet wrapper for AI agents that handles x402 payments automatically.
@@ -539,6 +563,8 @@ declare class AgentWallet {
539
563
  private totalSpentUsd;
540
564
  private sessionKeyInfo?;
541
565
  private sessionKeyInfoFetchedAt?;
566
+ private allowlist?;
567
+ private allowlistFetchedAt?;
542
568
  /**
543
569
  * Create a new AgentWallet instance.
544
570
  *
@@ -568,6 +594,35 @@ declare class AgentWallet {
568
594
  * Validate the configuration before initialization.
569
595
  */
570
596
  private validateConfig;
597
+ /**
598
+ * Fetch the merchant allowlist from the server.
599
+ * Caches the result for 5 minutes to avoid excessive API calls.
600
+ */
601
+ private fetchAllowlist;
602
+ /**
603
+ * Validate that a target is allowed by the session's allowlist.
604
+ *
605
+ * @param target - URL, domain, or tool name to check
606
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
607
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
608
+ */
609
+ private validateMerchantAllowed;
610
+ /**
611
+ * Check if a target matches any pattern in the allowlist.
612
+ */
613
+ private matchesAllowlist;
614
+ /**
615
+ * Check if a target matches a single pattern.
616
+ */
617
+ private matchPattern;
618
+ /**
619
+ * Match a URL or domain against a pattern.
620
+ */
621
+ private matchUrlPattern;
622
+ /**
623
+ * Match an MCP tool name against a pattern.
624
+ */
625
+ private matchToolPattern;
571
626
  /**
572
627
  * Register a new agent with MixrPay.
573
628
  *
@@ -589,6 +644,29 @@ declare class AgentWallet {
589
644
  * ```
590
645
  */
591
646
  static register(options: AgentRegisterOptions): Promise<AgentRegisterResult>;
647
+ /**
648
+ * Check if the MixrPay server is properly configured for agent registration.
649
+ *
650
+ * Use this to diagnose registration issues before attempting to register.
651
+ *
652
+ * @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
653
+ * @returns Server health status including agent registration availability
654
+ *
655
+ * @example
656
+ * ```typescript
657
+ * const status = await AgentWallet.checkServerHealth();
658
+ * if (!status.agentRegistrationAvailable) {
659
+ * console.error('Agent registration is not available:', status);
660
+ * }
661
+ * ```
662
+ */
663
+ static checkServerHealth(baseUrl?: string): Promise<{
664
+ healthy: boolean;
665
+ database: string;
666
+ agentRegistrationAvailable: boolean;
667
+ privyConfigured: boolean;
668
+ error?: string;
669
+ }>;
592
670
  /**
593
671
  * Get a session key for an already-registered agent.
594
672
  *
@@ -657,6 +735,31 @@ declare class AgentWallet {
657
735
  * ```
658
736
  */
659
737
  static revokeSessionKey(options: AgentRevokeSessionKeyOptions): Promise<boolean>;
738
+ /**
739
+ * Withdraw USDC from agent's MixrPay wallet to their external wallet.
740
+ *
741
+ * SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
742
+ * (the wallet used during `register()`). This prevents prompt injection
743
+ * attacks where a compromised agent might be tricked into withdrawing
744
+ * to an attacker's address.
745
+ *
746
+ * @param options - Withdrawal options
747
+ * @returns Withdrawal result with transaction hash
748
+ * @throws {MixrPayError} If withdrawal fails
749
+ *
750
+ * @example
751
+ * ```typescript
752
+ * const result = await AgentWallet.withdraw({
753
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
754
+ * amountUsd: 50.00,
755
+ * });
756
+ *
757
+ * console.log(`Withdrew $${result.amountUsd}`);
758
+ * console.log(`Transaction: ${result.txHash}`);
759
+ * console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
760
+ * ```
761
+ */
762
+ static withdraw(options: AgentWithdrawOptions): Promise<AgentWithdrawResult>;
660
763
  /**
661
764
  * Make an HTTP request, automatically handling x402 payment if required.
662
765
  *
@@ -1713,6 +1816,34 @@ declare class SessionRevokedError extends MixrPayError {
1713
1816
  readonly reason?: string;
1714
1817
  constructor(sessionId: string, reason?: string);
1715
1818
  }
1819
+ /**
1820
+ * Thrown when a payment is attempted to a merchant/tool not in the session's allowlist.
1821
+ *
1822
+ * This error indicates the session key has a configured allowlist that doesn't
1823
+ * include the target merchant or tool.
1824
+ *
1825
+ * ## Resolution
1826
+ * 1. Update the session's allowlist to include the target
1827
+ * 2. Create a new session with the correct allowlist
1828
+ * 3. Use a session without allowlist restrictions
1829
+ *
1830
+ * @example
1831
+ * ```typescript
1832
+ * catch (error) {
1833
+ * if (error instanceof MerchantNotAllowedError) {
1834
+ * console.log(`Cannot pay "${error.attempted}"`);
1835
+ * console.log(`Allowed patterns: ${error.allowedPatterns.join(', ')}`);
1836
+ * }
1837
+ * }
1838
+ * ```
1839
+ */
1840
+ declare class MerchantNotAllowedError extends MixrPayError {
1841
+ /** The merchant/tool that was attempted */
1842
+ readonly attempted: string;
1843
+ /** The patterns that are allowed by the session */
1844
+ readonly allowedPatterns: string[];
1845
+ constructor(attempted: string, allowedPatterns: string[]);
1846
+ }
1716
1847
  /**
1717
1848
  * Check if an error is a MixrPay SDK error.
1718
1849
  *
@@ -1749,4 +1880,4 @@ declare function isMixrPayError(error: unknown): error is MixrPayError;
1749
1880
  */
1750
1881
  declare function getErrorMessage(error: unknown): string;
1751
1882
 
1752
- export { type AgentMessage, type AgentRunConfig, type AgentRunEvent, type AgentRunOptions, type AgentRunResult, type AgentRunStatusResult, AgentWallet, type AgentWalletConfig, type CallMerchantApiOptions, type ChargeResult, type DiagnosticsResult, InsufficientBalanceError, InvalidSessionKeyError, MixrPayError, type PaymentEvent, PaymentFailedError, SDK_VERSION, type SessionAuthorization, SessionExpiredError, SessionKeyExpiredError, type SessionKeyInfo, SessionLimitExceededError, SessionNotFoundError, SessionRevokedError, type SessionStats, SpendingLimitExceededError, type SpendingStats, X402ProtocolError, getErrorMessage, isMixrPayError };
1883
+ export { type AgentMessage, type AgentRunConfig, type AgentRunEvent, type AgentRunOptions, type AgentRunResult, type AgentRunStatusResult, AgentWallet, type AgentWalletConfig, type CallMerchantApiOptions, type ChargeResult, type DiagnosticsResult, InsufficientBalanceError, InvalidSessionKeyError, MerchantNotAllowedError, MixrPayError, type PaymentEvent, PaymentFailedError, SDK_VERSION, type SessionAuthorization, SessionExpiredError, SessionKeyExpiredError, type SessionKeyInfo, SessionLimitExceededError, SessionNotFoundError, SessionRevokedError, type SessionStats, SpendingLimitExceededError, type SpendingStats, X402ProtocolError, getErrorMessage, isMixrPayError };
package/dist/index.js CHANGED
@@ -229,6 +229,23 @@ var SessionRevokedError = class extends MixrPayError {
229
229
  this.reason = reason;
230
230
  }
231
231
  };
232
+ var MerchantNotAllowedError = class extends MixrPayError {
233
+ /** The merchant/tool that was attempted */
234
+ attempted;
235
+ /** The patterns that are allowed by the session */
236
+ allowedPatterns;
237
+ constructor(attempted, allowedPatterns) {
238
+ const patternsPreview = allowedPatterns.slice(0, 3).join(", ");
239
+ const suffix = allowedPatterns.length > 3 ? "..." : "";
240
+ super(
241
+ `Payment to "${attempted}" not allowed. Session allowlist: ${patternsPreview}${suffix}. Update the session allowlist or create a new session.`,
242
+ "MERCHANT_NOT_ALLOWED"
243
+ );
244
+ this.name = "MerchantNotAllowedError";
245
+ this.attempted = attempted;
246
+ this.allowedPatterns = allowedPatterns;
247
+ }
248
+ };
232
249
  function isMixrPayError(error) {
233
250
  return error instanceof MixrPayError;
234
251
  }
@@ -471,7 +488,7 @@ function getAmountUsd(requirements) {
471
488
  }
472
489
 
473
490
  // src/agent-wallet.ts
474
- var SDK_VERSION = "0.6.0";
491
+ var SDK_VERSION = "0.6.1";
475
492
  var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
476
493
  var DEFAULT_TIMEOUT = 3e4;
477
494
  var NETWORKS = {
@@ -539,6 +556,9 @@ var AgentWallet = class {
539
556
  // Session key info cache
540
557
  sessionKeyInfo;
541
558
  sessionKeyInfoFetchedAt;
559
+ // Merchant allowlist (fetched from server)
560
+ allowlist;
561
+ allowlistFetchedAt;
542
562
  /**
543
563
  * Create a new AgentWallet instance.
544
564
  *
@@ -615,6 +635,126 @@ var AgentWallet = class {
615
635
  }
616
636
  }
617
637
  // ===========================================================================
638
+ // Merchant Allowlist Validation
639
+ // ===========================================================================
640
+ /**
641
+ * Fetch the merchant allowlist from the server.
642
+ * Caches the result for 5 minutes to avoid excessive API calls.
643
+ */
644
+ async fetchAllowlist() {
645
+ const CACHE_TTL_MS = 5 * 60 * 1e3;
646
+ if (this.allowlist !== void 0 && this.allowlistFetchedAt) {
647
+ const age = Date.now() - this.allowlistFetchedAt;
648
+ if (age < CACHE_TTL_MS) {
649
+ return this.allowlist;
650
+ }
651
+ }
652
+ try {
653
+ const info = await this.getSessionKeyInfo();
654
+ this.allowlist = info.allowedMerchants || [];
655
+ this.allowlistFetchedAt = Date.now();
656
+ this.logger.debug("Fetched allowlist", {
657
+ patterns: this.allowlist.length,
658
+ allowAll: this.allowlist.length === 0
659
+ });
660
+ return this.allowlist;
661
+ } catch (error) {
662
+ this.logger.warn("Failed to fetch allowlist, allowing all merchants", { error });
663
+ this.allowlist = [];
664
+ this.allowlistFetchedAt = Date.now();
665
+ return this.allowlist;
666
+ }
667
+ }
668
+ /**
669
+ * Validate that a target is allowed by the session's allowlist.
670
+ *
671
+ * @param target - URL, domain, or tool name to check
672
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
673
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
674
+ */
675
+ async validateMerchantAllowed(target, type) {
676
+ const allowlist = await this.fetchAllowlist();
677
+ if (allowlist.length === 0) {
678
+ return;
679
+ }
680
+ const isAllowed = this.matchesAllowlist(target, allowlist, type);
681
+ if (!isAllowed) {
682
+ throw new MerchantNotAllowedError(target, allowlist);
683
+ }
684
+ }
685
+ /**
686
+ * Check if a target matches any pattern in the allowlist.
687
+ */
688
+ matchesAllowlist(target, allowlist, type) {
689
+ for (const pattern of allowlist) {
690
+ if (this.matchPattern(target, pattern, type)) {
691
+ return true;
692
+ }
693
+ }
694
+ return false;
695
+ }
696
+ /**
697
+ * Check if a target matches a single pattern.
698
+ */
699
+ matchPattern(target, pattern, type) {
700
+ if (type === "url") {
701
+ return this.matchUrlPattern(target, pattern);
702
+ } else {
703
+ return this.matchToolPattern(target, pattern);
704
+ }
705
+ }
706
+ /**
707
+ * Match a URL or domain against a pattern.
708
+ */
709
+ matchUrlPattern(target, pattern) {
710
+ let hostname;
711
+ try {
712
+ if (target.includes("://")) {
713
+ const url = new URL(target);
714
+ hostname = url.hostname.toLowerCase();
715
+ } else {
716
+ hostname = target.toLowerCase();
717
+ }
718
+ } catch {
719
+ return false;
720
+ }
721
+ const normalizedPattern = pattern.toLowerCase().trim();
722
+ let patternDomain = normalizedPattern;
723
+ if (patternDomain.includes("://")) {
724
+ try {
725
+ patternDomain = new URL(patternDomain).hostname;
726
+ } catch {
727
+ }
728
+ }
729
+ if (hostname === patternDomain) {
730
+ return true;
731
+ }
732
+ if (patternDomain.startsWith("*.")) {
733
+ const baseDomain = patternDomain.substring(2);
734
+ if (hostname.endsWith(`.${baseDomain}`) && hostname !== baseDomain) {
735
+ return true;
736
+ }
737
+ }
738
+ return false;
739
+ }
740
+ /**
741
+ * Match an MCP tool name against a pattern.
742
+ */
743
+ matchToolPattern(toolName, pattern) {
744
+ const normalizedTool = toolName.toLowerCase().trim();
745
+ const normalizedPattern = pattern.toLowerCase().trim();
746
+ if (normalizedTool === normalizedPattern) {
747
+ return true;
748
+ }
749
+ if (normalizedPattern.endsWith("/*")) {
750
+ const baseProvider = normalizedPattern.substring(0, normalizedPattern.length - 2);
751
+ if (normalizedTool.startsWith(`${baseProvider}/`)) {
752
+ return true;
753
+ }
754
+ }
755
+ return false;
756
+ }
757
+ // ===========================================================================
618
758
  // Static Agent Registration Methods
619
759
  // ===========================================================================
620
760
  /**
@@ -663,7 +803,19 @@ var AgentWallet = class {
663
803
  });
664
804
  if (!registerResponse.ok) {
665
805
  const error = await registerResponse.json().catch(() => ({}));
666
- throw new MixrPayError(error.error || `Registration failed: ${registerResponse.status}`);
806
+ const errorMessage = error.error || `Registration failed with status ${registerResponse.status}`;
807
+ const requestId = error.request_id;
808
+ const errorCode = error.code;
809
+ let helpText = "";
810
+ if (registerResponse.status === 503) {
811
+ helpText = " The service may be temporarily unavailable. Please try again later.";
812
+ } else if (registerResponse.status === 500) {
813
+ helpText = " This is a server error. Please contact support with the request ID.";
814
+ } else if (errorCode === "MISSING_CHALLENGE" || errorCode === "MISSING_SIGNATURE") {
815
+ helpText = " This may indicate an SDK bug. Please update to the latest version.";
816
+ }
817
+ const fullMessage = requestId ? `${errorMessage} (request_id: ${requestId})${helpText}` : `${errorMessage}${helpText}`;
818
+ throw new MixrPayError(fullMessage);
667
819
  }
668
820
  const data = await registerResponse.json();
669
821
  return {
@@ -671,6 +823,52 @@ var AgentWallet = class {
671
823
  depositAddress: data.deposit_address
672
824
  };
673
825
  }
826
+ /**
827
+ * Check if the MixrPay server is properly configured for agent registration.
828
+ *
829
+ * Use this to diagnose registration issues before attempting to register.
830
+ *
831
+ * @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
832
+ * @returns Server health status including agent registration availability
833
+ *
834
+ * @example
835
+ * ```typescript
836
+ * const status = await AgentWallet.checkServerHealth();
837
+ * if (!status.agentRegistrationAvailable) {
838
+ * console.error('Agent registration is not available:', status);
839
+ * }
840
+ * ```
841
+ */
842
+ static async checkServerHealth(baseUrl) {
843
+ const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
844
+ try {
845
+ const response = await fetch(`${url}/api/health/ready?details=true`);
846
+ if (!response.ok) {
847
+ return {
848
+ healthy: false,
849
+ database: "unknown",
850
+ agentRegistrationAvailable: false,
851
+ privyConfigured: false,
852
+ error: `Health check failed with status ${response.status}`
853
+ };
854
+ }
855
+ const data = await response.json();
856
+ return {
857
+ healthy: data.status === "ready",
858
+ database: data.database || "unknown",
859
+ agentRegistrationAvailable: data.services?.agentRegistration?.available ?? false,
860
+ privyConfigured: data.services?.privy?.configured ?? false
861
+ };
862
+ } catch (error) {
863
+ return {
864
+ healthy: false,
865
+ database: "unreachable",
866
+ agentRegistrationAvailable: false,
867
+ privyConfigured: false,
868
+ error: error instanceof Error ? error.message : "Failed to reach server"
869
+ };
870
+ }
871
+ }
674
872
  /**
675
873
  * Get a session key for an already-registered agent.
676
874
  *
@@ -845,6 +1043,67 @@ var AgentWallet = class {
845
1043
  }
846
1044
  return true;
847
1045
  }
1046
+ /**
1047
+ * Withdraw USDC from agent's MixrPay wallet to their external wallet.
1048
+ *
1049
+ * SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
1050
+ * (the wallet used during `register()`). This prevents prompt injection
1051
+ * attacks where a compromised agent might be tricked into withdrawing
1052
+ * to an attacker's address.
1053
+ *
1054
+ * @param options - Withdrawal options
1055
+ * @returns Withdrawal result with transaction hash
1056
+ * @throws {MixrPayError} If withdrawal fails
1057
+ *
1058
+ * @example
1059
+ * ```typescript
1060
+ * const result = await AgentWallet.withdraw({
1061
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
1062
+ * amountUsd: 50.00,
1063
+ * });
1064
+ *
1065
+ * console.log(`Withdrew $${result.amountUsd}`);
1066
+ * console.log(`Transaction: ${result.txHash}`);
1067
+ * console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
1068
+ * ```
1069
+ */
1070
+ static async withdraw(options) {
1071
+ const { privateKey, amountUsd } = options;
1072
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1073
+ const account = privateKeyToAccount2(privateKey);
1074
+ const walletAddress = account.address;
1075
+ const challengeResponse = await fetch(
1076
+ `${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=withdraw`
1077
+ );
1078
+ if (!challengeResponse.ok) {
1079
+ const error = await challengeResponse.json().catch(() => ({}));
1080
+ throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
1081
+ }
1082
+ const { challenge, message } = await challengeResponse.json();
1083
+ const signature = await signMessage({ message, privateKey });
1084
+ const withdrawResponse = await fetch(`${baseUrl}/api/v1/agent/withdraw`, {
1085
+ method: "POST",
1086
+ headers: { "Content-Type": "application/json" },
1087
+ body: JSON.stringify({
1088
+ challenge,
1089
+ external_wallet: walletAddress,
1090
+ signature,
1091
+ to_address: walletAddress,
1092
+ // Always withdraw to self
1093
+ amount_usd: amountUsd
1094
+ })
1095
+ });
1096
+ if (!withdrawResponse.ok) {
1097
+ const error = await withdrawResponse.json().catch(() => ({}));
1098
+ throw new MixrPayError(error.error || `Withdrawal failed: ${withdrawResponse.status}`);
1099
+ }
1100
+ const data = await withdrawResponse.json();
1101
+ return {
1102
+ txHash: data.tx_hash,
1103
+ amountUsd: data.amount_usd,
1104
+ remainingBalanceUsd: data.remaining_balance_usd
1105
+ };
1106
+ }
848
1107
  // ===========================================================================
849
1108
  // Core Methods
850
1109
  // ===========================================================================
@@ -883,6 +1142,7 @@ var AgentWallet = class {
883
1142
  */
884
1143
  async fetch(url, init) {
885
1144
  this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
1145
+ await this.validateMerchantAllowed(url, "url");
886
1146
  const requestId = crypto.randomUUID();
887
1147
  const correlationId = this.extractCorrelationId(init?.headers);
888
1148
  const controller = new AbortController();
@@ -1156,7 +1416,8 @@ var AgentWallet = class {
1156
1416
  },
1157
1417
  expiresAt: data.expires_at ? new Date(data.expires_at) : null,
1158
1418
  createdAt: data.created_at ? new Date(data.created_at) : null,
1159
- name: data.name
1419
+ name: data.name,
1420
+ allowedMerchants: data.allowed_merchants ?? data.allowedMerchants ?? []
1160
1421
  };
1161
1422
  this.sessionKeyInfoFetchedAt = Date.now();
1162
1423
  return this.sessionKeyInfo;
@@ -1722,6 +1983,7 @@ var AgentWallet = class {
1722
1983
  feature
1723
1984
  } = options;
1724
1985
  this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
1986
+ await this.validateMerchantAllowed(url, "url");
1725
1987
  if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1726
1988
  throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1727
1989
  }
@@ -1929,6 +2191,7 @@ Timestamp: ${timestamp}`;
1929
2191
  */
1930
2192
  async callMCPTool(toolName, args = {}) {
1931
2193
  this.logger.debug("callMCPTool", { toolName, args });
2194
+ await this.validateMerchantAllowed(toolName, "tool");
1932
2195
  const authHeaders = await this.getMCPAuthHeaders();
1933
2196
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
1934
2197
  method: "POST",
@@ -2318,6 +2581,7 @@ Timestamp: ${timestamp}`;
2318
2581
  */
2319
2582
  async callMCPToolWithSession(sessionId, toolName, args = {}) {
2320
2583
  this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
2584
+ await this.validateMerchantAllowed(toolName, "tool");
2321
2585
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2322
2586
  method: "POST",
2323
2587
  headers: {
@@ -2367,6 +2631,7 @@ export {
2367
2631
  AgentWallet,
2368
2632
  InsufficientBalanceError,
2369
2633
  InvalidSessionKeyError,
2634
+ MerchantNotAllowedError,
2370
2635
  MixrPayError,
2371
2636
  PaymentFailedError,
2372
2637
  SDK_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mixrpay/agent-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",