@agentwonderland/mcp 0.1.24 → 0.1.26

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.
Files changed (99) hide show
  1. package/dist/core/__tests__/amount-utils.test.d.ts +1 -0
  2. package/dist/core/__tests__/amount-utils.test.js +11 -0
  3. package/dist/core/__tests__/api-client.test.d.ts +1 -0
  4. package/dist/core/__tests__/api-client.test.js +51 -0
  5. package/dist/core/__tests__/formatters.test.js +10 -0
  6. package/dist/core/__tests__/passes-api.test.d.ts +1 -0
  7. package/dist/core/__tests__/passes-api.test.js +27 -0
  8. package/dist/core/__tests__/payments.test.js +59 -6
  9. package/dist/core/__tests__/principal.test.js +41 -4
  10. package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
  11. package/dist/core/__tests__/solana-charge.test.js +50 -0
  12. package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
  13. package/dist/core/__tests__/spend-policy.test.js +40 -0
  14. package/dist/core/amount-utils.d.ts +1 -0
  15. package/dist/core/amount-utils.js +4 -0
  16. package/dist/core/api-client.d.ts +1 -0
  17. package/dist/core/api-client.js +8 -3
  18. package/dist/core/balances.d.ts +1 -0
  19. package/dist/core/balances.js +56 -0
  20. package/dist/core/base-charge.js +16 -8
  21. package/dist/core/config.d.ts +19 -0
  22. package/dist/core/config.js +22 -0
  23. package/dist/core/formatters.d.ts +5 -5
  24. package/dist/core/formatters.js +12 -8
  25. package/dist/core/passes.d.ts +1 -1
  26. package/dist/core/passes.js +5 -2
  27. package/dist/core/payments.d.ts +1 -0
  28. package/dist/core/payments.js +32 -9
  29. package/dist/core/principal.d.ts +3 -0
  30. package/dist/core/principal.js +29 -1
  31. package/dist/core/settings.d.ts +20 -0
  32. package/dist/core/settings.js +19 -0
  33. package/dist/core/solana-charge.d.ts +5 -0
  34. package/dist/core/solana-charge.js +31 -8
  35. package/dist/core/spend-policy.d.ts +12 -0
  36. package/dist/core/spend-policy.js +53 -0
  37. package/dist/core/tempo-charge.d.ts +7 -0
  38. package/dist/core/tempo-charge.js +84 -0
  39. package/dist/core/types.d.ts +1 -2
  40. package/dist/index.js +9 -5
  41. package/dist/prompts/index.js +4 -2
  42. package/dist/resources/agents.js +1 -1
  43. package/dist/tools/__tests__/jobs.test.d.ts +1 -0
  44. package/dist/tools/__tests__/jobs.test.js +71 -0
  45. package/dist/tools/__tests__/run.test.d.ts +1 -0
  46. package/dist/tools/__tests__/run.test.js +149 -0
  47. package/dist/tools/__tests__/solve.test.d.ts +1 -0
  48. package/dist/tools/__tests__/solve.test.js +158 -0
  49. package/dist/tools/__tests__/wallet.test.d.ts +1 -0
  50. package/dist/tools/__tests__/wallet.test.js +230 -0
  51. package/dist/tools/_payment-confirmation.js +1 -1
  52. package/dist/tools/agent-info.js +2 -2
  53. package/dist/tools/favorites.js +1 -1
  54. package/dist/tools/jobs.js +8 -1
  55. package/dist/tools/observability.d.ts +2 -0
  56. package/dist/tools/observability.js +20 -0
  57. package/dist/tools/passes.js +11 -6
  58. package/dist/tools/run.js +45 -29
  59. package/dist/tools/solve.js +53 -40
  60. package/dist/tools/wallet.js +58 -22
  61. package/package.json +2 -2
  62. package/src/core/__tests__/amount-utils.test.ts +13 -0
  63. package/src/core/__tests__/api-client.test.ts +78 -0
  64. package/src/core/__tests__/formatters.test.ts +12 -0
  65. package/src/core/__tests__/passes-api.test.ts +33 -0
  66. package/src/core/__tests__/payments.test.ts +79 -6
  67. package/src/core/__tests__/principal.test.ts +49 -4
  68. package/src/core/__tests__/solana-charge.test.ts +59 -0
  69. package/src/core/__tests__/spend-policy.test.ts +58 -0
  70. package/src/core/amount-utils.ts +5 -0
  71. package/src/core/api-client.ts +16 -3
  72. package/src/core/balances.ts +63 -0
  73. package/src/core/base-charge.ts +16 -8
  74. package/src/core/config.ts +45 -0
  75. package/src/core/formatters.ts +16 -11
  76. package/src/core/passes.ts +5 -2
  77. package/src/core/payments.ts +37 -9
  78. package/src/core/principal.ts +42 -1
  79. package/src/core/settings.ts +36 -0
  80. package/src/core/solana-charge.ts +45 -10
  81. package/src/core/spend-policy.ts +69 -0
  82. package/src/core/tempo-charge.ts +104 -0
  83. package/src/core/types.ts +1 -2
  84. package/src/index.ts +9 -5
  85. package/src/prompts/index.ts +4 -2
  86. package/src/resources/agents.ts +1 -1
  87. package/src/tools/__tests__/jobs.test.ts +89 -0
  88. package/src/tools/__tests__/run.test.ts +176 -0
  89. package/src/tools/__tests__/solve.test.ts +186 -0
  90. package/src/tools/__tests__/wallet.test.ts +289 -0
  91. package/src/tools/_payment-confirmation.ts +1 -1
  92. package/src/tools/agent-info.ts +2 -2
  93. package/src/tools/favorites.ts +1 -4
  94. package/src/tools/jobs.ts +10 -1
  95. package/src/tools/observability.ts +43 -0
  96. package/src/tools/passes.ts +12 -12
  97. package/src/tools/run.ts +50 -41
  98. package/src/tools/solve.ts +58 -52
  99. package/src/tools/wallet.ts +60 -24
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toAtomicAmount } from "../amount-utils.js";
3
+ describe("toAtomicAmount", () => {
4
+ it("converts decimal USDC challenge amounts into atomic units", () => {
5
+ expect(toAtomicAmount("0.020000")).toBe(20000n);
6
+ expect(toAtomicAmount("1.000000")).toBe(1000000n);
7
+ });
8
+ it("supports different decimal precisions", () => {
9
+ expect(toAtomicAmount("0.50", 2)).toBe(50n);
10
+ });
11
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
3
+ mockGetApiUrl: vi.fn(),
4
+ mockGetApiKey: vi.fn(),
5
+ mockGetPaymentFetch: vi.fn(),
6
+ mockEnsureConsumerPrincipalForMethod: vi.fn(),
7
+ mockGetConsumerPrincipalForMethod: vi.fn(),
8
+ mockGetBaseRebatePrincipal: vi.fn(),
9
+ mockPaymentFetch: vi.fn(),
10
+ }));
11
+ vi.mock("../config.js", () => ({
12
+ getApiUrl: () => mockGetApiUrl(),
13
+ getApiKey: () => mockGetApiKey(),
14
+ }));
15
+ vi.mock("../payments.js", () => ({
16
+ getPaymentFetch: (...args) => mockGetPaymentFetch(...args),
17
+ }));
18
+ vi.mock("../principal.js", () => ({
19
+ ensureConsumerPrincipal: vi.fn(),
20
+ ensureConsumerPrincipalForMethod: (...args) => mockEnsureConsumerPrincipalForMethod(...args),
21
+ getConsumerPrincipal: vi.fn(),
22
+ getConsumerPrincipalForMethod: (...args) => mockGetConsumerPrincipalForMethod(...args),
23
+ getBaseRebatePrincipal: (...args) => mockGetBaseRebatePrincipal(...args),
24
+ }));
25
+ describe("api-client headers", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ mockGetApiUrl.mockReturnValue("https://api.agentwonderland.test");
29
+ mockGetApiKey.mockReturnValue(null);
30
+ mockGetPaymentFetch.mockResolvedValue(mockPaymentFetch);
31
+ mockEnsureConsumerPrincipalForMethod.mockResolvedValue("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
32
+ mockGetConsumerPrincipalForMethod.mockResolvedValue("did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
33
+ mockGetBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
34
+ mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
35
+ status: 200,
36
+ headers: { "content-type": "application/json" },
37
+ }));
38
+ });
39
+ it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
40
+ const { apiPostWithPayment } = await import("../api-client.js");
41
+ await apiPostWithPayment("/agents/agent-1/run", { input: { text: "hello" } }, "solana");
42
+ expect(mockPaymentFetch).toHaveBeenCalledWith("https://api.agentwonderland.test/agents/agent-1/run", expect.objectContaining({
43
+ headers: expect.objectContaining({
44
+ "Content-Type": "application/json",
45
+ Accept: "application/json",
46
+ "X-AW-Consumer-Principal": "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
47
+ "X-AW-Rebate-Principal": "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
48
+ }),
49
+ }));
50
+ });
51
+ });
@@ -12,4 +12,14 @@ describe("formatRunResult", () => {
12
12
  expect(output).toContain("Covered by credit pack (pack-123)");
13
13
  expect(output).not.toContain("Paid:");
14
14
  });
