@mixrpay/agent-sdk 0.6.1 → 0.8.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/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.1";
528
+ var SDK_VERSION = "0.8.0";
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
  /**
@@ -1000,6 +1141,91 @@ var AgentWallet = class {
1000
1141
  remainingBalanceUsd: data.remaining_balance_usd
1001
1142
  };
1002
1143
  }
1144
+ /**
1145
+ * Claim an agent invite code to receive a session key for the inviter's wallet.
1146
+ *
1147
+ * This allows an agent to get a pre-configured session key from a human wallet owner
1148
+ * without needing to register their own wallet or fund it. The human sets the budget
1149
+ * limits and merchant whitelist when creating the invite.
1150
+ *
1151
+ * @param options - Claim invite options including the invite code and agent's private key
1152
+ * @returns Claim result with the new session key
1153
+ * @throws {MixrPayError} If claiming fails (e.g., invalid code, already claimed, expired)
1154
+ *
1155
+ * @example
1156
+ * ```typescript
1157
+ * // Human creates invite at their dashboard, shares code "mixr-abc123"
1158
+ *
1159
+ * const result = await AgentWallet.claimInvite({
1160
+ * inviteCode: 'mixr-abc123',
1161
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
1162
+ * });
1163
+ *
1164
+ * console.log(`Got session key: ${result.sessionKey}`);
1165
+ * console.log(`Budget: $${result.limits.budgetUsd}/${result.limits.budgetPeriod}`);
1166
+ * console.log(`Invited by: ${result.inviterName}`);
1167
+ *
1168
+ * // Use the session key to make payments
1169
+ * const wallet = new AgentWallet({ sessionKey: result.sessionKey });
1170
+ * const response = await wallet.fetch('https://api.example.com/endpoint');
1171
+ * ```
1172
+ */
1173
+ static async claimInvite(options) {
1174
+ const { inviteCode, privateKey } = options;
1175
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1176
+ const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
1177
+ const walletAddress = account.address;
1178
+ const challengeResponse = await fetch(
1179
+ `${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=claim-invite`
1180
+ );
1181
+ if (!challengeResponse.ok) {
1182
+ const error = await challengeResponse.json().catch(() => ({}));
1183
+ throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
1184
+ }
1185
+ const { challenge, message } = await challengeResponse.json();
1186
+ const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
1187
+ const claimResponse = await fetch(`${baseUrl}/api/v1/agent/claim-invite`, {
1188
+ method: "POST",
1189
+ headers: { "Content-Type": "application/json" },
1190
+ body: JSON.stringify({
1191
+ invite_code: inviteCode,
1192
+ challenge,
1193
+ agent_wallet_address: walletAddress,
1194
+ signature
1195
+ })
1196
+ });
1197
+ if (!claimResponse.ok) {
1198
+ const error = await claimResponse.json().catch(() => ({}));
1199
+ const errorMessage = error.error || `Failed to claim invite: ${claimResponse.status}`;
1200
+ let helpText = "";
1201
+ if (claimResponse.status === 404) {
1202
+ helpText = " The invite code may be invalid or misspelled.";
1203
+ } else if (claimResponse.status === 400) {
1204
+ if (errorMessage.includes("already claimed")) {
1205
+ helpText = " This invite has already been used by another agent.";
1206
+ } else if (errorMessage.includes("expired")) {
1207
+ helpText = " Ask the wallet owner to create a new invite.";
1208
+ } else if (errorMessage.includes("revoked")) {
1209
+ helpText = " The wallet owner has revoked this invite.";
1210
+ }
1211
+ }
1212
+ throw new MixrPayError(`${errorMessage}${helpText}`);
1213
+ }
1214
+ const data = await claimResponse.json();
1215
+ return {
1216
+ sessionKey: data.session_key,
1217
+ address: data.address,
1218
+ sessionKeyId: data.session_key_id,
1219
+ expiresAt: new Date(data.expires_at),
1220
+ limits: {
1221
+ budgetUsd: data.limits.budget_usd,
1222
+ budgetPeriod: data.limits.budget_period,
1223
+ maxPerTxUsd: data.limits.max_per_tx_usd
1224
+ },
1225
+ allowedMerchants: data.allowed_merchants || [],
1226
+ inviterName: data.inviter_name || "Anonymous"
1227
+ };
1228
+ }
1003
1229
  // ===========================================================================
