@agentwonderland/mcp 0.1.25 → 0.1.27

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 (78) hide show
  1. package/dist/core/__tests__/api-client.test.d.ts +1 -0
  2. package/dist/core/__tests__/api-client.test.js +51 -0
  3. package/dist/core/__tests__/formatters.test.js +10 -0
  4. package/dist/core/__tests__/passes-api.test.d.ts +1 -0
  5. package/dist/core/__tests__/passes-api.test.js +27 -0
  6. package/dist/core/__tests__/payments.test.js +10 -6
  7. package/dist/core/__tests__/principal.test.js +41 -4
  8. package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
  9. package/dist/core/__tests__/solana-charge.test.js +50 -0
  10. package/dist/core/api-client.d.ts +1 -0
  11. package/dist/core/api-client.js +8 -3
  12. package/dist/core/balances.d.ts +1 -0
  13. package/dist/core/balances.js +56 -0
  14. package/dist/core/base-charge.js +13 -6
  15. package/dist/core/formatters.d.ts +3 -2
  16. package/dist/core/formatters.js +7 -1
  17. package/dist/core/passes.d.ts +1 -1
  18. package/dist/core/passes.js +5 -2
  19. package/dist/core/payments.d.ts +1 -0
  20. package/dist/core/payments.js +20 -7
  21. package/dist/core/principal.d.ts +3 -0
  22. package/dist/core/principal.js +29 -1
  23. package/dist/core/settings.d.ts +20 -0
  24. package/dist/core/settings.js +19 -0
  25. package/dist/core/solana-charge.d.ts +5 -0
  26. package/dist/core/solana-charge.js +29 -7
  27. package/dist/core/tempo-charge.d.ts +7 -0
  28. package/dist/core/tempo-charge.js +84 -0
  29. package/dist/index.js +5 -7
  30. package/dist/prompts/index.js +1 -1
  31. package/dist/tools/__tests__/jobs.test.d.ts +1 -0
  32. package/dist/tools/__tests__/jobs.test.js +71 -0
  33. package/dist/tools/__tests__/run.test.d.ts +1 -0
  34. package/dist/tools/__tests__/run.test.js +149 -0
  35. package/dist/tools/__tests__/solve.test.d.ts +1 -0
  36. package/dist/tools/__tests__/solve.test.js +158 -0
  37. package/dist/tools/__tests__/wallet.test.d.ts +1 -0
  38. package/dist/tools/__tests__/wallet.test.js +230 -0
  39. package/dist/tools/_payment-confirmation.js +1 -1
  40. package/dist/tools/agent-info.js +14 -28
  41. package/dist/tools/jobs.js +82 -16
  42. package/dist/tools/passes.js +30 -14
  43. package/dist/tools/run.js +35 -20
  44. package/dist/tools/search.js +9 -8
  45. package/dist/tools/solve.js +45 -25
  46. package/dist/tools/wallet.js +35 -15
  47. package/package.json +2 -2
  48. package/src/core/__tests__/api-client.test.ts +78 -0
  49. package/src/core/__tests__/formatters.test.ts +12 -0
  50. package/src/core/__tests__/passes-api.test.ts +33 -0
  51. package/src/core/__tests__/payments.test.ts +17 -6
  52. package/src/core/__tests__/principal.test.ts +49 -4
  53. package/src/core/__tests__/solana-charge.test.ts +59 -0
  54. package/src/core/api-client.ts +16 -3
  55. package/src/core/balances.ts +63 -0
  56. package/src/core/base-charge.ts +13 -6
  57. package/src/core/formatters.ts +10 -3
  58. package/src/core/passes.ts +5 -2
  59. package/src/core/payments.ts +22 -7
  60. package/src/core/principal.ts +42 -1
  61. package/src/core/settings.ts +36 -0
  62. package/src/core/solana-charge.ts +43 -9
  63. package/src/core/tempo-charge.ts +104 -0
  64. package/src/index.ts +5 -7
  65. package/src/prompts/index.ts +1 -1
  66. package/src/tools/__tests__/jobs.test.ts +89 -0
  67. package/src/tools/__tests__/run.test.ts +176 -0
  68. package/src/tools/__tests__/solve.test.ts +186 -0
  69. package/src/tools/__tests__/wallet.test.ts +289 -0
  70. package/src/tools/_payment-confirmation.ts +1 -1
  71. package/src/tools/agent-info.ts +15 -38
  72. package/src/tools/jobs.ts +79 -17
  73. package/src/tools/passes.ts +30 -14
  74. package/src/tools/run.ts +38 -20
  75. package/src/tools/search.ts +10 -9
  76. package/src/tools/solve.ts +48 -25
  77. package/src/tools/wallet.ts +33 -17
  78. package/src/tools/observability.ts +0 -43
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
3
3
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
4
- import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
4
+ import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, isCardPaymentEnabled, } from "../core/payments.js";
5
5
  import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