15
+ it("formats string-valued settled amounts from job lookups", () => {
16
+ const output = formatRunResult({
17
+ agent_name: "Async Analyzer",
18
+ status: "completed",
19
+ settled_amount: "0.100000",
20
+ job_id: "job-123",
21
+ }, { paymentMethod: "card" });
22
+ expect(output).toContain("Paid: $0.10 via card");
23
+ expect(output).toContain("Job ID: job-123");
24
+ });
15
25
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const state = vi.hoisted(() => ({
3
+ apiGet: vi.fn(),
4
+ }));
5
+ vi.mock("../api-client.js", () => ({
6
+ apiGet: (...args) => state.apiGet(...args),
7
+ }));
8
+ describe("getCreditPackInventory", () => {
9
+ beforeEach(() => {
10
+ vi.resetModules();
11
+ state.apiGet.mockReset();
12
+ });
13
+ it("requests inventory for the selected payment method principal", async () => {
14
+ state.apiGet.mockResolvedValueOnce({
15
+ consumer_principal: "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
16
+ offers: [],
17
+ balances: [],
18
+ });
19
+ const { getCreditPackInventory } = await import("../passes.js");
20
+ const result = await getCreditPackInventory("agent-1", "solana");
21
+ expect(result?.consumer_principal).toContain("did:pkh:solana:");
22
+ expect(state.apiGet).toHaveBeenCalledWith("/agents/agent-1/credit-packs", {
23
+ ensureConsumerPrincipal: true,
24
+ principalMethod: "solana",
25
+ });
26
+ });
27
+ });
@@ -1,20 +1,26 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
2
3
  let currentCard = {
3
4
  consumerToken: "consumer_one",
4
5
  paymentMethodId: "pm_one",
5
6
  last4: "1111",
6
7
  brand: "visa",
7
8
  };