1004
1230
  // Core Methods
1005
1231
  // ===========================================================================
@@ -1038,6 +1264,7 @@ var AgentWallet = class {
1038
1264
  */
1039
1265
  async fetch(url, init) {
1040
1266
  this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
1267
+ await this.validateMerchantAllowed(url, "url");
1041
1268
  const requestId = crypto.randomUUID();
1042
1269
  const correlationId = this.extractCorrelationId(init?.headers);
1043
1270
  const controller = new AbortController();
@@ -1311,7 +1538,8 @@ var AgentWallet = class {
1311
1538
  },
1312
1539
  expiresAt: data.expires_at ? new Date(data.expires_at) : null,
1313
1540
  createdAt: data.created_at ? new Date(data.created_at) : null,
1314
- name: data.name
1541
+ name: data.name,
1542
+ allowedMerchants: data.allowed_merchants ?? data.allowedMerchants ?? []
1315
1543
  };
1316
1544
  this.sessionKeyInfoFetchedAt = Date.now();
1317
1545
  return this.sessionKeyInfo;
@@ -1877,6 +2105,7 @@ var AgentWallet = class {
1877
2105
  feature
1878
2106
  } = options;
1879
2107
  this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
2108
+ await this.validateMerchantAllowed(url, "url");
1880
2109
  if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1881
2110
  throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1882
2111
  }
@@ -2084,6 +2313,7 @@ Timestamp: ${timestamp}`;
2084
2313
  */
2085
2314
  async callMCPTool(toolName, args = {}) {
2086
2315
  this.logger.debug("callMCPTool", { toolName, args });
2316
+ await this.validateMerchantAllowed(toolName, "tool");
2087
2317
  const authHeaders = await this.getMCPAuthHeaders();
2088
2318
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2089
2319
  method: "POST",
@@ -2473,6 +2703,7 @@ Timestamp: ${timestamp}`;
2473
2703
  */
