@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,33 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const state = vi.hoisted(() => ({
4
+ apiGet: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("../api-client.js", () => ({
8
+ apiGet: (...args: unknown[]) => state.apiGet(...args),
9
+ }));
10
+
11
+ describe("getCreditPackInventory", () => {
12
+ beforeEach(() => {
13
+ vi.resetModules();
14
+ state.apiGet.mockReset();
15
+ });
16
+
17
+ it("requests inventory for the selected payment method principal", async () => {
18
+ state.apiGet.mockResolvedValueOnce({
19
+ consumer_principal: "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
20
+ offers: [],
21
+ balances: [],
22
+ });
23
+
24
+ const { getCreditPackInventory } = await import("../passes.js");
25
+ const result = await getCreditPackInventory("agent-1", "solana");
26
+
27
+ expect(result?.consumer_principal).toContain("did:pkh:solana:");
28
+ expect(state.apiGet).toHaveBeenCalledWith("/agents/agent-1/credit-packs", {
29
+ ensureConsumerPrincipal: true,
30
+ principalMethod: "solana",
31
+ });
32
+ });
33
+ });
@@ -1,5 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
+ const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
4
+
3
5
  let currentCard = {
4
6
  consumerToken: "consumer_one",
5
7
  paymentMethodId: "pm_one",
@@ -7,17 +9,23 @@ let currentCard = {
7
9
  brand: "visa",
8
10
  };
9
11
 
10
- const createdFetches = [vi.fn(), vi.fn(), vi.fn()];
12
+ let currentDefaultWallet: any;
13
+ let currentWallets: any[] = [];
14
+ let currentResolvedMethod: { wallet: any; chain: string } | null = null;
15
+
16
+ const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
11
17
  const mockMppxCreate = vi.fn();
12
18
  const mockStripe = vi.fn((opts: unknown) => opts);
19
+ const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
20
+ const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
13
21
 
14
22
  vi.mock("../config.js", () => ({
15
23
  getApiUrl: () => "http://api.test",
16
24
  getCardConfig: () => currentCard,
17
25
  getConfig: () => ({ defaultPaymentMethod: "card" }),
18
- getDefaultWallet: () => undefined,
19
- getWallets: () => [],
20
- resolveWalletAndChain: () => null,
26
+ getDefaultWallet: () => currentDefaultWallet,
27
+ getWallets: () => currentWallets,
28
+ resolveWalletAndChain: () => currentResolvedMethod,
21
29
  }));
22
30
 
23
31
  vi.mock("mppx/client", () => ({
@@ -27,19 +35,32 @@ vi.mock("mppx/client", () => ({
27
35
  stripe: (config: unknown) => mockStripe(config),
28
36
  }));
29
37
 
30
- describe("card payment fetch cache", () => {
38
+ vi.mock("../tempo-charge.js", () => ({
39
+ tempoChargeClient: (config: unknown) => mockTempoChargeClient(config),
40
+ }));
41
+
42
+ vi.mock("../base-charge.js", () => ({
43
+ baseChargeClient: (config: unknown) => mockBaseChargeClient(config),
44
+ }));
45
+
46
+ describe("payment method initialization", () => {
31
47
  beforeEach(() => {
32
48
  vi.clearAllMocks();
49
+ vi.resetModules();
33
50
  currentCard = {
34
51
  consumerToken: "consumer_one",
35
52
  paymentMethodId: "pm_one",
36
53
  last4: "1111",
37
54
  brand: "visa",
38
55
  };
56
+ currentDefaultWallet = undefined;
57
+ currentWallets = [];
58
+ currentResolvedMethod = null;
39
59
  mockMppxCreate
40
60
  .mockReturnValueOnce({ fetch: createdFetches[0] })
41
61
  .mockReturnValueOnce({ fetch: createdFetches[1] })
42
- .mockReturnValueOnce({ fetch: createdFetches[2] });
62
+ .mockReturnValueOnce({ fetch: createdFetches[2] })
63
+ .mockReturnValueOnce({ fetch: createdFetches[3] });
43
64
  });
44
65
 
45
66
  it("rebuilds the cached card fetch when the card config changes", async () => {
@@ -56,5 +77,57 @@ describe("card payment fetch cache", () => {
56
77
 
57
78
  expect(firstFetch).not.toBe(secondFetch);
58
79
  expect(mockMppxCreate).toHaveBeenCalledTimes(2);
80
+ expect(mockMppxCreate).toHaveBeenNthCalledWith(
81
+ 1,
82
+ expect.objectContaining({ polyfill: false }),
83
+ );
84
+ expect(mockMppxCreate).toHaveBeenNthCalledWith(
85
+ 2,
86
+ expect.objectContaining({ polyfill: false }),
87
+ );
88
+ });
89
+
90
+ it("initializes only the Base method when base is requested", async () => {
91
+ const wallet = {
92
+ id: "aw-main",
93
+ keyType: "raw",
94
+ key: TEST_KEY,
95
+ chains: ["tempo", "base"],
96
+ defaultChain: "base",
97
+ };
98
+ currentDefaultWallet = wallet;
99
+ currentWallets = [wallet];
100
+ currentResolvedMethod = { wallet, chain: "base" };
101
+
102
+ const { getPaymentFetch } = await import("../payments.js");
103
+ await getPaymentFetch("base");
104
+
105
+ expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
106
+ expect(mockTempoChargeClient).not.toHaveBeenCalled();
107
+ expect(mockMppxCreate).toHaveBeenCalledWith(
108
+ expect.objectContaining({ methods: ["base_method"], polyfill: false }),
109
+ );
110
+ });
111
+
112
+ it("initializes only the Tempo method when tempo is requested", async () => {
113
+ const wallet = {
114
+ id: "aw-main",
115
+ keyType: "raw",
116
+ key: TEST_KEY,
117
+ chains: ["tempo", "base"],
118
+ defaultChain: "tempo",
119
+ };
120
+ currentDefaultWallet = wallet;
121
+ currentWallets = [wallet];
122
+ currentResolvedMethod = { wallet, chain: "tempo" };
123
+
124
+ const { getPaymentFetch } = await import("../payments.js");
125
+ await getPaymentFetch("tempo");
126
+
127
+ expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
128
+ expect(mockBaseChargeClient).not.toHaveBeenCalled();
129
+ expect(mockMppxCreate).toHaveBeenCalledWith(
130
+ expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
131
+ );
59
132
  });
60
133
  });
@@ -23,15 +23,26 @@ vi.mock("../config.js", () => ({
23
23
  },
24
24
  getDefaultWallet: () => state.wallets[0],
25
25
  getWallets: () => state.wallets,
26
+ resolveWalletAndChain: (method: string) => {
27
+ for (const wallet of state.wallets) {
28
+ if (wallet.id === method) {
29
+ return { wallet, chain: wallet.defaultChain ?? wallet.chains[0] };
30
+ }
31
+ if (wallet.chains.includes(method)) {
32
+ return { wallet, chain: method };
33
+ }
34
+ }
35
+ return null;
36
+ },
26
37
  }));
27
38
 
28
39
  vi.mock("../payments.js", () => ({
29
- getWalletAddress: async (walletId: string) => {
30
- if (walletId in state.addresses) {
31
- return state.addresses[walletId] ?? null;
40
+ getWalletAddress: async (method?: string) => {
41
+ if (method && method in state.addresses) {
42
+ return state.addresses[method] ?? null;
32
43
  }
33
44
 
34
- const wallet = state.wallets.find((entry) => entry.id === walletId);
45
+ const wallet = state.wallets.find((entry) => entry.id === method);
35
46
  if (wallet?.key) {
36
47
  const { privateKeyToAccount } = await import("viem/accounts");
37
48
  return privateKeyToAccount(wallet.key as `0x${string}`).address;
@@ -84,4 +95,38 @@ describe("consumer principal helpers", () => {
84
95
  expect(state.addedWallets).toHaveLength(1);
85
96
  expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
86
97
  });
98
+
99
+ it("derives a Solana principal when the selected payment method is solana", async () => {
100
+ state.wallets = [
101
+ { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
102
+ ];
103
+ state.addresses = {
104
+ solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
105
+ tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
106
+ base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
107
+ };
108
+
109
+ const { getConsumerPrincipalForMethod } = await import("../principal.js");
110
+ const principal = await getConsumerPrincipalForMethod("solana");
111
+
112
+ expect(principal).toBe(
113
+ "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
114
+ );
115
+ });
116
+
117
+ it("returns the Base rebate principal when an EVM wallet is available", async () => {
118
+ state.wallets = [
119
+ { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
120
+ ];
121
+ state.addresses = {
122
+ solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
123
+ tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
124
+ base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
125
+ };
126
+
127
+ const { getBaseRebatePrincipal } = await import("../principal.js");
128
+ const principal = await getBaseRebatePrincipal();
129
+
130
+ expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
131
+ });
87
132
  });
@@ -0,0 +1,59 @@
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
+
6
+ describe("resolveRecipientTokenAccount", () => {
7
+ it("uses the recipient directly when Stripe gives us a token account", async () => {
8
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
9
+ const connection = {
10
+ getParsedAccountInfo: vi.fn().mockResolvedValue({
11
+ value: { owner: TOKEN_PROGRAM_ID },
12
+ }),
13
+ } as any;
14
+ const mint = new PublicKey(SOLANA_USDC_MINT);
15
+ const recipient = new PublicKey("42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
16
+
17
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
18
+
19
+ expect(resolved.tokenAccount.toBase58()).toBe(recipient.toBase58());
20
+ expect(resolved.needsCreateAssociatedAccount).toBe(false);
21
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(1);
22
+ });
23
+
24
+ it("reuses the associated token account when it already exists", async () => {
25
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
26
+ const connection = {
27
+ getParsedAccountInfo: vi.fn()
28
+ .mockResolvedValueOnce({ value: null })
29
+ .mockResolvedValueOnce({ value: { owner: TOKEN_PROGRAM_ID } }),
30
+ } as any;
31
+ const mint = new PublicKey(SOLANA_USDC_MINT);
32
+ const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
33
+ const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
34
+
35
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
36
+
37
+ expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
38
+ expect(resolved.needsCreateAssociatedAccount).toBe(false);
39
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
40
+ });
41
+
42
+ it("marks the associated token account for creation when neither account exists", async () => {
43
+ const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
44
+ const connection = {
45
+ getParsedAccountInfo: vi.fn()
46
+ .mockResolvedValueOnce({ value: null })
47
+ .mockResolvedValueOnce({ value: null }),
48
+ } as any;
49
+ const mint = new PublicKey(SOLANA_USDC_MINT);
50
+ const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
51
+ const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
52
+
53
+ const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
54
+
55
+ expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
56
+ expect(resolved.needsCreateAssociatedAccount).toBe(true);
57
+ expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
58
+ });
59
+ });
@@ -0,0 +1,58 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const state = {
4
+ policies: {} as Record<string, {
5
+ maxPerTxUsd?: number;
6
+ maxPerDayUsd?: number;
7
+ requireConfirmationAboveUsd?: number;
8
+ }>,
9
+ ledger: [] as Array<{ method: string; amountUsd: number; day: string; timestamp: string }>,
10
+ };
11
+
12
+ vi.mock("../config.js", () => ({
13
+ getSpendPolicy: (method: string) => state.policies[method] ?? null,
14
+ getSpendLedger: () => state.ledger,
15
+ saveSpendLedger: (entries: Array<{ method: string; amountUsd: number; day: string; timestamp: string }>) => {
16
+ state.ledger = entries;
17
+ },
18
+ }));
19
+
20
+ import {
21
+ canSpend,
22
+ getDailySpend,
23
+ recordSpend,
24
+ requiresPolicyConfirmation,
25
+ } from "../spend-policy.js";
26
+
27
+ describe("spend-policy", () => {
28
+ beforeEach(() => {
29
+ state.policies = {};
30
+ state.ledger = [];
31
+ });
32
+
33
+ it("allows spend when no policy is set", () => {
34
+ expect(canSpend({ method: "wallet-1", amountUsd: 5 })).toEqual({ ok: true });
35
+ });
36
+
37
+ it("blocks spends above max_per_tx", () => {
38
+ state.policies["wallet-1"] = { maxPerTxUsd: 2 };
39
+ const result = canSpend({ method: "wallet-1", amountUsd: 3 });
40
+ expect(result.ok).toBe(false);
41
+ });
42
+
43
+ it("tracks daily spend totals per method", () => {
44
+ const now = new Date("2026-04-08T12:00:00.000Z");
45
+ recordSpend("wallet-1", 1.25, now);
46
+ recordSpend("wallet-1", 0.75, now);
47
+ recordSpend("wallet-2", 10, now);
48
+
49
+ expect(getDailySpend("wallet-1", now)).toBeCloseTo(2);
50
+ expect(getDailySpend("wallet-2", now)).toBeCloseTo(10);
51
+ });
52
+
53
+ it("requires explicit confirmation above configured threshold", () => {
54
+ state.policies["wallet-1"] = { requireConfirmationAboveUsd: 2 };
55
+ expect(requiresPolicyConfirmation("wallet-1", 1.99)).toBe(false);
56
+ expect(requiresPolicyConfirmation("wallet-1", 2)).toBe(true);
57
+ });
58
+ });
@@ -0,0 +1,5 @@
1
+ import { parseUnits } from "viem";
2
+
3
+ export function toAtomicAmount(amount: string, decimals = 6): bigint {
4
+ return parseUnits(amount, decimals);
5
+ }
@@ -1,6 +1,12 @@
1
1
  import { getApiUrl, getApiKey } from "./config.js";
2
2
  import { getPaymentFetch } from "./payments.js";
3
- import { ensureConsumerPrincipal, getConsumerPrincipal } from "./principal.js";
3
+ import {
4
+ getBaseRebatePrincipal,
5
+ ensureConsumerPrincipal,
6
+ ensureConsumerPrincipalForMethod,
7
+ getConsumerPrincipal,
8
+ getConsumerPrincipalForMethod,
9
+ } from "./principal.js";
4
10
 
5
11
  // ── Error class ────────────────────────────────────────────────────
6
12
 
@@ -19,6 +25,7 @@ export class ApiError extends Error {
19
25
 
20
26
  interface RequestOptions {
21
27
  ensureConsumerPrincipal?: boolean;
28
+ principalMethod?: string;
22
29
  extraHeaders?: Record<string, string>;
23
30
  }
24
31
 
@@ -34,12 +41,17 @@ async function buildHeaders(options?: RequestOptions): Promise<Record<string, st
34
41
  }
35
42
 
36
43
  const principal = options?.ensureConsumerPrincipal
37
- ? await ensureConsumerPrincipal()
38
- : await getConsumerPrincipal();
44
+ ? await ensureConsumerPrincipalForMethod(options?.principalMethod)
45
+ : await getConsumerPrincipalForMethod(options?.principalMethod);
39
46
  if (principal) {
40
47
  headers["X-AW-Consumer-Principal"] = principal;
41
48
  }
42
49
 
50
+ const rebatePrincipal = await getBaseRebatePrincipal();
51
+ if (rebatePrincipal) {
52
+ headers["X-AW-Rebate-Principal"] = rebatePrincipal;
53
+ }
54
+
43
55
  if (options?.extraHeaders) {
44
56
  Object.assign(headers, options.extraHeaders);
45
57
  }
@@ -141,6 +153,7 @@ export async function apiPostWithPayment<T>(
141
153
  method: "POST",
142
154
  headers: await buildHeaders({
143
155
  ensureConsumerPrincipal: true,
156
+ principalMethod: payWith,
144
157
  ...options,
145
158
  }),
146
159
  body: JSON.stringify(body),
@@ -0,0 +1,63 @@
1
+ import { getSettings, type ChainSettings } from "./settings.js";
2
+
3
+ const ERC20_BALANCE_ABI = [
4
+ {
5
+ type: "function",
6
+ name: "balanceOf",
7
+ stateMutability: "view",
8
+ inputs: [{ name: "account", type: "address" }],
9
+ outputs: [{ name: "", type: "uint256" }],
10
+ },
11
+ ] as const;
12
+
13
+ async function fetchEvmUsdcBalance(
14
+ chain: ChainSettings,
15
+ address: string,
16
+ ): Promise<string | null> {
17
+ try {
18
+ const { createPublicClient, http, formatUnits } = await import("viem");
19
+ const client = createPublicClient({ transport: http(chain.rpcUrl) });
20
+ const raw = await client.readContract({
21
+ address: chain.usdc as `0x${string}`,
22
+ abi: ERC20_BALANCE_ABI,
23
+ functionName: "balanceOf",
24
+ args: [address as `0x${string}`],
25
+ });
26
+ return formatUnits(raw as bigint, 6);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ async function fetchSolanaUsdcBalance(
33
+ chain: ChainSettings,
34
+ address: string,
35
+ ): Promise<string | null> {
36
+ try {
37
+ const { Connection, PublicKey } = await import("@solana/web3.js");
38
+ const connection = new Connection(chain.rpcUrl, "confirmed");
39
+ const owner = new PublicKey(address);
40
+ const mint = new PublicKey(chain.usdc);
41
+ const accounts = await connection.getParsedTokenAccountsByOwner(owner, { mint });
42
+ let total = 0;
43
+ for (const { account } of accounts.value) {
44
+ const amount = (account.data as any)?.parsed?.info?.tokenAmount?.uiAmount;
45
+ if (typeof amount === "number") total += amount;
46
+ }
47
+ return total.toString();
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export async function fetchUsdcBalance(
54
+ chain: "tempo" | "base" | "solana",
55
+ address: string,
56
+ ): Promise<string | null> {
57
+ const settings = await getSettings();
58
+ if (!settings) return null;
59
+ const chainConfig = settings.chains[chain];
60
+ if (!chainConfig) return null;
61
+ if (chain === "solana") return fetchSolanaUsdcBalance(chainConfig, address);
62
+ return fetchEvmUsdcBalance(chainConfig, address);
63
+ }
@@ -6,10 +6,13 @@
6
6
  */
7
7
  import { Method, Credential, z } from "mppx";
8
8
  import type { LocalAccount } from "viem/accounts";
9
+ import { toAtomicAmount } from "./amount-utils.js";
9
10
 
10
11
  // Base USDC (Circle native)
11
12
  const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
13
+ const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as const;
12
14
  const BASE_CHAIN_ID = 8453;
15
+ const BASE_SEPOLIA_CHAIN_ID = 84532;
13
16
 
14
17
  // Standard ERC-20 transfer function ABI
15
18
  const ERC20_ABI = [
@@ -63,24 +66,29 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
63
66
  return Method.toClient(baseChargeMethod as any, {
64
67
  async createCredential({ challenge }: any) {
65
68
  const { request } = challenge;
66
- const amount = BigInt(request.amount);
69
+ const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
67
70
  const recipient = request.recipient as `0x${string}`;
68
- const currency = (request.currency ?? BASE_USDC) as `0x${string}`;
71
+ const chainId = request.chainId ?? BASE_CHAIN_ID;
72
+ const currency = (request.currency
73
+ ?? (chainId === BASE_SEPOLIA_CHAIN_ID ? BASE_SEPOLIA_USDC : BASE_USDC)) as `0x${string}`;
69
74
 
70
75
  // Dynamic imports to keep the module lightweight
71
- const { createWalletClient, createPublicClient, http, encodeFunctionData } = await import("viem");
72
- const { base } = await import("viem/chains");
76
+ const { createWalletClient, createPublicClient, http } = await import("viem");
77
+ const { base, baseSepolia } = await import("viem/chains");
73
78
 
74
- const rpcUrl = config.rpcUrl ?? "https://mainnet.base.org";
79
+ const chain = chainId === BASE_SEPOLIA_CHAIN_ID ? baseSepolia : base;
80
+ const rpcUrl = config.rpcUrl ?? (chainId === BASE_SEPOLIA_CHAIN_ID
81
+ ? "https://sepolia.base.org"
82
+ : "https://mainnet.base.org");
75
83
 
76
84
  const walletClient = createWalletClient({
77
85
  account: config.account,
78
- chain: base,
86
+ chain,
79
87
  transport: http(rpcUrl),
80
88
  });
81
89
 
82
90
  const publicClient = createPublicClient({
83
- chain: base,
91
+ chain,
84
92
  transport: http(rpcUrl),
85
93
  });
86
94
 
@@ -98,7 +106,7 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
98
106
  return Credential.serialize({
99
107
  challenge,
100
108
  payload: { hash, type: "hash" as const },
101
- source: `did:pkh:eip155:${BASE_CHAIN_ID}:${config.account.address}`,
109
+ source: `did:pkh:eip155:${chainId}:${config.account.address}`,
102
110
  });
103
111
  },
104
112
  });
@@ -21,6 +21,19 @@ export interface CardConfig {
21
21
  brand: string;
22
22
  }
23
23
 
24
+ export interface SpendPolicy {
25
+ maxPerTxUsd?: number;
26
+ maxPerDayUsd?: number;
27
+ requireConfirmationAboveUsd?: number;
28
+ }
29
+
30
+ export interface SpendLedgerEntry {
31
+ method: string;
32
+ amountUsd: number;
33
+ day: string;
34
+ timestamp: string;
35
+ }
36
+
24
37
  export interface Config {
25
38
  apiUrl: string;
26
39
  apiKey: string | null;
@@ -35,6 +48,10 @@ export interface Config {
35
48
  confirmBeforeSpend: boolean;
36
49
  /** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
37
50
  defaultTipAmount: number;
51
+ /** Optional per-method spend policies enforced client-side by MCP tools. */
52
+ spendPolicies?: Record<string, SpendPolicy>;
53
+ /** Daily spend ledger used to enforce max-per-day limits. */
54
+ spendLedger?: SpendLedgerEntry[];
38
55
  }
39
56
 
40
57
  /** All supported chain identifiers. */
@@ -82,6 +99,8 @@ interface LegacyConfig {
82
99
  defaultWallet?: string | null;
83
100
  card?: CardConfig | null;
84
101
  pendingCardSetupToken?: string | null;
102
+ spendPolicies?: Record<string, SpendPolicy>;
103
+ spendLedger?: SpendLedgerEntry[];
85
104
  }
86
105
 
87
106
  /**
@@ -104,6 +123,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
104
123
  favorites: r.favorites as string[] ?? [],
105
124
  confirmBeforeSpend: r.confirmBeforeSpend !== false,
106
125
  defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
126
+ spendPolicies: r.spendPolicies as Record<string, SpendPolicy> | undefined,
127
+ spendLedger: r.spendLedger as SpendLedgerEntry[] | undefined,
107
128
  };
108
129
  }
109
130
 
@@ -178,6 +199,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
178
199
  favorites: [],
179
200
  confirmBeforeSpend: true,
180
201
  defaultTipAmount: 0,
202
+ spendPolicies: {},
203
+ spendLedger: [],
181
204
  };
182
205
 
183
206
  // Write migrated config (only if there was something to migrate)
@@ -203,6 +226,8 @@ export function getConfig(): Config {
203
226
  favorites: [],
204
227
  confirmBeforeSpend: true,
205
228
  defaultTipAmount: 0,
229
+ spendPolicies: {},
230
+ spendLedger: [],
206
231
  };
207
232
 
208
233
  if (!existsSync(CONFIG_FILE)) {
@@ -249,6 +274,26 @@ export function getDefaultTipAmount(): number {
249
274
  return getConfig().defaultTipAmount;
250
275
  }
251
276
 
277
+ export function getSpendPolicy(method: string): SpendPolicy | null {
278
+ const policies = getConfig().spendPolicies ?? {};
279
+ return policies[method] ?? null;
280
+ }
281
+
282
+ export function setSpendPolicy(method: string, policy: SpendPolicy): void {
283
+ const config = getConfig();
284
+ const policies = config.spendPolicies ?? {};
285
+ policies[method] = policy;
286
+ saveConfig({ spendPolicies: policies });
287
+ }
288
+
289
+ export function getSpendLedger(): SpendLedgerEntry[] {
290
+ return getConfig().spendLedger ?? [];
291
+ }
292
+
293
+ export function saveSpendLedger(entries: SpendLedgerEntry[]): void {
294
+ saveConfig({ spendLedger: entries });
295
+ }
296
+
252
297
  // ── Wallet helpers ─────────────────────────────────────────────────
253
298
 
254
299
  /**