8
- const createdFetches = [vi.fn(), vi.fn(), vi.fn()];
9
+ let currentDefaultWallet;
10
+ let currentWallets = [];
11
+ let currentResolvedMethod = null;
12
+ const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
9
13
  const mockMppxCreate = vi.fn();
10
14
  const mockStripe = vi.fn((opts) => opts);
15
+ const mockTempoChargeClient = vi.fn((..._args) => "tempo_method");
16
+ const mockBaseChargeClient = vi.fn((..._args) => "base_method");
11
17
  vi.mock("../config.js", () => ({
12
18
  getApiUrl: () => "http://api.test",
13
19
  getCardConfig: () => currentCard,
14
20
  getConfig: () => ({ defaultPaymentMethod: "card" }),
15
- getDefaultWallet: () => undefined,
16
- getWallets: () => [],
17
- resolveWalletAndChain: () => null,
21
+ getDefaultWallet: () => currentDefaultWallet,
22
+ getWallets: () => currentWallets,
23
+ resolveWalletAndChain: () => currentResolvedMethod,
18
24
  }));
19
25
  vi.mock("mppx/client", () => ({
20
26
  Mppx: {
@@ -22,19 +28,30 @@ vi.mock("mppx/client", () => ({
22
28
  },
23
29
  stripe: (config) => mockStripe(config),
24
30
  }));
25
- describe("card payment fetch cache", () => {
31
+ vi.mock("../tempo-charge.js", () => ({
32
+ tempoChargeClient: (config) => mockTempoChargeClient(config),
33
+ }));
34
+ vi.mock("../base-charge.js", () => ({
35
+ baseChargeClient: (config) => mockBaseChargeClient(config),
36
+ }));
37
+ describe("payment method initialization", () => {
26
38
  beforeEach(() => {
27
39
  vi.clearAllMocks();
40
+ vi.resetModules();
28
41
  currentCard = {
29
42
  consumerToken: "consumer_one",
30
43
  paymentMethodId: "pm_one",
31
44
  last4: "1111",
32
45
  brand: "visa",
33
46
  };
47
+ currentDefaultWallet = undefined;
48
+ currentWallets = [];
49
+ currentResolvedMethod = null;
34
50
  mockMppxCreate
35
51
  .mockReturnValueOnce({ fetch: createdFetches[0] })
36
52
  .mockReturnValueOnce({ fetch: createdFetches[1] })
37
- .mockReturnValueOnce({ fetch: createdFetches[2] });
53
+ .mockReturnValueOnce({ fetch: createdFetches[2] })
54
+ .mockReturnValueOnce({ fetch: createdFetches[3] });
38
55
  });
39
56
  it("rebuilds the cached card fetch when the card config changes", async () => {
40
57
  const { getPaymentFetch } = await import("../payments.js");
@@ -48,5 +65,41 @@ describe("card payment fetch cache", () => {
48
65
  const secondFetch = await getPaymentFetch("card");
49
66
  expect(firstFetch).not.toBe(secondFetch);
50
67
  expect(mockMppxCreate).toHaveBeenCalledTimes(2);
68
+ expect(mockMppxCreate).toHaveBeenNthCalledWith(1, expect.objectContaining({ polyfill: false }));
69
+ expect(mockMppxCreate).toHaveBeenNthCalledWith(2, expect.objectContaining({ polyfill: false }));
70
+ });
71
+ it("initializes only the Base method when base is requested", async () => {
72
+ const wallet = {
73
+ id: "aw-main",
74
+ keyType: "raw",
75
+ key: TEST_KEY,
76
+ chains: ["tempo", "base"],
77
+ defaultChain: "base",
78
+ };
79
+ currentDefaultWallet = wallet;
80
+ currentWallets = [wallet];
81
+ currentResolvedMethod = { wallet, chain: "base" };
82
+ const { getPaymentFetch } = await import("../payments.js");
83
+ await getPaymentFetch("base");
84
+ expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
85
+ expect(mockTempoChargeClient).not.toHaveBeenCalled();
86
+ expect(mockMppxCreate).toHaveBeenCalledWith(expect.objectContaining({ methods: ["base_method"], polyfill: false }));
87
+ });
88
+ it("initializes only the Tempo method when tempo is requested", async () => {
89
+ const wallet = {
90
+ id: "aw-main",
91
+ keyType: "raw",
92
+ key: TEST_KEY,
93
+ chains: ["tempo", "base"],
94
+ defaultChain: "tempo",
95
+ };
96
+ currentDefaultWallet = wallet;
97
+ currentWallets = [wallet];
98
+ currentResolvedMethod = { wallet, chain: "tempo" };
99
+ const { getPaymentFetch } = await import("../payments.js");
100
+ await getPaymentFetch("tempo");
101
+ expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
102
+ expect(mockBaseChargeClient).not.toHaveBeenCalled();
103
+ expect(mockMppxCreate).toHaveBeenCalledWith(expect.objectContaining({ methods: ["tempo_method"], polyfill: false }));
51
104
  });
52
105
  });
@@ -13,13 +13,24 @@ vi.mock("../config.js", () => ({
13
13
  },
14
14
  getDefaultWallet: () => state.wallets[0],
15
15
  getWallets: () => state.wallets,
16
+ resolveWalletAndChain: (method) => {
17
+ for (const wallet of state.wallets) {
18
+ if (wallet.id === method) {
19
+ return { wallet, chain: wallet.defaultChain ?? wallet.chains[0] };
20
+ }
21
+ if (wallet.chains.includes(method)) {
22
+ return { wallet, chain: method };
23
+ }
24
+ }
25
+ return null;
26
+ },
16
27
  }));
17
28
  vi.mock("../payments.js", () => ({
18
- getWalletAddress: async (walletId) => {
19
- if (walletId in state.addresses) {
20
- return state.addresses[walletId] ?? null;
29
+ getWalletAddress: async (method) => {
30
+ if (method && method in state.addresses) {
31
+ return state.addresses[method] ?? null;
21
32
  }
22
- const wallet = state.wallets.find((entry) => entry.id === walletId);
33
+ const wallet = state.wallets.find((entry) => entry.id === method);
23
34
  if (wallet?.key) {
24
35
  const { privateKeyToAccount } = await import("viem/accounts");
25
36
  return privateKeyToAccount(wallet.key).address;
@@ -64,4 +75,30 @@ describe("consumer principal helpers", () => {
64
75
  expect(state.addedWallets).toHaveLength(1);
65
76
  expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
66
77
  });
78
+ it("derives a Solana principal when the selected payment method is solana", async () => {
79
+ state.wallets = [
80
+ { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
81
+ ];
82
+ state.addresses = {
83
+ solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
84
+ tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
85
+ base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
86
+ };
87
+ const { getConsumerPrincipalForMethod } = await import("../principal.js");
88
+ const principal = await getConsumerPrincipalForMethod("solana");
89
+ expect(principal).toBe("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
90
+ });
91
+ it("returns the Base rebate principal when an EVM wallet is available", async () => {
92
+ state.wallets = [
93
+ { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
94
+ ];
95
+ state.addresses = {
96
+ solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
97
+ tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
98
+ base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
99
+ };
100
+ const { getBaseRebatePrincipal } = await import("../principal.js");
101
+ const principal = await getBaseRebatePrincipal();
102
+ expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
103
+ });
67
104
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { SOLANA_USDC_MINT } from "../solana-charge.js";
5
+ describe("resolveRecipientTokenAccount", () => {
6
+ it("uses the recipient directly when Stripe gives us a token account", async () => {
7
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
8
+ const connection = {
9
+ getParsedAccountInfo: vi.fn().mockResolvedValue({
10
+ value: { owner: TOKEN_PROGRAM_ID },
11
+ }),
12
+ };
13
+ const mint = new PublicKey(SOLANA_USDC_MINT);
14
+ const recipient = new PublicKey("42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
15
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
16
+ expect(resolved.tokenAccount.toBase58()).toBe(recipient.toBase58());
17
+ expect(resolved.needsCreateAssociatedAccount).toBe(false);
18
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(1);
19
+ });
20
+ it("reuses the associated token account when it already exists", async () => {
21
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
22
+ const connection = {
23
+ getParsedAccountInfo: vi.fn()
24
+ .mockResolvedValueOnce({ value: null })
25
+ .mockResolvedValueOnce({ value: { owner: TOKEN_PROGRAM_ID } }),
26
+ };
27
+ const mint = new PublicKey(SOLANA_USDC_MINT);
28
+ const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
29
+ const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
30
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
31
+ expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
32
+ expect(resolved.needsCreateAssociatedAccount).toBe(false);
33
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
34
+ });
35
+ it("marks the associated token account for creation when neither account exists", async () => {
36
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
37
+ const connection = {
38
+ getParsedAccountInfo: vi.fn()
39
+ .mockResolvedValueOnce({ value: null })
40
+ .mockResolvedValueOnce({ value: null }),
41
+ };
42
+ const mint = new PublicKey(SOLANA_USDC_MINT);
43
+ const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
44
+ const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
45
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
46
+ expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
47
+ expect(resolved.needsCreateAssociatedAccount).toBe(true);
48
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
49
+ });
50
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const state = {
3
+ policies: {},
4
+ ledger: [],
5
+ };
6
+ vi.mock("../config.js", () => ({
7
+ getSpendPolicy: (method) => state.policies[method] ?? null,
8
+ getSpendLedger: () => state.ledger,
9
+ saveSpendLedger: (entries) => {
10
+ state.ledger = entries;
11
+ },
12
+ }));
13
+ import { canSpend, getDailySpend, recordSpend, requiresPolicyConfirmation, } from "../spend-policy.js";
14
+ describe("spend-policy", () => {
15
+ beforeEach(() => {
16
+ state.policies = {};
17
+ state.ledger = [];
18
+ });
19
+ it("allows spend when no policy is set", () => {
20
+ expect(canSpend({ method: "wallet-1", amountUsd: 5 })).toEqual({ ok: true });
21
+ });
22
+ it("blocks spends above max_per_tx", () => {
23
+ state.policies["wallet-1"] = { maxPerTxUsd: 2 };
24
+ const result = canSpend({ method: "wallet-1", amountUsd: 3 });
25
+ expect(result.ok).toBe(false);
26
+ });
27
+ it("tracks daily spend totals per method", () => {
28
+ const now = new Date("2026-04-08T12:00:00.000Z");
29
+ recordSpend("wallet-1", 1.25, now);
30
+ recordSpend("wallet-1", 0.75, now);
31
+ recordSpend("wallet-2", 10, now);
32
+ expect(getDailySpend("wallet-1", now)).toBeCloseTo(2);
33
+ expect(getDailySpend("wallet-2", now)).toBeCloseTo(10);
34
+ });
35
+ it("requires explicit confirmation above configured threshold", () => {
36
+ state.policies["wallet-1"] = { requireConfirmationAboveUsd: 2 };
37
+ expect(requiresPolicyConfirmation("wallet-1", 1.99)).toBe(false);
38
+ expect(requiresPolicyConfirmation("wallet-1", 2)).toBe(true);
39
+ });
40
+ });
@@ -0,0 +1 @@
1
+ export declare function toAtomicAmount(amount: string, decimals?: number): bigint;
@@ -0,0 +1,4 @@
1
+ import { parseUnits } from "viem";
2
+ export function toAtomicAmount(amount, decimals = 6) {
3
+ return parseUnits(amount, decimals);
4
+ }
@@ -5,6 +5,7 @@ export declare class ApiError extends Error {
5
5
  }
6
6
  interface RequestOptions {
7
7
  ensureConsumerPrincipal?: boolean;
8
+ principalMethod?: string;
8
9
  extraHeaders?: Record<string, string>;
9
10
  }
10
11
  export declare function apiGet<T>(path: string, options?: RequestOptions): Promise<T>;
@@ -1,6 +1,6 @@
1
1
  import { getApiUrl, getApiKey } from "./config.js";
2
2
  import { getPaymentFetch } from "./payments.js";
3
- import { ensureConsumerPrincipal, getConsumerPrincipal } from "./principal.js";
3
+ import { getBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
4
4
  // ── Error class ────────────────────────────────────────────────────
5
5
  export class ApiError extends Error {
6
6
  status;
@@ -22,11 +22,15 @@ async function buildHeaders(options) {
22
22
  headers["Authorization"] = `Bearer ${apiKey}`;
23
23
  }
24
24
  const principal = options?.ensureConsumerPrincipal
25
- ? await ensureConsumerPrincipal()
26
- : await getConsumerPrincipal();
25
+ ? await ensureConsumerPrincipalForMethod(options?.principalMethod)
26
+ : await getConsumerPrincipalForMethod(options?.principalMethod);
27
27
  if (principal) {
28
28
  headers["X-AW-Consumer-Principal"] = principal;
29
29
  }
30
+ const rebatePrincipal = await getBaseRebatePrincipal();
31
+ if (rebatePrincipal) {
32
+ headers["X-AW-Rebate-Principal"] = rebatePrincipal;
33
+ }
30
34
  if (options?.extraHeaders) {
31
35
  Object.assign(headers, options.extraHeaders);
32
36
  }
@@ -109,6 +113,7 @@ export async function apiPostWithPayment(path, body, payWith, options) {
109
113
  method: "POST",
110
114
  headers: await buildHeaders({
111
115
  ensureConsumerPrincipal: true,
116
+ principalMethod: payWith,
112
117
  ...options,
113
118
  }),
114
119
  body: JSON.stringify(body),
@@ -0,0 +1 @@
1
+ export declare function fetchUsdcBalance(chain: "tempo" | "base" | "solana", address: string): Promise<string | null>;
@@ -0,0 +1,56 @@
1
+ import { getSettings } from "./settings.js";
2
+ const ERC20_BALANCE_ABI = [
3
+ {
4
+ type: "function",
5
+ name: "balanceOf",
6
+ stateMutability: "view",
7
+ inputs: [{ name: "account", type: "address" }],
8
+ outputs: [{ name: "", type: "uint256" }],
9
+ },
10
+ ];
11
+ async function fetchEvmUsdcBalance(chain, address) {
12
+ try {
13
+ const { createPublicClient, http, formatUnits } = await import("viem");
14
+ const client = createPublicClient({ transport: http(chain.rpcUrl) });
15
+ const raw = await client.readContract({
16
+ address: chain.usdc,
17
+ abi: ERC20_BALANCE_ABI,
18
+ functionName: "balanceOf",
19
+ args: [address],
20
+ });
21
+ return formatUnits(raw, 6);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async function fetchSolanaUsdcBalance(chain, address) {
28
+ try {
29
+ const { Connection, PublicKey } = await import("@solana/web3.js");
30
+ const connection = new Connection(chain.rpcUrl, "confirmed");
31
+ const owner = new PublicKey(address);
32
+ const mint = new PublicKey(chain.usdc);
33
+ const accounts = await connection.getParsedTokenAccountsByOwner(owner, { mint });
34
+ let total = 0;
35
+ for (const { account } of accounts.value) {
36
+ const amount = account.data?.parsed?.info?.tokenAmount?.uiAmount;
37
+ if (typeof amount === "number")
38
+ total += amount;
39
+ }
40
+ return total.toString();
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ export async function fetchUsdcBalance(chain, address) {
47
+ const settings = await getSettings();
48
+ if (!settings)
49
+ return null;
50
+ const chainConfig = settings.chains[chain];
51
+ if (!chainConfig)
52
+ return null;
53
+ if (chain === "solana")
54
+ return fetchSolanaUsdcBalance(chainConfig, address);
55
+ return fetchEvmUsdcBalance(chainConfig, address);
56
+ }
@@ -5,9 +5,12 @@
5
5
  * the tx hash as a credential. Plugs into mppx's compose/dispatch system.
6
6
  */
7
7
  import { Method, Credential, z } from "mppx";
8
+ import { toAtomicAmount } from "./amount-utils.js";
8
9
  // Base USDC (Circle native)
9
10
  const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
11
+ const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
10
12
  const BASE_CHAIN_ID = 8453;
13
+ const BASE_SEPOLIA_CHAIN_ID = 84532;
11
14
  // Standard ERC-20 transfer function ABI
12
15
  const ERC20_ABI = [
13
16
  {
@@ -50,20 +53,25 @@ export function baseChargeClient(config) {
50
53
  return Method.toClient(baseChargeMethod, {
51
54
  async createCredential({ challenge }) {
52
55
  const { request } = challenge;
53
- const amount = BigInt(request.amount);
56
+ const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
54
57
  const recipient = request.recipient;
55
- const currency = (request.currency ?? BASE_USDC);
58
+ const chainId = request.chainId ?? BASE_CHAIN_ID;
59
+ const currency = (request.currency
60
+ ?? (chainId === BASE_SEPOLIA_CHAIN_ID ? BASE_SEPOLIA_USDC : BASE_USDC));
56
61
  // Dynamic imports to keep the module lightweight
57
- const { createWalletClient, createPublicClient, http, encodeFunctionData } = await import("viem");
58
- const { base } = await import("viem/chains");
59
- const rpcUrl = config.rpcUrl ?? "https://mainnet.base.org";
62
+ const { createWalletClient, createPublicClient, http } = await import("viem");
63
+ const { base, baseSepolia } = await import("viem/chains");
64
+ const chain = chainId === BASE_SEPOLIA_CHAIN_ID ? baseSepolia : base;
65
+ const rpcUrl = config.rpcUrl ?? (chainId === BASE_SEPOLIA_CHAIN_ID
66
+ ? "https://sepolia.base.org"
67
+ : "https://mainnet.base.org");
60
68
  const walletClient = createWalletClient({
61
69
  account: config.account,
62
- chain: base,
70
+ chain,
63
71
  transport: http(rpcUrl),
64
72
  });
65
73
  const publicClient = createPublicClient({
66
- chain: base,
74
+ chain,
67
75
  transport: http(rpcUrl),
68
76
  });
69
77
  // Send ERC-20 transfer
@@ -78,7 +86,7 @@ export function baseChargeClient(config) {
78
86
  return Credential.serialize({
79
87
  challenge,
80
88
  payload: { hash, type: "hash" },
81
- source: `did:pkh:eip155:${BASE_CHAIN_ID}:${config.account.address}`,
89
+ source: `did:pkh:eip155:${chainId}:${config.account.address}`,
82
90
  });
83
91
  },
84
92
  });
@@ -13,6 +13,17 @@ export interface CardConfig {
13
13
  last4: string;
14
14
  brand: string;
15
15
  }
16
+ export interface SpendPolicy {
17
+ maxPerTxUsd?: number;
18
+ maxPerDayUsd?: number;
19
+ requireConfirmationAboveUsd?: number;
20
+ }
21
+ export interface SpendLedgerEntry {
22
+ method: string;
23
+ amountUsd: number;
24
+ day: string;
25
+ timestamp: string;
26
+ }
16
27
  export interface Config {
17
28
  apiUrl: string;
18
29
  apiKey: string | null;
@@ -27,6 +38,10 @@ export interface Config {
27
38
  confirmBeforeSpend: boolean;
28
39
  /** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
29
40
  defaultTipAmount: number;
41
+ /** Optional per-method spend policies enforced client-side by MCP tools. */
42
+ spendPolicies?: Record<string, SpendPolicy>;
43
+ /** Daily spend ledger used to enforce max-per-day limits. */
44
+ spendLedger?: SpendLedgerEntry[];
30
45
  }
31
46
  /** All supported chain identifiers. */
32
47
  export declare const SUPPORTED_CHAINS: readonly ["tempo", "base", "solana"];
@@ -43,6 +58,10 @@ export declare function getApiKey(): string | null;
43
58
  export declare function isAuthenticated(): boolean;
44
59
  export declare function requiresSpendConfirmation(): boolean;
45
60
  export declare function getDefaultTipAmount(): number;
61
+ export declare function getSpendPolicy(method: string): SpendPolicy | null;
62
+ export declare function setSpendPolicy(method: string, policy: SpendPolicy): void;
63
+ export declare function getSpendLedger(): SpendLedgerEntry[];
64
+ export declare function saveSpendLedger(entries: SpendLedgerEntry[]): void;
46
65
  /**
47
66
  * Get all wallets from config + env var synthetic wallets.
48
67
  */
@@ -40,6 +40,8 @@ function migrateIfNeeded(raw) {
40
40
  favorites: r.favorites ?? [],
41
41
  confirmBeforeSpend: r.confirmBeforeSpend !== false,
42
42
  defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
43
+ spendPolicies: r.spendPolicies,
44
+ spendLedger: r.spendLedger,
43
45
  };
44
46
  }
45
47
  // Build wallets from legacy flat fields
@@ -111,6 +113,8 @@ function migrateIfNeeded(raw) {
111
113
  favorites: [],
112
114
  confirmBeforeSpend: true,
113
115
  defaultTipAmount: 0,
116
+ spendPolicies: {},
117
+ spendLedger: [],
114
118
  };
115
119
  // Write migrated config (only if there was something to migrate)
116
120
  if (raw.tempoPrivateKey || raw.evmPrivateKey || raw.stripeConsumerToken) {
@@ -132,6 +136,8 @@ export function getConfig() {
132
136
  favorites: [],
133
137
  confirmBeforeSpend: true,
134
138
  defaultTipAmount: 0,
139
+ spendPolicies: {},
140
+ spendLedger: [],
135
141
  };
136
142
  if (!existsSync(CONFIG_FILE)) {
137
143
  return defaults;
@@ -172,6 +178,22 @@ export function requiresSpendConfirmation() {
172
178
  export function getDefaultTipAmount() {
173
179
  return getConfig().defaultTipAmount;
174
180
  }
181
+ export function getSpendPolicy(method) {
182
+ const policies = getConfig().spendPolicies ?? {};
183
+ return policies[method] ?? null;
184
+ }
185
+ export function setSpendPolicy(method, policy) {
186
+ const config = getConfig();
187
+ const policies = config.spendPolicies ?? {};
188
+ policies[method] = policy;
189
+ saveConfig({ spendPolicies: policies });
190
+ }
191
+ export function getSpendLedger() {
192
+ return getConfig().spendLedger ?? [];
193
+ }
194
+ export function saveSpendLedger(entries) {
195
+ saveConfig({ spendLedger: entries });
196
+ }
175
197
  // ── Wallet helpers ─────────────────────────────────────────────────
176
198
  /**
177
199
  * Get all wallets from config + env var synthetic wallets.