2474
2704
  async callMCPToolWithSession(sessionId, toolName, args = {}) {
2475
2705
  this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
2706
+ await this.validateMerchantAllowed(toolName, "tool");
2476
2707
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2477
2708
  method: "POST",
2478
2709
  headers: {
@@ -2523,6 +2754,7 @@ Timestamp: ${timestamp}`;
2523
2754
  AgentWallet,
2524
2755
  InsufficientBalanceError,
2525
2756
  InvalidSessionKeyError,
2757
+ MerchantNotAllowedError,
2526
2758
  MixrPayError,
2527
2759
  PaymentFailedError,
2528
2760
  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.1";
383
+ declare const SDK_VERSION = "0.8.0";
382
384
  /** Supported networks */
383
385
  declare const NETWORKS: {
384
386
  readonly BASE_MAINNET: {
@@ -509,6 +511,43 @@ interface AgentWithdrawResult {
509
511
  /** Remaining balance after withdrawal */
510
512
  remainingBalanceUsd: number;
511
513
  }
514
+ /**
515
+ * Options for claiming an agent invite
516
+ */
517
+ interface AgentClaimInviteOptions {
518
+ /** The invite code provided by the human wallet owner (e.g., "mixr-abc123") */
519
+ inviteCode: string;
520
+ /** The agent's external wallet private key (used for signing, NOT transmitted) */
521
+ privateKey: `0x${string}`;
522
+ /** MixrPay API base URL (default: https://www.mixrpay.com) */
523
+ baseUrl?: string;
524
+ }
525
+ /**
526
+ * Result from claiming an agent invite
527
+ */
528
+ interface AgentClaimInviteResult {
529
+ /** The session key in sk_live_{hex} format - STORE SECURELY! */
530
+ sessionKey: string;
531
+ /** The derived public address of the session key */
532
+ address: string;
533
+ /** Session key database ID */
534
+ sessionKeyId: string;
535
+ /** When the session key expires */
536
+ expiresAt: Date;
537
+ /** Budget limits set by the inviter */
538
+ limits: {
539
+ /** Total budget in USD */
540
+ budgetUsd: number;
541
+ /** Budget period: 'daily', 'monthly', or 'total' */
542
+ budgetPeriod: string;
543
+ /** Maximum per transaction in USD (null = no limit) */
544
+ maxPerTxUsd: number | null;
545
+ };
546
+ /** Whitelisted merchants/tools (empty = allow all) */
547
+ allowedMerchants: string[];
548
+ /** Name of the person who created the invite */
549
+ inviterName: string;
550
+ }
512
551
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
513
552
  /**
514
553
  * A wallet wrapper for AI agents that handles x402 payments automatically.
@@ -561,6 +600,8 @@ declare class AgentWallet {
561
600
  private totalSpentUsd;
562
601
  private sessionKeyInfo?;
563
602
  private sessionKeyInfoFetchedAt?;
603
+ private allowlist?;
604
+ private allowlistFetchedAt?;
564
605
  /**
565
606
  * Create a new AgentWallet instance.
566
607
  *
@@ -590,6 +631,35 @@ declare class AgentWallet {
590
631
  * Validate the configuration before initialization.
591
632
  */
592
633
  private validateConfig;
634
+ /**
635
+ * Fetch the merchant allowlist from the server.
636
+ * Caches the result for 5 minutes to avoid excessive API calls.
637
+ */
638
+ private fetchAllowlist;
639
+ /**
640
+ * Validate that a target is allowed by the session's allowlist.
641
+ *
642
+ * @param target - URL, domain, or tool name to check
643
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
644
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
645
+ */
646
+ private validateMerchantAllowed;
647
+ /**
648
+ * Check if a target matches any pattern in the allowlist.
649
+ */
650
+ private matchesAllowlist;
651
+ /**
652
+ * Check if a target matches a single pattern.
653
+ */
654
+ private matchPattern;
655
+ /**
656
+ * Match a URL or domain against a pattern.
657
+ */
658
+ private matchUrlPattern;
659
+ /**
660
+ * Match an MCP tool name against a pattern.
661
+ */
662
+ private matchToolPattern;
593
663
  /**
594
664
  * Register a new agent with MixrPay.
595
665
  *
@@ -727,6 +797,36 @@ declare class AgentWallet {
727
797
  * ```
728
798
  */
729
799
  static withdraw(options: AgentWithdrawOptions): Promise<AgentWithdrawResult>;
800
+ /**
801
+ * Claim an agent invite code to receive a session key for the inviter's wallet.
802
+ *
803
+ * This allows an agent to get a pre-configured session key from a human wallet owner
804
+ * without needing to register their own wallet or fund it. The human sets the budget
805
+ * limits and merchant whitelist when creating the invite.
806
+ *
807
+ * @param options - Claim invite options including the invite code and agent's private key
808
+ * @returns Claim result with the new session key
809
+ * @throws {MixrPayError} If claiming fails (e.g., invalid code, already claimed, expired)
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * // Human creates invite at their dashboard, shares code "mixr-abc123"
814
+ *
815
+ * const result = await AgentWallet.claimInvite({
816
+ * inviteCode: 'mixr-abc123',
817
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
818
+ * });
819
+ *
820
+ * console.log(`Got session key: ${result.sessionKey}`);
821
+ * console.log(`Budget: $${result.limits.budgetUsd}/${result.limits.budgetPeriod}`);
822
+ * console.log(`Invited by: ${result.inviterName}`);
823
+ *
824
+ * // Use the session key to make payments
825
+ * const wallet = new AgentWallet({ sessionKey: result.sessionKey });
826
+ * const response = await wallet.fetch('https://api.example.com/endpoint');
827
+ * ```
828
+ */
829
+ static claimInvite(options: AgentClaimInviteOptions): Promise<AgentClaimInviteResult>;
730
830
  /**
731
831
  * Make an HTTP request, automatically handling x402 payment if required.
732
832
  *
@@ -1783,6 +1883,34 @@ declare class SessionRevokedError extends MixrPayError {
1783
1883
  readonly reason?: string;
1784
1884
  constructor(sessionId: string, reason?: string);
1785
1885
  }
1886
+ /**
1887
+ * Thrown when a payment is attempted to a merchant/tool not in the session's allowlist.
1888
+ *
1889
+ * This error indicates the session key has a configured allowlist that doesn't
1890
+ * include the target merchant or tool.
1891
+ *
1892
+ * ## Resolution
1893
+ * 1. Update the session's allowlist to include the target
1894
+ * 2. Create a new session with the correct allowlist
1895
+ * 3. Use a session without allowlist restrictions
1896
+ *
1897
+ * @example
1898
+ * ```typescript
1899
+ * catch (error) {
1900
+ * if (error instanceof MerchantNotAllowedError) {
1901
+ * console.log(`Cannot pay "${error.attempted}"`);
1902
+ * console.log(`Allowed patterns: ${error.allowedPatterns.join(', ')}`);
1903
+ * }
1904
+ * }
1905
+ * ```
1906
+ */
1907
+ declare class MerchantNotAllowedError extends MixrPayError {
1908
+ /** The merchant/tool that was attempted */
1909
+ readonly attempted: string;
1910
+ /** The patterns that are allowed by the session */
1911
+ readonly allowedPatterns: string[];
1912
+ constructor(attempted: string, allowedPatterns: string[]);
1913
+ }
1786
1914
  /**
1787
1915
  * Check if an error is a MixrPay SDK error.
1788
1916
  *
@@ -1819,4 +1947,4 @@ declare function isMixrPayError(error: unknown): error is MixrPayError;
1819
1947
  */
1820
1948
  declare function getErrorMessage(error: unknown): string;
1821
1949
 
1822
- 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 };
1950
+ 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.1";
383
+ declare const SDK_VERSION = "0.8.0";
382
384
  /** Supported networks */
383
385
  declare const NETWORKS: {
384
386
  readonly BASE_MAINNET: {
@@ -509,6 +511,43 @@ interface AgentWithdrawResult {
509
511
  /** Remaining balance after withdrawal */
510
512
  remainingBalanceUsd: number;
511
513
  }
514
+ /**
515
+ * Options for claiming an agent invite
516
+ */
517
+ interface AgentClaimInviteOptions {
518
+ /** The invite code provided by the human wallet owner (e.g., "mixr-abc123") */
519
+ inviteCode: string;
520
+ /** The agent's external wallet private key (used for signing, NOT transmitted) */
521
+ privateKey: `0x${string}`;
522
+ /** MixrPay API base URL (default: https://www.mixrpay.com) */
523
+ baseUrl?: string;
524
+ }
525
+ /**
526
+ * Result from claiming an agent invite
527
+ */
528
+ interface AgentClaimInviteResult {
529
+ /** The session key in sk_live_{hex} format - STORE SECURELY! */
530
+ sessionKey: string;
531
+ /** The derived public address of the session key */
532
+ address: string;
533
+ /** Session key database ID */
534
+ sessionKeyId: string;
535
+ /** When the session key expires */
536
+ expiresAt: Date;
537
+ /** Budget limits set by the inviter */
538
+ limits: {
539
+ /** Total budget in USD */
540
+ budgetUsd: number;
541
+ /** Budget period: 'daily', 'monthly', or 'total' */
542
+ budgetPeriod: string;
543
+ /** Maximum per transaction in USD (null = no limit) */
544
+ maxPerTxUsd: number | null;
545
+ };
546
+ /** Whitelisted merchants/tools (empty = allow all) */
547
+ allowedMerchants: string[];
548
+ /** Name of the person who created the invite */
549
+ inviterName: string;
550
+ }
512
551
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
513
552
  /**
514
553
  * A wallet wrapper for AI agents that handles x402 payments automatically.
@@ -561,6 +600,8 @@ declare class AgentWallet {
561
600
  private totalSpentUsd;
562
601
  private sessionKeyInfo?;
563
602
  private sessionKeyInfoFetchedAt?;
603
+ private allowlist?;
604
+ private allowlistFetchedAt?;
564
605
  /**
565
606
  * Create a new AgentWallet instance.
566
607
  *
@@ -590,6 +631,35 @@ declare class AgentWallet {
590
631
  * Validate the configuration before initialization.
591
632
  */
592
633
  private validateConfig;
634
+ /**
635
+ * Fetch the merchant allowlist from the server.
636
+ * Caches the result for 5 minutes to avoid excessive API calls.
637
+ */
638
+ private fetchAllowlist;
639
+ /**
640
+ * Validate that a target is allowed by the session's allowlist.
641
+ *
642
+ * @param target - URL, domain, or tool name to check
643
+ * @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
644
+ * @throws {MerchantNotAllowedError} If target is not in allowlist
645
+ */
646
+ private validateMerchantAllowed;
647
+ /**
648
+ * Check if a target matches any pattern in the allowlist.
649
+ */
650
+ private matchesAllowlist;
651
+ /**
652
+ * Check if a target matches a single pattern.
653
+ */
654
+ private matchPattern;
655
+ /**
656
+ * Match a URL or domain against a pattern.
657
+ */
658
+ private matchUrlPattern;
659
+ /**
660
+ * Match an MCP tool name against a pattern.
661
+ */
662
+ private matchToolPattern;
593
663
  /**
594
664
  * Register a new agent with MixrPay.
595
665
  *
@@ -727,6 +797,36 @@ declare class AgentWallet {
727
797
  * ```
728
798
  */
729
799
  static withdraw(options: AgentWithdrawOptions): Promise<AgentWithdrawResult>;
800
+ /**
801
+ * Claim an agent invite code to receive a session key for the inviter's wallet.
802
+ *
803
+ * This allows an agent to get a pre-configured session key from a human wallet owner
804
+ * without needing to register their own wallet or fund it. The human sets the budget
805
+ * limits and merchant whitelist when creating the invite.
806
+ *
807
+ * @param options - Claim invite options including the invite code and agent's private key
808
+ * @returns Claim result with the new session key
809
+ * @throws {MixrPayError} If claiming fails (e.g., invalid code, already claimed, expired)
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * // Human creates invite at their dashboard, shares code "mixr-abc123"
814
+ *
815
+ * const result = await AgentWallet.claimInvite({
816
+ * inviteCode: 'mixr-abc123',
817
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
818
+ * });
819
+ *
820
+ * console.log(`Got session key: ${result.sessionKey}`);
821
+ * console.log(`Budget: $${result.limits.budgetUsd}/${result.limits.budgetPeriod}`);
822
+ * console.log(`Invited by: ${result.inviterName}`);
823
+ *
824
+ * // Use the session key to make payments
825
+ * const wallet = new AgentWallet({ sessionKey: result.sessionKey });
826
+ * const response = await wallet.fetch('https://api.example.com/endpoint');
827
+ * ```
828
+ */
829
+ static claimInvite(options: AgentClaimInviteOptions): Promise<AgentClaimInviteResult>;
730
830
  /**
731
831
  * Make an HTTP request, automatically handling x402 payment if required.
732
832
  *
@@ -1783,6 +1883,34 @@ declare class SessionRevokedError extends MixrPayError {
1783
1883
  readonly reason?: string;
1784
1884
  constructor(sessionId: string, reason?: string);
1785
1885
  }
1886
+ /**
1887
+ * Thrown when a payment is attempted to a merchant/tool not in the session's allowlist.
1888
+ *
1889
+ * This error indicates the session key has a configured allowlist that doesn't
1890
+ * include the target merchant or tool.
1891
+ *
1892
+ * ## Resolution
1893
+ * 1. Update the session's allowlist to include the target
1894
+ * 2. Create a new session with the correct allowlist
1895
+ * 3. Use a session without allowlist restrictions
1896
+ *
1897
+ * @example
1898
+ * ```typescript
1899
+ * catch (error) {
1900
+ * if (error instanceof MerchantNotAllowedError) {
1901
+ * console.log(`Cannot pay "${error.attempted}"`);
1902
+ * console.log(`Allowed patterns: ${error.allowedPatterns.join(', ')}`);
1903
+ * }
1904
+ * }
1905
+ * ```
1906
+ */
1907
+ declare class MerchantNotAllowedError extends MixrPayError {
1908
+ /** The merchant/tool that was attempted */
1909
+ readonly attempted: string;
1910
+ /** The patterns that are allowed by the session */
1911
+ readonly allowedPatterns: string[];
1912
+ constructor(attempted: string, allowedPatterns: string[]);
1913
+ }
1786
1914
  /**
1787
1915
  * Check if an error is a MixrPay SDK error.
1788
1916
  *
@@ -1819,4 +1947,4 @@ declare function isMixrPayError(error: unknown): error is MixrPayError;
1819
1947
  */
1820
1948
  declare function getErrorMessage(error: unknown): string;
1821
1949
 
1822
- 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 };
1950
+ 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.1";
491
+ var SDK_VERSION = "0.8.0";
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
  /**
@@ -964,6 +1104,91 @@ var AgentWallet = class {
964
1104
  remainingBalanceUsd: data.remaining_balance_usd
965
1105
  };
966
1106
  }
1107
+ /**
1108
+ * Claim an agent invite code to receive a session key for the inviter's wallet.
1109
+ *
1110
+ * This allows an agent to get a pre-configured session key from a human wallet owner
1111
+ * without needing to register their own wallet or fund it. The human sets the budget
1112
+ * limits and merchant whitelist when creating the invite.
1113
+ *
1114
+ * @param options - Claim invite options including the invite code and agent's private key
1115
+ * @returns Claim result with the new session key
1116
+ * @throws {MixrPayError} If claiming fails (e.g., invalid code, already claimed, expired)
1117
+ *
1118
+ * @example
1119
+ * ```typescript
1120
+ * // Human creates invite at their dashboard, shares code "mixr-abc123"
1121
+ *
1122
+ * const result = await AgentWallet.claimInvite({
1123
+ * inviteCode: 'mixr-abc123',
1124
+ * privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
1125
+ * });
1126
+ *
1127
+ * console.log(`Got session key: ${result.sessionKey}`);
1128
+ * console.log(`Budget: $${result.limits.budgetUsd}/${result.limits.budgetPeriod}`);
1129
+ * console.log(`Invited by: ${result.inviterName}`);
1130
+ *
1131
+ * // Use the session key to make payments
1132
+ * const wallet = new AgentWallet({ sessionKey: result.sessionKey });
1133
+ * const response = await wallet.fetch('https://api.example.com/endpoint');
1134
+ * ```
1135
+ */
1136
+ static async claimInvite(options) {
1137
+ const { inviteCode, privateKey } = options;
1138
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
1139
+ const account = privateKeyToAccount2(privateKey);
1140
+ const walletAddress = account.address;
1141
+ const challengeResponse = await fetch(
1142
+ `${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=claim-invite`
1143
+ );
1144
+ if (!challengeResponse.ok) {
1145
+ const error = await challengeResponse.json().catch(() => ({}));
1146
+ throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
1147
+ }
1148
+ const { challenge, message } = await challengeResponse.json();
1149
+ const signature = await signMessage({ message, privateKey });
1150
+ const claimResponse = await fetch(`${baseUrl}/api/v1/agent/claim-invite`, {
1151
+ method: "POST",
1152
+ headers: { "Content-Type": "application/json" },
1153
+ body: JSON.stringify({
1154
+ invite_code: inviteCode,
1155
+ challenge,
1156
+ agent_wallet_address: walletAddress,
1157
+ signature
1158
+ })
1159
+ });
1160
+ if (!claimResponse.ok) {
1161
+ const error = await claimResponse.json().catch(() => ({}));
1162
+ const errorMessage = error.error || `Failed to claim invite: ${claimResponse.status}`;
1163
+ let helpText = "";
1164
+ if (claimResponse.status === 404) {
1165
+ helpText = " The invite code may be invalid or misspelled.";
1166
+ } else if (claimResponse.status === 400) {
1167
+ if (errorMessage.includes("already claimed")) {
1168
+ helpText = " This invite has already been used by another agent.";
1169
+ } else if (errorMessage.includes("expired")) {
1170
+ helpText = " Ask the wallet owner to create a new invite.";
1171
+ } else if (errorMessage.includes("revoked")) {
1172
+ helpText = " The wallet owner has revoked this invite.";
1173
+ }
1174
+ }
1175
+ throw new MixrPayError(`${errorMessage}${helpText}`);
1176
+ }
1177
+ const data = await claimResponse.json();
1178
+ return {
1179
+ sessionKey: data.session_key,
1180
+ address: data.address,
1181
+ sessionKeyId: data.session_key_id,
1182
+ expiresAt: new Date(data.expires_at),
1183
+ limits: {
1184
+ budgetUsd: data.limits.budget_usd,
1185
+ budgetPeriod: data.limits.budget_period,
1186
+ maxPerTxUsd: data.limits.max_per_tx_usd
1187
+ },
1188
+ allowedMerchants: data.allowed_merchants || [],
1189
+ inviterName: data.inviter_name || "Anonymous"
1190
+ };
1191
+ }
967
1192
  // ===========================================================================
968
1193
  // Core Methods
969
1194
  // ===========================================================================
@@ -1002,6 +1227,7 @@ var AgentWallet = class {
1002
1227
  */
1003
1228
  async fetch(url, init) {
1004
1229
  this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
1230
+ await this.validateMerchantAllowed(url, "url");
1005
1231
  const requestId = crypto.randomUUID();
1006
1232
  const correlationId = this.extractCorrelationId(init?.headers);
1007
1233
  const controller = new AbortController();
@@ -1275,7 +1501,8 @@ var AgentWallet = class {
1275
1501
  },
1276
1502
  expiresAt: data.expires_at ? new Date(data.expires_at) : null,
1277
1503
  createdAt: data.created_at ? new Date(data.created_at) : null,
1278
- name: data.name
1504
+ name: data.name,
1505
+ allowedMerchants: data.allowed_merchants ?? data.allowedMerchants ?? []
1279
1506
  };
1280
1507
  this.sessionKeyInfoFetchedAt = Date.now();
1281
1508
  return this.sessionKeyInfo;
@@ -1841,6 +2068,7 @@ var AgentWallet = class {
1841
2068
  feature
1842
2069
  } = options;
1843
2070
  this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
2071
+ await this.validateMerchantAllowed(url, "url");
1844
2072
  if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1845
2073
  throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1846
2074
  }
@@ -2048,6 +2276,7 @@ Timestamp: ${timestamp}`;
2048
2276
  */
2049
2277
  async callMCPTool(toolName, args = {}) {
2050
2278
  this.logger.debug("callMCPTool", { toolName, args });
2279
+ await this.validateMerchantAllowed(toolName, "tool");
2051
2280
  const authHeaders = await this.getMCPAuthHeaders();
2052
2281
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2053
2282
  method: "POST",
@@ -2437,6 +2666,7 @@ Timestamp: ${timestamp}`;
2437
2666
  */
2438
2667
  async callMCPToolWithSession(sessionId, toolName, args = {}) {
2439
2668
  this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
2669
+ await this.validateMerchantAllowed(toolName, "tool");
2440
2670
  const response = await fetch(`${this.baseUrl}/api/mcp`, {
2441
2671
  method: "POST",
2442
2672
  headers: {
@@ -2486,6 +2716,7 @@ export {
2486
2716
  AgentWallet,
2487
2717
  InsufficientBalanceError,
2488
2718
  InvalidSessionKeyError,
2719
+ MerchantNotAllowedError,
2489
2720
  MixrPayError,
2490
2721
  PaymentFailedError,
2491
2722
  SDK_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mixrpay/agent-sdk",
3
- "version": "0.6.1",
3
+ "version": "0.8.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",