@agentwonderland/mcp 0.1.24 → 0.1.25

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 (49) 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__/payments.test.js +55 -6
  4. package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
  5. package/dist/core/__tests__/spend-policy.test.js +40 -0
  6. package/dist/core/amount-utils.d.ts +1 -0
  7. package/dist/core/amount-utils.js +4 -0
  8. package/dist/core/base-charge.js +3 -2
  9. package/dist/core/config.d.ts +19 -0
  10. package/dist/core/config.js +22 -0
  11. package/dist/core/formatters.d.ts +2 -3
  12. package/dist/core/formatters.js +5 -7
  13. package/dist/core/payments.js +14 -4
  14. package/dist/core/solana-charge.js +2 -1
  15. package/dist/core/spend-policy.d.ts +12 -0
  16. package/dist/core/spend-policy.js +53 -0
  17. package/dist/core/types.d.ts +1 -2
  18. package/dist/index.js +5 -2
  19. package/dist/prompts/index.js +4 -2
  20. package/dist/resources/agents.js +1 -1
  21. package/dist/tools/agent-info.js +2 -2
  22. package/dist/tools/favorites.js +1 -1
  23. package/dist/tools/observability.d.ts +2 -0
  24. package/dist/tools/observability.js +20 -0
  25. package/dist/tools/run.js +40 -28
  26. package/dist/tools/solve.js +45 -39
  27. package/dist/tools/wallet.js +26 -10
  28. package/package.json +1 -1
  29. package/src/core/__tests__/amount-utils.test.ts +13 -0
  30. package/src/core/__tests__/payments.test.ts +68 -6
  31. package/src/core/__tests__/spend-policy.test.ts +58 -0
  32. package/src/core/amount-utils.ts +5 -0
  33. package/src/core/base-charge.ts +3 -2
  34. package/src/core/config.ts +45 -0
  35. package/src/core/formatters.ts +6 -8
  36. package/src/core/payments.ts +17 -4
  37. package/src/core/solana-charge.ts +2 -1
  38. package/src/core/spend-policy.ts +69 -0
  39. package/src/core/types.ts +1 -2
  40. package/src/index.ts +5 -2
  41. package/src/prompts/index.ts +4 -2
  42. package/src/resources/agents.ts +1 -1
  43. package/src/tools/agent-info.ts +2 -2
  44. package/src/tools/favorites.ts +1 -4
  45. package/src/tools/observability.ts +43 -0
  46. package/src/tools/passes.ts +1 -7
  47. package/src/tools/run.ts +44 -43
  48. package/src/tools/solve.ts +50 -55
  49. package/src/tools/wallet.ts +30 -10