6
6
  import { agentList, formatRunResult } from "../core/formatters.js";
7
7
  import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
@@ -11,9 +11,9 @@ import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey
11
11
  import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
12
12
  const POLL_INTERVAL_MS = 3000;
13
13
  const POLL_MAX_MS = 120000;
14
- async function pollSolveJob(jobId) {
14
+ async function pollSolveJob(jobId, paymentMethod) {
15
15
  const deadline = Date.now() + POLL_MAX_MS;
16
- const walletAddress = await getWalletAddress();
16
+ const walletAddress = await getWalletAddress(paymentMethod);
17
17
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
18
18
  while (Date.now() < deadline) {
19
19
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -106,29 +106,41 @@ export function registerSolveTools(server) {
106
106
  .string()
107
107
  .trim()
108
108
  .min(1)
109
- .describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
109
+ .optional()
110
+ .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
110
111
  confirmed: z
111
112
  .boolean()
112
113
  .optional()
113
114
  .describe("Set to true to confirm spending after seeing the price quote."),
114
115
  }, async ({ intent, input, budget, pay_with, confirmed }) => {
115
116
  if (!hasWalletConfigured()) {
116
- try {
117
- const { url } = await getOrCreatePendingCardSetup();
118
- return multiText(...formatCardSetupBlocks(url));
117
+ if (isCardPaymentEnabled()) {
118
+ try {
119
+ const { url } = await getOrCreatePendingCardSetup();
120
+ return multiText(...formatCardSetupBlocks(url));
121
+ }
122
+ catch {
123
+ // Fall through to the setup message below.
124
+ }
119
125
  }
120
- catch {
121
- return text("No payment method configured.\n\n" +
122
- "To add a credit card: wallet_setup({ action: \"add-card\" })\n" +
123
- "To use crypto: wallet_setup({ action: \"create\" })");
126
+ const setupLines = [
127
+ "No payment method configured.",
128
+ "",
129
+ "Supported rails: Tempo USDC, Base USDC, Solana USDC.",
130
+ "Run wallet_setup({ action: \"create\" }) to create a crypto wallet,",
131
+ "or wallet_setup({ action: \"import\" }) with an existing key.",
132
+ ];
133
+ if (isCardPaymentEnabled()) {
134
+ setupLines.push("", "Or wallet_setup({ action: \"add-card\" }) to connect a credit card.");
124
135
  }
136
+ return text(setupLines.join("\n"));
125
137
  }
126
138
  const pendingKey = makeSolvePendingKey(intent, input, budget);
127
139
  const pending = pendingSolves.get(pendingKey);
128
140
  const configuredMethods = getConfiguredMethods();
129
- const requestedMethod = pay_with;
130
- const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
131
- if (!normalizedRequestedMethod) {
141
+ const requestedMethod = pay_with ?? pending?.method;
142
+ const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
143
+ if (requestedMethod && !normalizedRequestedMethod) {
132
144
  return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
133
145
  "Use wallet_status to review your current payment methods.");
134
146
  }
@@ -156,13 +168,16 @@ export function registerSolveTools(server) {
156
168
  const cost = result.cost;
157
169
  const usedCreditPack = result.consumption_mode === "credit_pack";
158
170
  const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
159
- return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
171
+ const header = agentName ? [`Matched agent: ${agentName}`, ""].join("\n") : "";
172
+ return multiText(header + formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
160
173
  }
161
174
  catch (err) {
162
- const isAuthError = err instanceof Error &&
163
- "status" in err &&
164
- err.status === 401;
165
- if (!isAuthError || !hasWalletConfigured())
175
+ const status = err instanceof Error &&
176
+ "status" in err
177
+ ? err.status
178
+ : undefined;
179
+ const isRecoverableDirectSolveError = status === 401 || status === 402;
180
+ if (!isRecoverableDirectSolveError || !hasWalletConfigured())
166
181
  throw err;
167
182
  }
168
183
  const params = new URLSearchParams({ q: intent, limit: "5" });
@@ -182,15 +197,19 @@ export function registerSolveTools(server) {
182
197
  return price <= budget;
183
198
  });
184
199
  const selected = affordable[0] ?? agents[0];
185
- const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
200
+ const activeCreditPack = await getCreditPackInventory(selected.id, requestedMethod).then(getActiveCreditPack);
186
201
  const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
187
- if (!compatibleMethods.includes(normalizedRequestedMethod)) {
202
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
188
203
  return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
189
204
  `Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
190
205
  "Choose another payment method or refine your search.");
191
206
  }
192
- const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
193
- const spendCheckMethod = method ?? normalizedRequestedMethod;
207
+ if (!activeCreditPack && compatibleMethods.length === 0) {
208
+ return text(`No compatible payment methods are configured for ${selected.name}.\n\n` +
209
+ "Use wallet_status to review your current payment methods.");
210
+ }
211
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
212
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
194
213
  const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
195
214
  const estimatedCost = selectedPrice;
196
215
  const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
@@ -247,8 +266,9 @@ export function registerSolveTools(server) {
247
266
  catch (err) {
248
267
  const apiErr = err;
249
268
  if (apiErr?.status === 402) {
269
+ const reason = apiErr.message ? `Payment failed: ${apiErr.message}` : "Payment failed — wallet may not have enough funds or the selected method was rejected.";
250
270
  return text([
251
- "Payment failed — your wallet may not have enough funds or the selected method was rejected.",
271
+ reason,
252
272
  "",
253
273
  "Check your balance and try again.",
254
274
  "Use wallet_status to check your current payment methods.",
@@ -268,7 +288,7 @@ export function registerSolveTools(server) {
268
288
  storeFeedbackToken(jobId, result.feedback_token, agentId);
269
289
  }
270
290
  if (status === "processing") {
271
- const pollResult = await pollSolveJob(jobId);
291
+ const pollResult = await pollSolveJob(jobId, method);
272
292
  if (pollResult.status === "completed") {
273
293
  const asyncFormatted = formatRunResult({
274
294
  ...result,
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
3
- import { getWalletAddress } from "../core/payments.js";
3
+ import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
4
+ import { fetchUsdcBalance } from "../core/balances.js";
5
+ import { getSettings } from "../core/settings.js";
4
6
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
5
7
  import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
6
8
  import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
@@ -17,18 +19,33 @@ export function registerWalletTools(server) {
17
19
  if (wallets.length === 0 && !card && !pendingCardSetupToken) {
18
20
  return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
19
21
  }
20
- const lines = ["Payment methods:"];
22
+ const settings = await getSettings();
23
+ const networkLabel = settings ? ` (${settings.network})` : "";
24
+ const lines = [`Payment methods${networkLabel}:`];
21
25
  for (const w of wallets) {
22
26
  const label = w.label ? ` (${w.label})` : "";
23
27
  const storage = w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
24
- const chainAddresses = await Promise.all(w.chains.map(async (chainName) => {
28
+ const chainLines = await Promise.all(w.chains.map(async (chainName) => {
25
29
  const addr = await getWalletAddress(chainName);
26
- return `${chainName}: ${addr ?? "unknown"}`;
30
+ if (!addr)
31
+ return `${chainName}: unknown`;
32
+ let balanceStr = "";
33
+ if (chainName === "tempo" || chainName === "base" || chainName === "solana") {
34
+ const balance = await fetchUsdcBalance(chainName, addr);
35
+ if (balance !== null) {
36
+ const num = Number(balance);
37
+ balanceStr = ` ${Number.isFinite(num) ? num.toFixed(4).replace(/\.?0+$/, "") : balance} USDC`;
38
+ }
39
+ }
40
+ return `${chainName}: ${addr}${balanceStr}`;
27
41
  }));
28
- lines.push(` ${w.id}${label}${storage}: ${chainAddresses.join(" | ")}`);
42
+ lines.push(` ${w.id}${label}${storage}:`);
43
+ for (const line of chainLines)
44
+ lines.push(` ${line}`);
29
45
  }
30
- if (card) {
31
- lines.push(` Card: ${card.brand} ****${card.last4}`);
46
+ if (card && isCardPaymentEnabled()) {
47
+ const stripeMode = settings?.stripe.mode === "test" ? " [Stripe test mode]" : "";
48
+ lines.push(` Card: ${card.brand} ****${card.last4}${stripeMode}`);
32
49
  const capabilities = await getCardCapabilities();
33
50
  if (capabilities.spt_status === "enabled") {
34
51
  lines.push(" Card MPP: ready");
@@ -48,11 +65,11 @@ export function registerWalletTools(server) {
48
65
  }
49
66
  return text(lines.join("\n"));
50
67
  });
51
- // ── wallet_setup (NEW) ──────────────────────────────────────────
52
- server.tool("wallet_setup", "Set up or manage payment methods. Options: 'add-card' to connect a credit/debit card (recommended), 'remove-card' to disconnect a card, 'create' a crypto wallet, or 'import' an existing key. For crypto wallet changes (removal, key rotation), direct users to edit their config files manually never handle private keys programmatically.", {
68
+ // ── wallet_setup ────────────────────────────────────────────────
69
+ server.tool("wallet_setup", "Set up or manage a payment wallet. 'create' makes a new encrypted crypto wallet (OWS); 'import' takes an existing private key. Tempo/Base share one EVM key — a single wallet covers both. Solana uses a separate ed25519 key. For crypto wallet deletion or key rotation, direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually; never handle key material programmatically.", {
53
70
  action: z
54
71
  .enum(["create", "import", "add-card", "remove-card"])
55
- .describe("'add-card' to connect a card, 'remove-card' to disconnect it, 'create' a crypto wallet, or 'import' an existing key"),
72
+ .describe("'create' a crypto wallet (recommended), 'import' an existing key, 'add-card'/'remove-card' for credit card (card-backed MPP availability depends on Stripe SPT)"),
56
73
  name: z
57
74
  .string()
58
75
  .optional()
@@ -62,10 +79,13 @@ export function registerWalletTools(server) {
62
79
  .optional()
63
80
  .describe("Private key hex string (required for 'import', ignored for 'create')"),
64
81
  chain: z.enum(["tempo", "base", "solana"]).optional()
65
- .describe("Primary chain (default: tempo). Solana uses an OWS wallet plus Stripe deposit-mode USDC."),
82
+ .describe("Primary chain (default: tempo). Tempo/Base use a shared EVM wallet; Solana uses a separate OWS wallet."),
66
83
  }, async ({ action, name, key, chain }) => {
67
84
  // ── Card setup flow ──────────────────────────────────────
68
85
  if (action === "add-card") {
86
+ if (!isCardPaymentEnabled()) {
87
+ return text("Card payments are temporarily unavailable. Use wallet_setup({ action: \"create\" }) for a crypto wallet instead.");
88
+ }
69
89
  const existing = getCardConfig();
70
90
  if (existing) {
71
91
  return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
@@ -257,8 +277,8 @@ export function registerWalletTools(server) {
257
277
  }
258
278
  });
259
279
  // ── wallet_set_policy (NEW) ─────────────────────────────────────
260
- server.tool("wallet_set_policy", "Set spending limits on a wallet to control agent costs. Limits reset daily.", {
261
- wallet_id: z.string().describe("Wallet ID to set policy on"),
280
+ server.tool("wallet_set_policy", "Set client-side spending limits on a wallet to control agent costs in this MCP client. Limits reset daily.", {
281
+ wallet_id: z.string().describe("Wallet ID to set local policy on"),
262
282
  max_per_tx: z
263
283
  .number()
264
284
  .positive()
@@ -306,10 +326,10 @@ export function registerWalletTools(server) {
306
326
  policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
307
327
  }
308
328
  return text([
309
- `Spending policy set for wallet "${wallet_id}":`,
329
+ `Local spending policy set for wallet "${wallet_id}":`,
310
330
  ...policies.map((p) => ` ${p}`),
311
331
  "",
312
- "Policy will be enforced on all future transactions from this wallet.",
332
+ "Policy is stored in this MCP client's local config and enforced on future transactions from this wallet on this client.",
313
333
  ].join("\n"));
314
334
  });
315
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
26
  "@solana/spl-token": "^0.4.14",
27
27
  "@solana/web3.js": "^1.98.4",
28
- "mppx": "^0.4.9",
28
+ "mppx": "^0.5.10",
29
29
  "qrcode": "^1.5.4",
30
30
  "viem": "^2.47.6",
31
31
  "zod": "^3.24.0"
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ mockGetApiUrl,
5
+ mockGetApiKey,
6
+ mockGetPaymentFetch,
7
+ mockEnsureConsumerPrincipalForMethod,
8
+ mockGetConsumerPrincipalForMethod,
9
+ mockGetBaseRebatePrincipal,
10
+ mockPaymentFetch,
11
+ } = vi.hoisted(() => ({
12
+ mockGetApiUrl: vi.fn(),
13
+ mockGetApiKey: vi.fn(),
14
+ mockGetPaymentFetch: vi.fn(),
15
+ mockEnsureConsumerPrincipalForMethod: vi.fn(),
16
+ mockGetConsumerPrincipalForMethod: vi.fn(),
17
+ mockGetBaseRebatePrincipal: vi.fn(),
18
+ mockPaymentFetch: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("../config.js", () => ({
22
+ getApiUrl: () => mockGetApiUrl(),
23
+ getApiKey: () => mockGetApiKey(),
24
+ }));
25
+
26
+ vi.mock("../payments.js", () => ({
27
+ getPaymentFetch: (...args: unknown[]) => mockGetPaymentFetch(...args),
28
+ }));
29
+
30
+ vi.mock("../principal.js", () => ({
31
+ ensureConsumerPrincipal: vi.fn(),
32
+ ensureConsumerPrincipalForMethod: (...args: unknown[]) => mockEnsureConsumerPrincipalForMethod(...args),
33
+ getConsumerPrincipal: vi.fn(),
34
+ getConsumerPrincipalForMethod: (...args: unknown[]) => mockGetConsumerPrincipalForMethod(...args),
35
+ getBaseRebatePrincipal: (...args: unknown[]) => mockGetBaseRebatePrincipal(...args),
36
+ }));
37
+
38
+ describe("api-client headers", () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ mockGetApiUrl.mockReturnValue("https://api.agentwonderland.test");
42
+ mockGetApiKey.mockReturnValue(null);
43
+ mockGetPaymentFetch.mockResolvedValue(mockPaymentFetch);
44
+ mockEnsureConsumerPrincipalForMethod.mockResolvedValue(
45
+ "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
46
+ );
47
+ mockGetConsumerPrincipalForMethod.mockResolvedValue(
48
+ "did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
49
+ );
50
+ mockGetBaseRebatePrincipal.mockResolvedValue(
51
+ "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
52
+ );
53
+ mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
54
+ status: 200,
55
+ headers: { "content-type": "application/json" },
56
+ }));
57
+ });
58
+
59
+ it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
60
+ const { apiPostWithPayment } = await import("../api-client.js");
61
+
62
+ await apiPostWithPayment("/agents/agent-1/run", { input: { text: "hello" } }, "solana");
63
+
64
+ expect(mockPaymentFetch).toHaveBeenCalledWith(
65
+ "https://api.agentwonderland.test/agents/agent-1/run",
66
+ expect.objectContaining({
67
+ headers: expect.objectContaining({
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json",
70
+ "X-AW-Consumer-Principal":
71
+ "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
72
+ "X-AW-Rebate-Principal":
73
+ "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
74
+ }),
75
+ }),
76
+ );
77
+ });
78
+ });
@@ -14,4 +14,16 @@ describe("formatRunResult", () => {
14
14
  expect(output).toContain("Covered by credit pack (pack-123)");
15
15
  expect(output).not.toContain("Paid:");
16
16
  });
17
+
18
+ it("formats string-valued settled amounts from job lookups", () => {
19
+ const output = formatRunResult({
20
+ agent_name: "Async Analyzer",
21
+ status: "completed",
22
+ settled_amount: "0.100000",
23
+ job_id: "job-123",
24
+ }, { paymentMethod: "card" });
25
+
26
+ expect(output).toContain("Paid: $0.10 via card");
27
+ expect(output).toContain("Job ID: job-123");
28
+ });
17
29
  });
@@ -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
+ });
@@ -16,7 +16,7 @@ let currentResolvedMethod: { wallet: any; chain: string } | null = null;
16
16
  const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
17
17
  const mockMppxCreate = vi.fn();
18
18
  const mockStripe = vi.fn((opts: unknown) => opts);
19
- const mockTempo = vi.fn((..._args: unknown[]) => "tempo_method");
19
+ const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
20
20
  const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
21
21
 
22
22
  vi.mock("../config.js", () => ({
@@ -33,7 +33,10 @@ vi.mock("mppx/client", () => ({
33
33
  create: (config: unknown) => mockMppxCreate(config),
34
34
  },
35
35
  stripe: (config: unknown) => mockStripe(config),
36
- tempo: (config: unknown) => mockTempo(config),
36
+ }));
37
+
38
+ vi.mock("../tempo-charge.js", () => ({
39
+ tempoChargeClient: (config: unknown) => mockTempoChargeClient(config),
37
40
  }));
38
41
 
39
42
  vi.mock("../base-charge.js", () => ({
@@ -74,6 +77,14 @@ describe("payment method initialization", () => {
74
77
 
75
78
  expect(firstFetch).not.toBe(secondFetch);
76
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
+ );
77
88
  });
78
89
 
79
90
  it("initializes only the Base method when base is requested", async () => {
@@ -92,9 +103,9 @@ describe("payment method initialization", () => {
92
103
  await getPaymentFetch("base");
93
104
 
94
105
  expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
95
- expect(mockTempo).not.toHaveBeenCalled();
106
+ expect(mockTempoChargeClient).not.toHaveBeenCalled();
96
107
  expect(mockMppxCreate).toHaveBeenCalledWith(
97
- expect.objectContaining({ methods: ["base_method"] }),
108
+ expect.objectContaining({ methods: ["base_method"], polyfill: false }),
98
109
  );
99
110
  });
100
111
 
@@ -113,10 +124,10 @@ describe("payment method initialization", () => {
113
124
  const { getPaymentFetch } = await import("../payments.js");
114
125
  await getPaymentFetch("tempo");
115
126
 
116
- expect(mockTempo).toHaveBeenCalledTimes(1);
127
+ expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
117
128
  expect(mockBaseChargeClient).not.toHaveBeenCalled();
118
129
  expect(mockMppxCreate).toHaveBeenCalledWith(
119
- expect.objectContaining({ methods: ["tempo_method"] }),
130
+ expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
120
131
  );
121
132
  });
122
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
+ });
@@ -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),