@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,289 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ type WalletEntry = {
5
+ id: string;
6
+ keyType: "evm" | "ows";
7
+ key?: string;
8
+ owsWalletId?: string;
9
+ chains: string[];
10
+ defaultChain?: string;
11
+ label?: string;
12
+ };
13
+
14
+ type CardConfig = {
15
+ consumerToken: string;
16
+ paymentMethodId?: string;
17
+ last4: string;
18
+ brand: string;
19
+ } | null;
20
+
21
+ type WalletToolResult = {
22
+ content: Array<{ type: "text"; text: string }>;
23
+ };
24
+
25
+ const state = vi.hoisted(() => ({
26
+ wallets: [] as WalletEntry[],
27
+ addedWallets: [] as WalletEntry[],
28
+ card: null as CardConfig,
29
+ pendingCardSetupToken: null as string | null,
30
+ spendPolicies: {} as Record<string, unknown>,
31
+ consumerPrincipal: "did:pkh:eip155:8453:0xabc",
32
+ owsAvailable: true,
33
+ owsWallets: [] as Array<{ id: string; name: string; address: string }>,
34
+ owsWalletsByChain: [] as Array<{ id: string; name: string; address: string }>,
35
+ createdWalletCalls: [] as Array<{ name: string; chain: "evm" | "solana" }>,
36
+ importWalletCalls: [] as Array<{ privateKey: string; name: string; chain: "evm" | "solana" }>,
37
+ createdWalletResult: {
38
+ walletId: "ows-wallet-created",
39
+ address: "0x1111111111111111111111111111111111111111",
40
+ },
41
+ importWalletResult: {
42
+ walletId: "ows-wallet-imported",
43
+ address: "0x2222222222222222222222222222222222222222",
44
+ },
45
+ cardSetup: {
46
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
47
+ token: "setup-token",
48
+ isNew: true,
49
+ },
50
+ cardSetupBlocks: [
51
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
52
+ ],
53
+ pollCardSetupCalls: [] as Array<{ token: string; timeoutMs: number }>,
54
+ pollCardSetupResult: null as
55
+ | {
56
+ brand: string;
57
+ last4: string;
58
+ consumerToken: string;
59
+ }
60
+ | null,
61
+ setCardConfigCalls: [] as CardConfig[],
62
+ }));
63
+
64
+ vi.mock("../../core/config.js", () => ({
65
+ addWallet: (wallet: WalletEntry) => {
66
+ state.addedWallets.push(wallet);
67
+ const existing = state.wallets.findIndex((entry) => entry.id === wallet.id);
68
+ if (existing >= 0) {
69
+ state.wallets[existing] = wallet;
70
+ } else {
71
+ state.wallets.push(wallet);
72
+ }
73
+ },
74
+ getCardConfig: () => state.card,
75
+ getPendingCardSetupToken: () => state.pendingCardSetupToken,
76
+ getSpendPolicy: (walletId: string) => state.spendPolicies[walletId] ?? null,
77
+ getWallets: () => state.wallets,
78
+ setCardConfig: (card: CardConfig) => {
79
+ state.card = card;
80
+ state.setCardConfigCalls.push(card);
81
+ },
82
+ setSpendPolicy: (walletId: string, policy: unknown) => {
83
+ state.spendPolicies[walletId] = policy;
84
+ },
85
+ }));
86
+
87
+ vi.mock("../../core/payments.js", () => ({
88
+ getWalletAddress: async () => null,
89
+ }));
90
+
91
+ vi.mock("../../core/card-setup.js", () => ({
92
+ formatCardSetupBlocks: () => state.cardSetupBlocks,
93
+ getCardCapabilities: async () => ({
94
+ spt_status: "enabled",
95
+ }),
96
+ getOrCreatePendingCardSetup: async () => state.cardSetup,
97
+ pollCardSetup: async (token: string, timeoutMs: number) => {
98
+ state.pollCardSetupCalls.push({ token, timeoutMs });
99
+ return state.pollCardSetupResult;
100
+ },
101
+ }));
102
+
103
+ vi.mock("../../core/ows-adapter.js", () => ({
104
+ createOwsWallet: async (name: string, chain: "evm" | "solana") => {
105
+ state.createdWalletCalls.push({ name, chain });
106
+ return state.createdWalletResult;
107
+ },
108
+ importKeyToOws: async (privateKey: string, name: string, chain: "evm" | "solana") => {
109
+ state.importWalletCalls.push({ privateKey, name, chain });
110
+ return state.importWalletResult;
111
+ },
112
+ isOwsAvailable: async () => state.owsAvailable,
113
+ listOwsWallets: async () => state.owsWallets,
114
+ listOwsWalletsByChain: async () => state.owsWalletsByChain,
115
+ }));
116
+
117
+ vi.mock("../../core/principal.js", () => ({
118
+ ensureConsumerPrincipal: async () => state.consumerPrincipal,
119
+ getConsumerPrincipal: async () => state.consumerPrincipal,
120
+ }));
121
+
122
+ function resetState(): void {
123
+ state.wallets = [];
124
+ state.addedWallets = [];
125
+ state.card = null;
126
+ state.pendingCardSetupToken = null;
127
+ state.spendPolicies = {};
128
+ state.consumerPrincipal = "did:pkh:eip155:8453:0xabc";
129
+ state.owsAvailable = true;
130
+ state.owsWallets = [];
131
+ state.owsWalletsByChain = [];
132
+ state.createdWalletCalls = [];
133
+ state.importWalletCalls = [];
134
+ state.createdWalletResult = {
135
+ walletId: "ows-wallet-created",
136
+ address: "0x1111111111111111111111111111111111111111",
137
+ };
138
+ state.importWalletResult = {
139
+ walletId: "ows-wallet-imported",
140
+ address: "0x2222222222222222222222222222222222222222",
141
+ };
142
+ state.cardSetup = {
143
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
144
+ token: "setup-token",
145
+ isNew: true,
146
+ };
147
+ state.cardSetupBlocks = [
148
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
149
+ ];
150
+ state.pollCardSetupCalls = [];
151
+ state.pollCardSetupResult = null;
152
+ state.setCardConfigCalls = [];
153
+ }
154
+
155
+ async function getWalletSetupTool(): Promise<(args: Record<string, unknown>) => Promise<WalletToolResult>> {
156
+ const tools = new Map<string, (args: Record<string, unknown>) => Promise<WalletToolResult>>();
157
+ const server = {
158
+ tool: (
159
+ name: string,
160
+ _description: string,
161
+ _schema: unknown,
162
+ handler: (args: Record<string, unknown>) => Promise<WalletToolResult>,
163
+ ) => {
164
+ tools.set(name, handler);
165
+ },
166
+ } as unknown as McpServer;
167
+
168
+ const { registerWalletTools } = await import("../wallet.js");
169
+ registerWalletTools(server);
170
+
171
+ const walletSetup = tools.get("wallet_setup");
172
+ if (!walletSetup) {
173
+ throw new Error("wallet_setup tool was not registered");
174
+ }
175
+ return walletSetup;
176
+ }
177
+
178
+ function flattenText(result: WalletToolResult): string {
179
+ return result.content.map((item) => item.text).join("\n");
180
+ }
181
+
182
+ describe("wallet_setup tool", () => {
183
+ beforeEach(() => {
184
+ vi.resetModules();
185
+ resetState();
186
+ });
187
+
188
+ it("creates an encrypted OWS wallet for Tempo/Base with Base as the default chain", async () => {
189
+ const walletSetup = await getWalletSetupTool();
190
+
191
+ const result = await walletSetup({ action: "create", name: "launch-wallet", chain: "base" });
192
+ const text = flattenText(result);
193
+
194
+ expect(state.createdWalletCalls).toEqual([
195
+ { name: "launch-wallet", chain: "evm" },
196
+ ]);
197
+ expect(state.addedWallets).toEqual([
198
+ {
199
+ id: "launch-wallet",
200
+ keyType: "ows",
201
+ owsWalletId: "ows-wallet-created",
202
+ chains: ["tempo", "base"],
203
+ defaultChain: "base",
204
+ label: "launch-wallet",
205
+ },
206
+ ]);
207
+ expect(text).toContain("Wallet created [encrypted]:");
208
+ expect(text).toContain("Address: 0x1111111111111111111111111111111111111111");
209
+ expect(text).toContain("Chains: tempo, base");
210
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
211
+ });
212
+
213
+ it("imports a wallet into OWS encrypted storage with Base as the default chain", async () => {
214
+ const walletSetup = await getWalletSetupTool();
215
+
216
+ const result = await walletSetup({
217
+ action: "import",
218
+ key: "0x1234",
219
+ name: "imported-wallet",
220
+ chain: "base",
221
+ });
222
+ const text = flattenText(result);
223
+
224
+ expect(state.importWalletCalls).toEqual([
225
+ { privateKey: "0x1234", name: "imported-wallet", chain: "evm" },
226
+ ]);
227
+ expect(state.addedWallets).toEqual([
228
+ {
229
+ id: "imported-wallet",
230
+ keyType: "ows",
231
+ owsWalletId: "ows-wallet-imported",
232
+ chains: ["tempo", "base"],
233
+ defaultChain: "base",
234
+ label: "imported-wallet",
235
+ },
236
+ ]);
237
+ expect(text).toContain("Key imported to OWS [encrypted]:");
238
+ expect(text).toContain("Address: 0x2222222222222222222222222222222222222222");
239
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
240
+ });
241
+
242
+ it("starts card setup when no card is connected", async () => {
243
+ const walletSetup = await getWalletSetupTool();
244
+
245
+ const result = await walletSetup({ action: "add-card" });
246
+ const text = flattenText(result);
247
+
248
+ expect(text).toContain("Open this setup page to connect your card:");
249
+ expect(text).toContain("https://api.agentwonderland.com/card/handoff/setup-token");
250
+ expect(state.pollCardSetupCalls).toEqual([]);
251
+ });
252
+
253
+ it("completes a pending card setup when Stripe handoff has finished", async () => {
254
+ state.pendingCardSetupToken = "setup-token";
255
+ state.pollCardSetupResult = {
256
+ brand: "Visa",
257
+ last4: "4242",
258
+ consumerToken: "consumer-token",
259
+ };
260
+
261
+ const walletSetup = await getWalletSetupTool();
262
+ const result = await walletSetup({ action: "add-card" });
263
+ const text = flattenText(result);
264
+
265
+ expect(state.pollCardSetupCalls).toEqual([
266
+ { token: "setup-token", timeoutMs: 250 },
267
+ ]);
268
+ expect(text).toContain("Connected! Visa ****4242 is ready for payments.");
269
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
270
+ });
271
+
272
+ it("removes the connected card", async () => {
273
+ state.card = {
274
+ consumerToken: "consumer-token",
275
+ paymentMethodId: "pm_123",
276
+ last4: "4242",
277
+ brand: "Visa",
278
+ };
279
+
280
+ const walletSetup = await getWalletSetupTool();
281
+ const result = await walletSetup({ action: "remove-card" });
282
+ const text = flattenText(result);
283
+
284
+ expect(state.setCardConfigCalls).toEqual([null]);
285
+ expect(state.card).toBeNull();
286
+ expect(text).toContain("Removed Visa ****4242.");
287
+ expect(text).toContain("Card disconnected from Agent Wonderland.");
288
+ });
289
+ });
@@ -47,6 +47,6 @@ export function formatPaymentChoicePrompt(
47
47
  ...commands,
48
48
  "",
49
49
  `Available methods: ${methods.map((method) => `"${method}"`).join(", ")}`,
50
- "For fully agentic execution, include pay_with explicitly.",
50
+ "You can omit pay_with to use the default compatible method.",
51
51
  ].join("\n");