@@ -36,11 +36,10 @@ export function isFileOutput(output: unknown): output is { type: "file"; url: st
36
36
 
37
37
  // ── Price formatting ─────────────────────────────────────────────
38
38
 
39
- export function formatPrice(pricePer1kTokens?: string | null, pricingModel?: string | null): string {
40
- if (!pricePer1kTokens) return "free";
41
- const p = parseFloat(pricePer1kTokens);
42
- if (pricingModel === "fixed") return `$${p.toFixed(2)}/req`;
43
- return `$${p.toFixed(3)}/1k tokens`;
39
+ export function formatPrice(pricePerRunUsd?: string | null): string {
40
+ if (!pricePerRunUsd) return "free";
41
+ const p = parseFloat(pricePerRunUsd);
42
+ return `$${p.toFixed(2)}/req`;
44
43
  }
45
44
 
46
45
  // ── Agent line (compact, one line per agent) ─────────────────────
@@ -52,8 +51,7 @@ interface AgentLike {
52
51
  avgRating?: number | null;
53
52
  ratingCount?: number;
54
53
  totalExecutions?: number;
55
- pricePer1kTokens?: string;
56
- pricingModel?: string;
54
+ pricePerRunUsd?: string;
57
55
  stats?: { completedJobs?: number; avgRating?: number | null };
58
56
  [key: string]: unknown;
59
57
  }
@@ -63,7 +61,7 @@ export function agentLine(agent: AgentLike): string {
63
61
  const slug = agent.slug ? ` (${agent.slug})` : "";
64
62
  const rating = agent.avgRating ?? agent.stats?.avgRating ?? null;
65
63
  const jobs = agent.stats?.completedJobs ?? agent.totalExecutions ?? 0;
66
- const price = formatPrice(agent.pricePer1kTokens, agent.pricingModel);
64
+ const price = formatPrice(agent.pricePerRunUsd);
67
65
  const reliability = agent.successRate != null && Number(agent.successRate) < 1
68
66
  ? ` • ${(Number(agent.successRate) * 100).toFixed(0)}% reliable`
69
67
  : "";
@@ -63,7 +63,10 @@ function clearStaleCardCache(activeKey?: string): void {
63
63
 
64
64
  // ── Per-protocol initializers ───────────────────────────────────
65
65
 
66
- async function initMpp(wallet: WalletEntry): Promise<typeof fetch | null> {
66
+ async function initEvmMppForChain(
67
+ wallet: WalletEntry,
68
+ chain: "tempo" | "base",
69
+ ): Promise<typeof fetch | null> {
67
70
  try {
68
71
  const { Mppx, tempo } = await import("mppx/client");
69
72
  let account;
@@ -78,8 +81,15 @@ async function initMpp(wallet: WalletEntry): Promise<typeof fetch | null> {
78
81
  return null;
79
82
  }
80
83
 
81
- const { baseChargeClient } = await import("./base-charge.js");
82
- const mppx = Mppx.create({ methods: [tempo({ account }), baseChargeClient({ account })] as any });
84
+ const methods = [];
85
+ if (chain === "tempo") {
86
+ methods.push(tempo({ account }));
87
+ } else {
88
+ const { baseChargeClient } = await import("./base-charge.js");
89
+ methods.push(baseChargeClient({ account }));
90
+ }
91
+
92
+ const mppx = Mppx.create({ methods: methods as any });
83
93
  return mppx.fetch.bind(mppx) as typeof fetch;
84
94
  } catch {
85
95
  return null;
@@ -150,7 +160,10 @@ async function initForChain(wallet: WalletEntry, chain: string): Promise<typeof
150
160
  if (chain === "solana") {
151
161
  return initSolanaMpp(wallet);
152
162
  }
153
- return initMpp(wallet);
163
+ if (chain === "tempo" || chain === "base") {
164
+ return initEvmMppForChain(wallet, chain);
165
+ }
166
+ return null;
154
167
  }
155
168
 
156
169
  // ── Public API ──────────────────────────────────────────────────
@@ -13,6 +13,7 @@ import {
13
13
  getAssociatedTokenAddressSync,
14
14
  } from "@solana/spl-token";
15
15
  import type { WalletEntry } from "./config.js";
16
+ import { toAtomicAmount } from "./amount-utils.js";
16
17
 
17
18
  export const SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as const;
18
19
  export const SOLANA_CHAIN_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as const;
@@ -98,7 +99,7 @@ export function solanaChargeClient(config: SolanaChargeClientConfig) {
98
99
  return Method.toClient(solanaChargeMethod as any, {
99
100
  async createCredential({ challenge }: any) {
100
101
  const { request } = challenge;
101
- const amount = BigInt(request.amount);
102
+ const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
102
103
  const decimals = request.decimals ?? 6;
103
104
  const mint = new PublicKey(request.currency ?? SOLANA_USDC_MINT);
104
105
  const recipient = new PublicKey(request.recipient);
@@ -0,0 +1,69 @@
1
+ import {
2
+ getSpendLedger,
3
+ getSpendPolicy,
4
+ saveSpendLedger,
5
+ type SpendLedgerEntry,
6
+ } from "./config.js";
7
+
8
+ const LEDGER_RETENTION_DAYS = 35;
9
+
10
+ function utcDay(now: Date): string {
11
+ return now.toISOString().slice(0, 10);
12
+ }
13
+
14
+ function pruneLedger(entries: SpendLedgerEntry[], now: Date): SpendLedgerEntry[] {
15
+ const cutoff = new Date(now);
16
+ cutoff.setUTCDate(cutoff.getUTCDate() - LEDGER_RETENTION_DAYS);
17
+ return entries.filter((entry) => new Date(entry.timestamp) >= cutoff);
18
+ }
19
+
20
+ export function getDailySpend(method: string, now = new Date()): number {
21
+ const day = utcDay(now);
22
+ return getSpendLedger()
23
+ .filter((entry) => entry.method === method && entry.day === day)
24
+ .reduce((sum, entry) => sum + entry.amountUsd, 0);
25
+ }
26
+
27
+ export function canSpend(params: {
28
+ method: string;
29
+ amountUsd: number;
30
+ }): { ok: true } | { ok: false; message: string } {
31
+ const policy = getSpendPolicy(params.method);
32
+ if (!policy) return { ok: true };
33
+
34
+ if (policy.maxPerTxUsd != null && params.amountUsd > policy.maxPerTxUsd) {
35
+ return {
36
+ ok: false,
37
+ message: `Transaction blocked by spend policy: $${params.amountUsd.toFixed(2)} exceeds max_per_tx of $${policy.maxPerTxUsd.toFixed(2)} for "${params.method}".`,
38
+ };
39
+ }
40
+
41
+ if (policy.maxPerDayUsd != null) {
42
+ const spentToday = getDailySpend(params.method);
43
+ if (spentToday + params.amountUsd > policy.maxPerDayUsd) {
44
+ return {
45
+ ok: false,
46
+ message: `Transaction blocked by spend policy: daily spend would be $${(spentToday + params.amountUsd).toFixed(2)} > $${policy.maxPerDayUsd.toFixed(2)} for "${params.method}".`,
47
+ };
48
+ }
49
+ }
50
+
51
+ return { ok: true };
52
+ }
53
+
54
+ export function requiresPolicyConfirmation(method: string, amountUsd: number): boolean {
55
+ const policy = getSpendPolicy(method);
56
+ if (!policy?.requireConfirmationAboveUsd) return false;
57
+ return amountUsd >= policy.requireConfirmationAboveUsd;
58
+ }
59
+
60
+ export function recordSpend(method: string, amountUsd: number, now = new Date()): void {
61
+ const entries = pruneLedger(getSpendLedger(), now);
62
+ entries.push({
63
+ method,
64
+ amountUsd,
65
+ day: utcDay(now),
66
+ timestamp: now.toISOString(),
67
+ });
68
+ saveSpendLedger(entries);
69
+ }
package/src/core/types.ts CHANGED
@@ -6,8 +6,7 @@ export interface AgentRecord {
6
6
  id: string;
7
7
  name: string;
8
8
  description?: string;
9
- pricePer1kTokens?: string;
10
- pricingModel?: string;
9
+ pricePerRunUsd?: string;
11
10
  avgRating?: number | null;
12
11
  totalExecutions?: number;
13
12
  successRate?: number | string | null;
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { registerWalletTools } from "./tools/wallet.js";
14
14
  import { registerFavoriteTools } from "./tools/favorites.js";
15
15
  import { registerTipTools } from "./tools/tip.js";
16
16
  import { registerPassTools } from "./tools/passes.js";
17
+ import { registerObservabilityTools } from "./tools/observability.js";
17
18
 
18
19
  // ── Resources ────────────────────────────────────────────────────
19
20
  import { registerAgentResources } from "./resources/agents.js";
@@ -50,8 +51,9 @@ export async function startMcpServer(): Promise<void> {
50
51
  "- Crypto wallets (Tempo USDC, Base USDC, Solana USDC) are available for advanced users.",
51
52
  "- Card and crypto are SEPARATE payment methods. Card charges a credit card; crypto sends USDC on-chain.",
52
53
  "- Card is set as the default payment method when configured.",
53
- "- If multiple valid payment methods are available for a run and pay_with is not specified, ask the user which one to use.",
54
- "- For fully agentic execution, include pay_with explicitly.",
54
+ "- run_agent() and solve() require pay_with explicitly.",
55
+ "- Use wallet_status to see the exact payment methods the user has configured before calling a paid tool.",
56
+ "- Use open_observability_dashboard() to open a secure web usage dashboard for runs/spend/rebates.",
55
57
  "- Payment is automatic once configured. Users are never charged for failed runs.",
56
58
  "- Do NOT ask the user to set up payment before they try to run an agent. Let them explore freely.",
57
59
  "- If a specific payment method fails, report the error clearly. Do NOT silently fall back to a different method.",
@@ -79,6 +81,7 @@ export async function startMcpServer(): Promise<void> {
79
81
  registerFavoriteTools(server);
80
82
  registerTipTools(server);
81
83
  registerPassTools(server);
84
+ registerObservabilityTools(server);
82
85
 
83
86
  // Register resources
84
87
  registerAgentResources(server);
@@ -18,7 +18,7 @@ export function registerPrompts(server: McpServer) {
18
18
  "1. Check my wallet status with wallet_status",
19
19
  "2. If no wallet is configured, help me create one with wallet_setup",
20
20
  "3. Show me some popular agents with search_agents",
21
- "4. Briefly explain how solve, run_agent, buy_agent_credit_pack, rating, and tipping work",
21
+ "4. Briefly explain how solve, run_agent, buy_agent_credit_pack, pay_with selection, rating, and tipping work",
22
22
  ].join("\n"),
23
23
  },
24
24
  }],
@@ -86,7 +86,7 @@ export function registerPrompts(server: McpServer) {
86
86
  text: [
87
87
  `Complete this task on Agent Wonderland within $${budget || "1.00"}: "${task}"`,
88
88
  "",
89
- "Use the solve tool with the budget parameter. Show me what agent was selected and why.",
89
+ "Use the solve tool with the budget parameter and an explicit pay_with method. Show me what agent was selected and why.",
90
90
  ].join("\n"),
91
91
  },
92
92
  }],
@@ -108,6 +108,8 @@ export function registerPrompts(server: McpServer) {
108
108
  text: [
109
109
  `Run agent ${agent_id} with this input: ${input}`,
110
110
  "",
111
+ "Include an explicit pay_with method when you call run_agent.",
112
+ "",
111
113
  "After getting the result:",
112
114
  "1. Summarize the output",
113
115
  "2. Assess the quality",
@@ -9,7 +9,7 @@ export function registerAgentResources(server: McpServer) {
9
9
  const lines = agents.map(a => {
10
10
  const rating = stars(a.reputationScore);
11
11
  const jobs = compactNumber(a.totalExecutions);
12
- const price = formatPrice(a.pricePer1kTokens);
12
+ const price = formatPrice(a.pricePerRunUsd);
13
13
  return `${a.name} ${rating} ${jobs} jobs | ${price}/req — ${a.description || ""}`;
14
14
  });
15
15
  return {
@@ -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}`);
@@ -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
+ }
@@ -155,7 +155,6 @@ export function registerPassTools(server: McpServer): void {
155
155
  }
156
156
 
157
157
  const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
158
-
159
158
  if (requiresSpendConfirmation() && !confirmed) {
160
159
  pendingCreditPackPurchases.set(agent.id, {
161
160
  agentId: agent.id,
@@ -182,12 +181,7 @@ export function registerPassTools(server: McpServer): void {
182
181
  consumer_principal: string;
183
182
  offer: CreditPackOffer;
184
183
  credit_pack: CreditPackRecord;
185
- }>(
186
- `/agents/${agent.id}/credit-packs/purchase`,
187
- { pack_id: resolvedOffer.pack_id },
188
- method,
189
- { ensureConsumerPrincipal: true },
190
- );
184
+ }>(`/agents/${agent.id}/credit-packs/purchase`, { pack_id: resolvedOffer.pack_id }, method, { ensureConsumerPrincipal: true });
191
185
 
192
186
  pendingCreditPackPurchases.delete(agent.id);
193
187
 
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,
@@ -100,7 +100,7 @@ export function registerRunTools(server: McpServer): void {
100
100
  {
101
101
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
102
102
  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."),
103
+ pay_with: z.string().trim().min(1).describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
104
104
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
105
105
  },
106
106
  async ({ agent_id, input, pay_with, confirmed }) => {
@@ -124,24 +124,23 @@ export function registerRunTools(server: McpServer): void {
124
124
  return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
125
125
  }
126
126
 
127
- const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
127
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
128
128
  const agentName = agent.name ?? agent_id;
129
129
  const creditPackInventory = await getCreditPackInventory(agent.id);
130
130
  const activeCreditPack = getActiveCreditPack(creditPackInventory);
131
- const configuredMethods = getConfiguredMethods();
132
- const compatibleMethods = getCompatiblePaymentMethods(agent, configuredMethods);
131
+ const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
133
132
  const pending = pendingRuns.get(agent.id);
134
- const requestedMethod = pay_with ?? pending?.method;
135
- const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
133
+ const requestedMethod = pay_with;
134
+ const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
136
135
 
137
- if (!activeCreditPack && requestedMethod && !normalizedRequestedMethod) {
136
+ if (!normalizedRequestedMethod) {
138
137
  return text(
139
138
  `Payment method "${requestedMethod}" is not configured.\n\n` +
140
139
  "Use wallet_status to review your current payment methods.",
141
140
  );
142
141
  }
143
142
 
144
- if (!activeCreditPack && normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
143
+ if (!compatibleMethods.includes(normalizedRequestedMethod)) {
145
144
  return text(
146
145
  `This agent cannot be paid with "${requestedMethod}".\n\n` +
147
146
  `Available payment methods for this agent: ${compatibleMethods.join(", ") || "none"}.\n` +
@@ -149,30 +148,21 @@ export function registerRunTools(server: McpServer): void {
149
148
  );
150
149
  }
151
150
 
152
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length === 0) {
153
- return text(
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.",
158
- );
159
- }
151
+ const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
152
+ const spendCheckMethod = method ?? normalizedRequestedMethod;
160
153
 
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
- );
154
+ if (!activeCreditPack) {
155
+ const spendCheck = canSpend({
156
+ method: spendCheckMethod,
157
+ amountUsd: price,
158
+ });
159
+ if (!spendCheck.ok) {
160
+ return text(spendCheck.message);
161
+ }
169
162
  }
170
163
 
171
- const method = activeCreditPack
172
- ? undefined
173
- : resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
174
-
175
- if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
164
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, price);
165
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
176
166
  pendingRuns.set(agent.id, {
177
167
  agent: { id: agent.id, name: agentName, price },
178
168
  input,
@@ -218,26 +208,37 @@ export function registerRunTools(server: McpServer): void {
218
208
  }
219
209
 
220
210
  let result: Record<string, unknown>;
211
+ let usedPaidMethod = !activeCreditPack;
221
212
  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>>(
213
+ if (activeCreditPack) {
214
+ try {
215
+ result = await apiPost<Record<string, unknown>>(
216
+ `/agents/${agent.id}/run`,
217
+ { input: processedInput },
218
+ { ensureConsumerPrincipal: true },
219
+ );
220
+ } catch (packErr) {
221
+ const packApiErr = packErr as { status?: number };
222
+ if (packApiErr?.status !== 402) throw packErr;
223
+ result = await apiPostWithPayment<Record<string, unknown>>(
224
+ `/agents/${agent.id}/run`,
225
+ { input: processedInput },
226
+ method,
227
+ );
228
+ usedPaidMethod = true;
229
+ }
230
+ } else {
231
+ result = await apiPostWithPayment<Record<string, unknown>>(
229
232
  `/agents/${agent.id}/run`,
230
233
  { input: processedInput },
231
234
  method,
232
235
  );
236
+ }
237
+ if (usedPaidMethod) {
238
+ recordSpend(spendCheckMethod, price);
239
+ }
233
240
  } catch (err: unknown) {
234
241
  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
242
  if (apiErr?.status === 402) {
242
243
  const allMethods = getConfiguredMethods();
243
244
  const methodName = method ?? allMethods[0] ?? "auto";
@@ -13,11 +13,11 @@ import {
13
13
  } from "../core/payments.js";
14
14
  import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
15
15
  import { agentList, formatRunResult } from "../core/formatters.js";
16
+ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
16
17
  import { uploadLocalFiles } from "../core/file-upload.js";
17
18
  import type { AgentRecord } from "../core/types.js";
18
19
  import { storeFeedbackToken } from "./_token-cache.js";
19
20
  import {
20
- formatPaymentChoicePrompt,
21
21
  formatPaymentLabel,
22
22
  formatSolveConfirmationCommand,
23
23
  makeSolvePendingKey,
@@ -137,8 +137,9 @@ export function registerSolveTools(server: McpServer): void {
137
137
  .describe("Maximum budget in USD"),
138
138
  pay_with: z
139
139
  .string()
140
- .optional()
141
- .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
140
+ .trim()
141
+ .min(1)
142
+ .describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
142
143
  confirmed: z
143
144
  .boolean()
144
145
  .optional()
@@ -161,10 +162,10 @@ export function registerSolveTools(server: McpServer): void {
161
162
  const pendingKey = makeSolvePendingKey(intent, input, budget);
162
163
  const pending = pendingSolves.get(pendingKey);
163
164
  const configuredMethods = getConfiguredMethods();
164
- const requestedMethod = pay_with ?? pending?.method;
165
- const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
165
+ const requestedMethod = pay_with;
166
+ const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
166
167
 
167
- if (requestedMethod && !normalizedRequestedMethod) {
168
+ if (!normalizedRequestedMethod) {
168
169
  return text(
169
170
  `Payment method "${requestedMethod}" is not configured.\n\n` +
170
171
  "Use wallet_status to review your current payment methods.",
@@ -220,17 +221,15 @@ export function registerSolveTools(server: McpServer): void {
220
221
  }
221
222
 
222
223
  const discovery = agentList(agents, intent);
223
- const inputTokens = Math.ceil(JSON.stringify(input).length / 4);
224
224
  const affordable = agents.filter((agent) => {
225
- const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
226
- const cost = agent.pricingModel === "fixed" ? price : (inputTokens / 1000) * price;
227
- return cost <= budget;
225
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
226
+ return price <= budget;
228
227
  });
229
228
  const selected = affordable[0] ?? agents[0];
230
229
  const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
231
230
  const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
232
231
 
233
- if (!activeCreditPack && normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
232
+ if (!compatibleMethods.includes(normalizedRequestedMethod)) {
234
233
  return text(
235
234
  `The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
236
235
  `Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
@@ -238,39 +237,14 @@ export function registerSolveTools(server: McpServer): void {
238
237
  );
239
238
  }
240
239
 
241
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length === 0) {
242
- return text(
243
- `No compatible payment methods are configured for ${selected.name}.\n\n` +
244
- `Your configured methods: ${configuredMethods.join(", ") || "none"}\n` +
245
- `Agent accepts: ${selected.payment?.accepted_payments?.join(", ") || "unknown"}\n` +
246
- "Use wallet_status to review your current setup.",
247
- );
248
- }
249
-
250
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length > 1) {
251
- return text([
252
- discovery,
253
- "",
254
- formatPaymentChoicePrompt(
255
- selected.name ?? selected.id,
256
- compatibleMethods,
257
- compatibleMethods.map((method) =>
258
- formatSolveConfirmationCommand(intent, budget, method).replace(", confirmed: true", ""),
259
- ),
260
- ),
261
- ].join("\n"));
262
- }
263
-
264
- const method = activeCreditPack
265
- ? undefined
266
- : resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
240
+ const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
241
+ const spendCheckMethod = method ?? normalizedRequestedMethod;
267
242
 
268
- const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
269
- const estimatedCost = selected.pricingModel === "fixed"
270
- ? selectedPrice
271
- : (inputTokens / 1000) * selectedPrice;
243
+ const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
244
+ const estimatedCost = selectedPrice;
272
245
 
273
- if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
246
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
247
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
274
248
  pendingSolves.set(pendingKey, { method });
275
249
  return text([
276
250
  discovery,
@@ -291,26 +265,47 @@ export function registerSolveTools(server: McpServer): void {
291
265
  }
292
266
 
293
267
  let result: Record<string, unknown>;
268
+ if (!activeCreditPack) {
269
+ const spendCheck = canSpend({
270
+ method: spendCheckMethod,
271
+ amountUsd: estimatedCost,
272
+ });
273
+ if (!spendCheck.ok) {
274
+ return text(spendCheck.message);
275
+ }
276
+ }
277
+
294
278
  try {
295
- result = activeCreditPack
296
- ? await apiPost<Record<string, unknown>>(
297
- `/agents/${selected.id}/run`,
298
- { input: processedInput },
299
- { ensureConsumerPrincipal: true },
300
- )
301
- : await apiPostWithPayment<Record<string, unknown>>(
279
+ let usedPaidMethod = !activeCreditPack;
280
+ if (activeCreditPack) {
281
+ try {
282
+ result = await apiPost<Record<string, unknown>>(
283
+ `/agents/${selected.id}/run`,
284
+ { input: processedInput },
285
+ { ensureConsumerPrincipal: true },
286
+ );
287
+ } catch (packErr) {
288
+ const packApiErr = packErr as { status?: number };
289
+ if (packApiErr?.status !== 402) throw packErr;
290
+ result = await apiPostWithPayment<Record<string, unknown>>(
291
+ `/agents/${selected.id}/run`,
292
+ { input: processedInput },
293
+ method,
294
+ );
295
+ usedPaidMethod = true;
296
+ }
297
+ } else {
298
+ result = await apiPostWithPayment<Record<string, unknown>>(
302
299
  `/agents/${selected.id}/run`,
303
300
  { input: processedInput },
304
301
  method,
305
302
  );
303
+ }
304
+ if (usedPaidMethod) {
305
+ recordSpend(spendCheckMethod, estimatedCost);
306
+ }
306
307
  } catch (err: unknown) {
307
308
  const apiErr = err as { status?: number; message?: string };
308
- if (activeCreditPack && apiErr?.status === 402) {
309
- return text(
310
- `Your available credit packs for ${selected.name} could not cover this run.\n\n` +
311
- "Use list_agent_credit_packs to inspect your balances, or retry with a payment method.",
312
- );
313
- }
314
309
  if (apiErr?.status === 402) {
315
310
  return text(
316
311
  [