52
52
  }
@@ -37,7 +37,7 @@ export function registerAgentInfoTools(server: McpServer): void {
37
37
  "",
38
38
  (a.description as string) ?? "",
39
39
  "",
40
- `Pricing: ${formatPrice(a.pricePer1kTokens, a.pricingModel)}`,
40
+ `Pricing: ${formatPrice(a.pricePerRunUsd)}`,
41
41
  `Reliability: ${a.successRate != null ? (Number(a.successRate) * 100).toFixed(0) + "%" : "N/A"}`,
42
42
  `Avg latency: ${(a.avgResponseTimeMs as number) != null ? a.avgResponseTimeMs + "ms" : "N/A"}`,
43
43
  ...(() => {
@@ -130,7 +130,7 @@ export function registerAgentInfoTools(server: McpServer): void {
130
130
  return [
131
131
  ` ${a.name}`,
132
132
  ` ${stars(rating)} (${s.ratingCount ?? 0} reviews)${tipCount > 0 ? ` • ${tipCount} tips` : ""}`,
133
- ` ${compactNumber(jobs)} jobs • ${formatPrice(a.pricePer1kTokens, a.pricingModel)}`,
133
+ ` ${compactNumber(jobs)} jobs • ${formatPrice(a.pricePerRunUsd)}`,
134
134
  ` Success: ${a.successRate != null ? (Number(a.successRate) * 100).toFixed(0) + "%" : "N/A"}`,
135
135
  ` ${agentWebUrl(a.id)}`,
136
136
  "",
@@ -54,10 +54,7 @@ export function registerFavoriteTools(server: McpServer) {
54
54
  const agent = await apiGet<Record<string, unknown>>(`/agents/${id}`);
55
55
  const rating = stars(agent.avgRating as number | null | undefined);
56
56
  const jobs = compactNumber(agent.totalExecutions as number | null | undefined);
57
- const price = formatPrice(
58
- agent.pricePer1kTokens as string | null | undefined,
59
- agent.pricingModel as string | null | undefined,
60
- );
57
+ const price = formatPrice(agent.pricePerRunUsd as string | null | undefined);
61
58
  lines.push(`${agent.name} ${rating} ${jobs} jobs | ${price}`);
62
59
  lines.push(` ID: ${id}`);
63
60
  if (agent.description) lines.push(` ${agent.description}`);
package/src/tools/jobs.ts CHANGED
@@ -18,7 +18,16 @@ export function registerJobTools(server: McpServer): void {
18
18
  job_id: z.string().describe("Job ID (UUID)"),
19
19
  },
20
20
  async ({ job_id }) => {
21
- const result = await apiGet<Record<string, unknown>>(`/jobs/${job_id}`);
21
+ let url = `/jobs/${job_id}`;
22
+
23
+ if (!isAuthenticated() && hasWalletConfigured()) {
24
+ const address = await getWalletAddress();
25
+ if (address) {
26
+ url += `?wallet=${encodeURIComponent(address)}`;
27
+ }
28
+ }
29
+
30
+ const result = await apiGet<Record<string, unknown>>(url);
22
31
  if (result.status === "processing") {
23
32
  return text(`Job ${job_id} is still processing...`);
24
33
  }
@@ -0,0 +1,43 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { apiPost } from "../core/api-client.js";
3
+
4
+ function text(t: string) {
5
+ return { content: [{ type: "text" as const, text: t }] };
6
+ }
7
+
8
+ export function registerObservabilityTools(server: McpServer): void {
9
+ server.tool(
10
+ "open_observability_dashboard",
11
+ "Generate a secure one-click sign-in URL for the Agent Wonderland web observability dashboard. The dashboard shows your agent runs, spend, rebates, and recent activity.",
12
+ {},
13
+ async () => {
14
+ const result = await apiPost<{
15
+ url: string;
16
+ expires_at: string;
17
+ consumer_principal?: string;
18
+ }>(
19
+ "/observability/link",
20
+ {},
21
+ { ensureConsumerPrincipal: true },
22
+ );
23
+
24
+ const lines = [
25
+ "Your secure observability link is ready:",
26
+ result.url,
27
+ "",
28
+ `Expires: ${result.expires_at}`,
29
+ ];
30
+
31
+ if (result.consumer_principal) {
32
+ lines.push(`Consumer principal: ${result.consumer_principal}`);
33
+ }
34
+
35
+ lines.push(
36
+ "",
37
+ "Open the link in your browser to view usage metrics, spend, rebates, and recent runs.",
38
+ );
39
+
40
+ return text(lines.join("\n"));
41
+ },
42
+ );
43
+ }
@@ -4,6 +4,7 @@ import { apiGet, apiPostWithPayment } from "../core/api-client.js";
4
4
  import {
5
5
  formatCreditPack,
6
6
  formatCreditPackOffer,
7
+ getCreditPackInventory,
7
8
  getCreditPackProgram,
8
9
  } from "../core/passes.js";
9
10
  import {
@@ -13,7 +14,7 @@ import {
13
14
  normalizePaymentMethod,
14
15
  } from "../core/payments.js";
15
16
  import { requiresSpendConfirmation } from "../core/config.js";
16
- import { ensureConsumerPrincipal } from "../core/principal.js";
17
+ import { ensureConsumerPrincipalForMethod } from "../core/principal.js";
17
18
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
18
19
  import {
19
20
  formatPaymentChoicePrompt,
@@ -84,7 +85,6 @@ export function registerPassTools(server: McpServer): void {
84
85
 
85
86
  const agent = await getAgent(agent_id);
86
87
  const agentName = agent.name ?? agent_id;
87
- const principal = await ensureConsumerPrincipal();
88
88
  const program = getCreditPackProgram(agent);
89
89
  const offers = (program?.packs ?? [])
90
90
  .map((pack) => findOffer(agent, pack.key ?? ""))
@@ -155,7 +155,7 @@ export function registerPassTools(server: McpServer): void {
155
155
  }
156
156
 
157
157
  const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
158
-
158
+ const principal = await ensureConsumerPrincipalForMethod(method);
159
159
  if (requiresSpendConfirmation() && !confirmed) {
160
160
  pendingCreditPackPurchases.set(agent.id, {
161
161
  agentId: agent.id,
@@ -182,12 +182,7 @@ export function registerPassTools(server: McpServer): void {
182
182
  consumer_principal: string;
183
183
  offer: CreditPackOffer;
184
184
  credit_pack: CreditPackRecord;
185
- }>(
186
- `/agents/${agent.id}/credit-packs/purchase`,
187
- { pack_id: resolvedOffer.pack_id },
188
- method,
189
- { ensureConsumerPrincipal: true },
190
- );
185
+ }>(`/agents/${agent.id}/credit-packs/purchase`, { pack_id: resolvedOffer.pack_id }, method, { ensureConsumerPrincipal: true });
191
186
 
192
187
  pendingCreditPackPurchases.delete(agent.id);
193
188
 
@@ -198,7 +193,8 @@ export function registerPassTools(server: McpServer): void {
198
193
  formatCreditPack(result.credit_pack),
199
194
  `Consumer principal: ${result.consumer_principal}`,
200
195
  "",
201
- "Future runs through run_agent will automatically use this credit pack while units remain.",
196
+ "Future runs for this agent will automatically use this credit pack while units remain.",
197
+ "That includes run_agent, and solve whenever it selects this same agent.",
202
198
  ].join("\n"));
203
199
  },
204
200
  );
@@ -208,10 +204,14 @@ export function registerPassTools(server: McpServer): void {
208
204
  "Show discounted credit-pack offers for an agent plus any balances available under the current consumer principal.",
209
205
  {
210
206
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
207
+ pay_with: z.string().optional().describe("Optional payment method context used to inspect the matching consumer principal."),
211
208
  },
212
- async ({ agent_id }) => {
209
+ async ({ agent_id, pay_with }) => {
213
210
  const agent = await getAgent(agent_id);
214
- const result = await apiGet<CreditPackInventory>(`/agents/${agent.id}/credit-packs`, { ensureConsumerPrincipal: true });
211
+ const result = await getCreditPackInventory(agent.id, pay_with);
212
+ if (!result) {
213
+ return text(`Could not load credit-pack inventory for ${agent.name}.`);
214
+ }
215
215
 
216
216
  const lines = [
217
217
  `Credit packs for ${agent.name}`,
package/src/tools/run.ts CHANGED
@@ -17,10 +17,10 @@ import {
17
17
  } from "../core/payments.js";
18
18
  import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
19
19
  import { formatRunResult } from "../core/formatters.js";
20
+ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
20
21
  import { storeFeedbackToken } from "./_token-cache.js";
21
22
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
22
23
  import {
23
- formatPaymentChoicePrompt,
24
24
  formatPaymentLabel,
25
25
  formatRunConfirmationCommand,
26
26
  resolveConfirmationMethod,
@@ -32,9 +32,10 @@ const POLL_MAX_MS = 120000;
32
32
 
33
33
  async function pollJobUntilDone(
34
34
  jobId: string,
35
+ paymentMethod?: string,
35
36
  ): Promise<{ status: string; output?: unknown; error_code?: string }> {
36
37
  const deadline = Date.now() + POLL_MAX_MS;
37
- const walletAddress = await getWalletAddress();
38
+ const walletAddress = await getWalletAddress(paymentMethod);
38
39
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
39
40
 
40
41
  while (Date.now() < deadline) {
@@ -100,7 +101,7 @@ export function registerRunTools(server: McpServer): void {
100
101
  {
101
102
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
102
103
  input: z.record(z.unknown()).describe("Input payload for the agent"),
103
- pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
104
+ pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
104
105
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
105
106
  },
106
107
  async ({ agent_id, input, pay_with, confirmed }) => {
@@ -124,24 +125,23 @@ export function registerRunTools(server: McpServer): void {
124
125
  return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
125
126
  }
126
127
 
127
- const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
128
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
128
129
  const agentName = agent.name ?? agent_id;
129
- const creditPackInventory = await getCreditPackInventory(agent.id);
130
- const activeCreditPack = getActiveCreditPack(creditPackInventory);
131
- const configuredMethods = getConfiguredMethods();
132
- const compatibleMethods = getCompatiblePaymentMethods(agent, configuredMethods);
130
+ const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
133
131
  const pending = pendingRuns.get(agent.id);
134
132
  const requestedMethod = pay_with ?? pending?.method;
135
133
  const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
134
+ const creditPackInventory = await getCreditPackInventory(agent.id, requestedMethod);
135
+ const activeCreditPack = getActiveCreditPack(creditPackInventory);
136
136
 
137
- if (!activeCreditPack && requestedMethod && !normalizedRequestedMethod) {
137
+ if (requestedMethod && !normalizedRequestedMethod) {
138
138
  return text(
139
139
  `Payment method "${requestedMethod}" is not configured.\n\n` +
140
140
  "Use wallet_status to review your current payment methods.",
141
141
  );
142
142
  }
143
143
 
144
- if (!activeCreditPack && normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
144
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
145
145
  return text(
146
146
  `This agent cannot be paid with "${requestedMethod}".\n\n` +
147
147
  `Available payment methods for this agent: ${compatibleMethods.join(", ") || "none"}.\n` +
@@ -149,30 +149,28 @@ export function registerRunTools(server: McpServer): void {
149
149
  );
150
150
  }
151
151
 
152
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length === 0) {
152
+ if (!activeCreditPack && compatibleMethods.length === 0) {
153
153
  return text(
154
154
  `No compatible payment methods are configured for ${agentName}.\n\n` +
155
- `Your configured methods: ${configuredMethods.join(", ") || "none"}\n` +
156
- `Agent accepts: ${agent.payment?.accepted_payments?.join(", ") || "unknown"}\n` +
157
- "Use wallet_status to review your current setup.",
155
+ "Use wallet_status to review your current payment methods.",
158
156
  );
159
157
  }
160
158
 
161
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length > 1) {
162
- return text(
163
- formatPaymentChoicePrompt(
164
- agentName,
165
- compatibleMethods,
166
- compatibleMethods.map((method) => formatRunConfirmationCommand(agent.id, method).replace(", confirmed: true", "")),
167
- ),
168
- );
169
- }
159
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
160
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
170
161
 
171
- const method = activeCreditPack
172
- ? undefined
173
- : resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
162
+ if (!activeCreditPack) {
163
+ const spendCheck = canSpend({
164
+ method: spendCheckMethod,
165
+ amountUsd: price,
166
+ });
167
+ if (!spendCheck.ok) {
168
+ return text(spendCheck.message);
169
+ }
170
+ }
174
171
 
175
- if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
172
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, price);
173
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
176
174
  pendingRuns.set(agent.id, {
177
175
  agent: { id: agent.id, name: agentName, price },
178
176
  input,
@@ -218,26 +216,37 @@ export function registerRunTools(server: McpServer): void {
218
216
  }
219
217
 
220
218
  let result: Record<string, unknown>;
219
+ let usedPaidMethod = !activeCreditPack;
221
220
  try {
222
- result = activeCreditPack
223
- ? await apiPost<Record<string, unknown>>(
224
- `/agents/${agent.id}/run`,
225
- { input: processedInput },
226
- { ensureConsumerPrincipal: true },
227
- )
228
- : await apiPostWithPayment<Record<string, unknown>>(
221
+ if (activeCreditPack) {
222
+ try {
223
+ result = await apiPost<Record<string, unknown>>(
224
+ `/agents/${agent.id}/run`,
225
+ { input: processedInput },
226
+ { ensureConsumerPrincipal: true },
227
+ );
228
+ } catch (packErr) {
229
+ const packApiErr = packErr as { status?: number };
230
+ if (packApiErr?.status !== 402) throw packErr;
231
+ result = await apiPostWithPayment<Record<string, unknown>>(
232
+ `/agents/${agent.id}/run`,
233
+ { input: processedInput },
234
+ method,
235
+ );
236
+ usedPaidMethod = true;
237
+ }
238
+ } else {
239
+ result = await apiPostWithPayment<Record<string, unknown>>(
229
240
  `/agents/${agent.id}/run`,
230
241
  { input: processedInput },
231
242
  method,
232
243
  );
244
+ }
245
+ if (usedPaidMethod) {
246
+ recordSpend(spendCheckMethod, price);
247
+ }
233
248
  } catch (err: unknown) {
234
249
  const apiErr = err as { status?: number; message?: string };
235
- if (activeCreditPack && apiErr?.status === 402) {
236
- return text(
237
- `Your available credit packs for ${agentName} could not cover this run.\n\n` +
238
- "Use list_agent_credit_packs to inspect your balances, or retry with a payment method.",
239
- );
240
- }
241
250
  if (apiErr?.status === 402) {
242
251
  const allMethods = getConfiguredMethods();
243
252
  const methodName = method ?? allMethods[0] ?? "auto";
@@ -277,7 +286,7 @@ export function registerRunTools(server: McpServer): void {
277
286
  const usedCreditPack = result.consumption_mode === "credit_pack";
278
287
 
279
288
  if (status === "processing") {
280
- const pollResult = await pollJobUntilDone(jobId);
289
+ const pollResult = await pollJobUntilDone(jobId, method);
281
290
  if (pollResult.status === "completed") {
282
291
  const asyncFormatted = formatRunResult({
283
292
  ...result,