@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.
- package/dist/core/__tests__/amount-utils.test.d.ts +1 -0
- package/dist/core/__tests__/amount-utils.test.js +11 -0
- package/dist/core/__tests__/payments.test.js +55 -6
- package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
- package/dist/core/__tests__/spend-policy.test.js +40 -0
- package/dist/core/amount-utils.d.ts +1 -0
- package/dist/core/amount-utils.js +4 -0
- package/dist/core/base-charge.js +3 -2
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.js +22 -0
- package/dist/core/formatters.d.ts +2 -3
- package/dist/core/formatters.js +5 -7
- package/dist/core/payments.js +14 -4
- package/dist/core/solana-charge.js +2 -1
- package/dist/core/spend-policy.d.ts +12 -0
- package/dist/core/spend-policy.js +53 -0
- package/dist/core/types.d.ts +1 -2
- package/dist/index.js +5 -2
- package/dist/prompts/index.js +4 -2
- package/dist/resources/agents.js +1 -1
- package/dist/tools/agent-info.js +2 -2
- package/dist/tools/favorites.js +1 -1
- package/dist/tools/observability.d.ts +2 -0
- package/dist/tools/observability.js +20 -0
- package/dist/tools/run.js +40 -28
- package/dist/tools/solve.js +45 -39
- package/dist/tools/wallet.js +26 -10
- package/package.json +1 -1
- package/src/core/__tests__/amount-utils.test.ts +13 -0
- package/src/core/__tests__/payments.test.ts +68 -6
- package/src/core/__tests__/spend-policy.test.ts +58 -0
- package/src/core/amount-utils.ts +5 -0
- package/src/core/base-charge.ts +3 -2
- package/src/core/config.ts +45 -0
- package/src/core/formatters.ts +6 -8
- package/src/core/payments.ts +17 -4
- package/src/core/solana-charge.ts +2 -1
- package/src/core/spend-policy.ts +69 -0
- package/src/core/types.ts +1 -2
- package/src/index.ts +5 -2
- package/src/prompts/index.ts +4 -2
- package/src/resources/agents.ts +1 -1
- package/src/tools/agent-info.ts +2 -2
- package/src/tools/favorites.ts +1 -4
- package/src/tools/observability.ts +43 -0
- package/src/tools/passes.ts +1 -7
- package/src/tools/run.ts +44 -43
- package/src/tools/solve.ts +50 -55
- package/src/tools/wallet.ts +30 -10
package/dist/tools/run.js
CHANGED
|
@@ -5,9 +5,10 @@ import { formatCreditPackOffer, getActiveCreditPack, getCreditPackInventory, get
|
|
|
5
5
|
import { getCompatiblePaymentMethods, getConfiguredMethods, hasWalletConfigured, getWalletAddress, normalizePaymentMethod, } from "../core/payments.js";
|
|
6
6
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
7
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
8
|
+
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
8
9
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
9
10
|
import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
|
|
10
|
-
import {
|
|
11
|
+
import { formatPaymentLabel, formatRunConfirmationCommand, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
11
12
|
const POLL_INTERVAL_MS = 3000;
|
|
12
13
|
const POLL_MAX_MS = 120000;
|
|
13
14
|
async function pollJobUntilDone(jobId) {
|
|
@@ -60,7 +61,7 @@ export function registerRunTools(server) {
|
|
|
60
61
|
server.tool("run_agent", "Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking. If spending confirmation is enabled, first call returns a price quote — call again with confirmed: true to execute. Local file paths in the input (e.g. /Users/.../photo.jpg) are automatically uploaded to temporary storage and replaced with download URLs before execution.", {
|
|
61
62
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
62
63
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
63
|
-
pay_with: z.string().
|
|
64
|
+
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."),
|
|
64
65
|
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
65
66
|
}, async ({ agent_id, input, pay_with, confirmed }) => {
|
|
66
67
|
if (!hasWalletConfigured()) {
|
|
@@ -81,37 +82,36 @@ export function registerRunTools(server) {
|
|
|
81
82
|
catch {
|
|
82
83
|
return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
|
|
83
84
|
}
|
|
84
|
-
const price = parseFloat(agent.
|
|
85
|
+
const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
|
|
85
86
|
const agentName = agent.name ?? agent_id;
|
|
86
87
|
const creditPackInventory = await getCreditPackInventory(agent.id);
|
|
87
88
|
const activeCreditPack = getActiveCreditPack(creditPackInventory);
|
|
88
|
-
const
|
|
89
|
-
const compatibleMethods = getCompatiblePaymentMethods(agent, configuredMethods);
|
|
89
|
+
const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
|
|
90
90
|
const pending = pendingRuns.get(agent.id);
|
|
91
|
-
const requestedMethod = pay_with
|
|
92
|
-
const normalizedRequestedMethod =
|
|
93
|
-
if (!
|
|
91
|
+
const requestedMethod = pay_with;
|
|
92
|
+
const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
|
|
93
|
+
if (!normalizedRequestedMethod) {
|
|
94
94
|
return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
|
|
95
95
|
"Use wallet_status to review your current payment methods.");
|
|
96
96
|
}
|
|
97
|
-
if (!
|
|
97
|
+
if (!compatibleMethods.includes(normalizedRequestedMethod)) {
|
|
98
98
|
return text(`This agent cannot be paid with "${requestedMethod}".\n\n` +
|
|
99
99
|
`Available payment methods for this agent: ${compatibleMethods.join(", ") || "none"}.\n` +
|
|
100
100
|
"Use get_agent to inspect the agent details or choose another payment method.");
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
103
|
+
const spendCheckMethod = method ?? normalizedRequestedMethod;
|
|
104
|
+
if (!activeCreditPack) {
|
|
105
|
+
const spendCheck = canSpend({
|
|
106
|
+
method: spendCheckMethod,
|
|
107
|
+
amountUsd: price,
|
|
108
|
+
});
|
|
109
|
+
if (!spendCheck.ok) {
|
|
110
|
+
return text(spendCheck.message);
|
|
111
|
+
}
|
|
110
112
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
: resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
114
|
-
if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
|
|
113
|
+
const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, price);
|
|
114
|
+
if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
|
|
115
115
|
pendingRuns.set(agent.id, {
|
|
116
116
|
agent: { id: agent.id, name: agentName, price },
|
|
117
117
|
input,
|
|
@@ -146,17 +146,29 @@ export function registerRunTools(server) {
|
|
|
146
146
|
return text(`Error: ${msg}`);
|
|
147
147
|
}
|
|
148
148
|
let result;
|
|
149
|
+
let usedPaidMethod = !activeCreditPack;
|
|
149
150
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
if (activeCreditPack) {
|
|
152
|
+
try {
|
|
153
|
+
result = await apiPost(`/agents/${agent.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
|
|
154
|
+
}
|
|
155
|
+
catch (packErr) {
|
|
156
|
+
const packApiErr = packErr;
|
|
157
|
+
if (packApiErr?.status !== 402)
|
|
158
|
+
throw packErr;
|
|
159
|
+
result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
|
|
160
|
+
usedPaidMethod = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
|
|
165
|
+
}
|
|
166
|
+
if (usedPaidMethod) {
|
|
167
|
+
recordSpend(spendCheckMethod, price);
|
|
168
|
+
}
|
|
153
169
|
}
|
|
154
170
|
catch (err) {
|
|
155
171
|
const apiErr = err;
|
|
156
|
-
if (activeCreditPack && apiErr?.status === 402) {
|
|
157
|
-
return text(`Your available credit packs for ${agentName} could not cover this run.\n\n` +
|
|
158
|
-
"Use list_agent_credit_packs to inspect your balances, or retry with a payment method.");
|
|
159
|
-
}
|
|
160
172
|
if (apiErr?.status === 402) {
|
|
161
173
|
const allMethods = getConfiguredMethods();
|
|
162
174
|
const methodName = method ?? allMethods[0] ?? "auto";
|
package/dist/tools/solve.js
CHANGED
|
@@ -4,9 +4,10 @@ import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card
|
|
|
4
4
|
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
|
|
5
5
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
6
6
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
7
|
+
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
7
8
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
8
9
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
9
|
-
import {
|
|
10
|
+
import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
10
11
|
import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
|
|
11
12
|
const POLL_INTERVAL_MS = 3000;
|
|
12
13
|
const POLL_MAX_MS = 120000;
|
|
@@ -103,8 +104,9 @@ export function registerSolveTools(server) {
|
|
|
103
104
|
.describe("Maximum budget in USD"),
|
|
104
105
|
pay_with: z
|
|
105
106
|
.string()
|
|
106
|
-
.
|
|
107
|
-
.
|
|
107
|
+
.trim()
|
|
108
|
+
.min(1)
|
|
109
|
+
.describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
|
|
108
110
|
confirmed: z
|
|
109
111
|
.boolean()
|
|
110
112
|
.optional()
|
|
@@ -124,9 +126,9 @@ export function registerSolveTools(server) {
|
|
|
124
126
|
const pendingKey = makeSolvePendingKey(intent, input, budget);
|
|
125
127
|
const pending = pendingSolves.get(pendingKey);
|
|
126
128
|
const configuredMethods = getConfiguredMethods();
|
|
127
|
-
const requestedMethod = pay_with
|
|
128
|
-
const normalizedRequestedMethod =
|
|
129
|
-
if (
|
|
129
|
+
const requestedMethod = pay_with;
|
|
130
|
+
const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
|
|
131
|
+
if (!normalizedRequestedMethod) {
|
|
130
132
|
return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
|
|
131
133
|
"Use wallet_status to review your current payment methods.");
|
|
132
134
|
}
|
|
@@ -175,41 +177,24 @@ export function registerSolveTools(server) {
|
|
|
175
177
|
return text(`No agents found matching "${intent}".`);
|
|
176
178
|
}
|
|
177
179
|
const discovery = agentList(agents, intent);
|
|
178
|
-
const inputTokens = Math.ceil(JSON.stringify(input).length / 4);
|
|
179
180
|
const affordable = agents.filter((agent) => {
|
|
180
|
-
const price = parseFloat(agent.
|
|
181
|
-
|
|
182
|
-
return cost <= budget;
|
|
181
|
+
const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
|
|
182
|
+
return price <= budget;
|
|
183
183
|
});
|
|
184
184
|
const selected = affordable[0] ?? agents[0];
|
|
185
185
|
const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
|
|
186
186
|
const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
|
|
187
|
-
if (!
|
|
187
|
+
if (!compatibleMethods.includes(normalizedRequestedMethod)) {
|
|
188
188
|
return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
|
|
189
189
|
`Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
|
|
190
190
|
"Choose another payment method or refine your search.");
|
|
191
191
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!activeCreditPack && !requestedMethod && compatibleMethods.length > 1) {
|
|
199
|
-
return text([
|
|
200
|
-
discovery,
|
|
201
|
-
"",
|
|
202
|
-
formatPaymentChoicePrompt(selected.name ?? selected.id, compatibleMethods, compatibleMethods.map((method) => formatSolveConfirmationCommand(intent, budget, method).replace(", confirmed: true", ""))),
|
|
203
|
-
].join("\n"));
|
|
204
|
-
}
|
|
205
|
-
const method = activeCreditPack
|
|
206
|
-
? undefined
|
|
207
|
-
: resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
208
|
-
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
209
|
-
const estimatedCost = selected.pricingModel === "fixed"
|
|
210
|
-
? selectedPrice
|
|
211
|
-
: (inputTokens / 1000) * selectedPrice;
|
|
212
|
-
if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
|
|
192
|
+
const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
193
|
+
const spendCheckMethod = method ?? normalizedRequestedMethod;
|
|
194
|
+
const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
|
|
195
|
+
const estimatedCost = selectedPrice;
|
|
196
|
+
const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
|
|
197
|
+
if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
|
|
213
198
|
pendingSolves.set(pendingKey, { method });
|
|
214
199
|
return text([
|
|
215
200
|
discovery,
|
|
@@ -229,17 +214,38 @@ export function registerSolveTools(server) {
|
|
|
229
214
|
].join("\n"));
|
|
230
215
|
}
|
|
231
216
|
let result;
|
|
217
|
+
if (!activeCreditPack) {
|
|
218
|
+
const spendCheck = canSpend({
|
|
219
|
+
method: spendCheckMethod,
|
|
220
|
+
amountUsd: estimatedCost,
|
|
221
|
+
});
|
|
222
|
+
if (!spendCheck.ok) {
|
|
223
|
+
return text(spendCheck.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
232
226
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
let usedPaidMethod = !activeCreditPack;
|
|
228
|
+
if (activeCreditPack) {
|
|
229
|
+
try {
|
|
230
|
+
result = await apiPost(`/agents/${selected.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
|
|
231
|
+
}
|
|
232
|
+
catch (packErr) {
|
|
233
|
+
const packApiErr = packErr;
|
|
234
|
+
if (packApiErr?.status !== 402)
|
|
235
|
+
throw packErr;
|
|
236
|
+
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
|
|
237
|
+
usedPaidMethod = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
|
|
242
|
+
}
|
|
243
|
+
if (usedPaidMethod) {
|
|
244
|
+
recordSpend(spendCheckMethod, estimatedCost);
|
|
245
|
+
}
|
|
236
246
|
}
|
|
237
247
|
catch (err) {
|
|
238
248
|
const apiErr = err;
|
|
239
|
-
if (activeCreditPack && apiErr?.status === 402) {
|
|
240
|
-
return text(`Your available credit packs for ${selected.name} could not cover this run.\n\n` +
|
|
241
|
-
"Use list_agent_credit_packs to inspect your balances, or retry with a payment method.");
|
|
242
|
-
}
|
|
243
249
|
if (apiErr?.status === 402) {
|
|
244
250
|
return text([
|
|
245
251
|
"Payment failed — your wallet may not have enough funds or the selected method was rejected.",
|
package/dist/tools/wallet.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, } from "../core/config.js";
|
|
2
|
+
import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
|
|
3
3
|
import { getWalletAddress } from "../core/payments.js";
|
|
4
4
|
import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
|
|
5
5
|
import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
|
|
@@ -269,26 +269,42 @@ export function registerWalletTools(server) {
|
|
|
269
269
|
.positive()
|
|
270
270
|
.optional()
|
|
271
271
|
.describe("Maximum USD per day across all transactions"),
|
|
272
|
-
|
|
272
|
+
require_confirmation_above: z
|
|
273
|
+
.number()
|
|
274
|
+
.positive()
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("Require manual confirmation above this USD amount even when auto-spend is enabled"),
|
|
277
|
+
}, async ({ wallet_id, max_per_tx, max_per_day, require_confirmation_above }) => {
|
|
273
278
|
const wallets = getWallets();
|
|
274
279
|
const wallet = wallets.find((w) => w.id === wallet_id);
|
|
275
280
|
if (!wallet) {
|
|
276
281
|
const available = wallets.map((w) => w.id).join(", ") || "none";
|
|
277
282
|
return text(`Wallet "${wallet_id}" not found. Available wallets: ${available}`);
|
|
278
283
|
}
|
|
279
|
-
if (max_per_tx == null &&
|
|
280
|
-
|
|
284
|
+
if (max_per_tx == null &&
|
|
285
|
+
max_per_day == null &&
|
|
286
|
+
require_confirmation_above == null) {
|
|
287
|
+
return text("No policy changes specified. Provide at least one policy field.");
|
|
281
288
|
}
|
|
289
|
+
const existing = getSpendPolicy(wallet_id) ?? {};
|
|
290
|
+
const nextPolicy = {
|
|
291
|
+
...existing,
|
|
292
|
+
...(max_per_tx != null ? { maxPerTxUsd: max_per_tx } : {}),
|
|
293
|
+
...(max_per_day != null ? { maxPerDayUsd: max_per_day } : {}),
|
|
294
|
+
...(require_confirmation_above != null ? { requireConfirmationAboveUsd: require_confirmation_above } : {}),
|
|
295
|
+
};
|
|
296
|
+
setSpendPolicy(wallet_id, nextPolicy);
|
|
282
297
|
// Build policy summary
|
|
283
298
|
const policies = [];
|
|
284
|
-
if (
|
|
285
|
-
policies.push(`Max per transaction: $${
|
|
299
|
+
if (nextPolicy.maxPerTxUsd != null) {
|
|
300
|
+
policies.push(`Max per transaction: $${nextPolicy.maxPerTxUsd.toFixed(2)}`);
|
|
301
|
+
}
|
|
302
|
+
if (nextPolicy.maxPerDayUsd != null) {
|
|
303
|
+
policies.push(`Max per day: $${nextPolicy.maxPerDayUsd.toFixed(2)}`);
|
|
286
304
|
}
|
|
287
|
-
if (
|
|
288
|
-
policies.push(`
|
|
305
|
+
if (nextPolicy.requireConfirmationAboveUsd != null) {
|
|
306
|
+
policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
|
|
289
307
|
}
|
|
290
|
-
// Note: actual persistence depends on the config module supporting policy fields.
|
|
291
|
-
// For now, we report what would be set. The core/config module will handle storage.
|
|
292
308
|
return text([
|
|
293
309
|
`Spending policy set for wallet "${wallet_id}":`,
|
|
294
310
|
...policies.map((p) => ` ${p}`),
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAtomicAmount } from "../amount-utils.js";
|
|
3
|
+
|
|
4
|
+
describe("toAtomicAmount", () => {
|
|
5
|
+
it("converts decimal USDC challenge amounts into atomic units", () => {
|
|
6
|
+
expect(toAtomicAmount("0.020000")).toBe(20000n);
|
|
7
|
+
expect(toAtomicAmount("1.000000")).toBe(1000000n);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("supports different decimal precisions", () => {
|
|
11
|
+
expect(toAtomicAmount("0.50", 2)).toBe(50n);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
4
|
+
|
|
3
5
|
let currentCard = {
|
|
4
6
|
consumerToken: "consumer_one",
|
|
5
7
|
paymentMethodId: "pm_one",
|
|
@@ -7,17 +9,23 @@ let currentCard = {
|
|
|
7
9
|
brand: "visa",
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
let currentDefaultWallet: any;
|
|
13
|
+
let currentWallets: any[] = [];
|
|
14
|
+
let currentResolvedMethod: { wallet: any; chain: string } | null = null;
|
|
15
|
+
|
|
16
|
+
const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
|
|
11
17
|
const mockMppxCreate = vi.fn();
|
|
12
18
|
const mockStripe = vi.fn((opts: unknown) => opts);
|
|
19
|
+
const mockTempo = vi.fn((..._args: unknown[]) => "tempo_method");
|
|
20
|
+
const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
|
|
13
21
|
|
|
14
22
|
vi.mock("../config.js", () => ({
|
|
15
23
|
getApiUrl: () => "http://api.test",
|
|
16
24
|
getCardConfig: () => currentCard,
|
|
17
25
|
getConfig: () => ({ defaultPaymentMethod: "card" }),
|
|
18
|
-
getDefaultWallet: () =>
|
|
19
|
-
getWallets: () =>
|
|
20
|
-
resolveWalletAndChain: () =>
|
|
26
|
+
getDefaultWallet: () => currentDefaultWallet,
|
|
27
|
+
getWallets: () => currentWallets,
|
|
28
|
+
resolveWalletAndChain: () => currentResolvedMethod,
|
|
21
29
|
}));
|
|
22
30
|
|
|
23
31
|
vi.mock("mppx/client", () => ({
|
|
@@ -25,21 +33,31 @@ vi.mock("mppx/client", () => ({
|
|
|
25
33
|
create: (config: unknown) => mockMppxCreate(config),
|
|
26
34
|
},
|
|
27
35
|
stripe: (config: unknown) => mockStripe(config),
|
|
36
|
+
tempo: (config: unknown) => mockTempo(config),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock("../base-charge.js", () => ({
|
|
40
|
+
baseChargeClient: (config: unknown) => mockBaseChargeClient(config),
|
|
28
41
|
}));
|
|
29
42
|
|
|
30
|
-
describe("
|
|
43
|
+
describe("payment method initialization", () => {
|
|
31
44
|
beforeEach(() => {
|
|
32
45
|
vi.clearAllMocks();
|
|
46
|
+
vi.resetModules();
|
|
33
47
|
currentCard = {
|
|
34
48
|
consumerToken: "consumer_one",
|
|
35
49
|
paymentMethodId: "pm_one",
|
|
36
50
|
last4: "1111",
|
|
37
51
|
brand: "visa",
|
|
38
52
|
};
|
|
53
|
+
currentDefaultWallet = undefined;
|
|
54
|
+
currentWallets = [];
|
|
55
|
+
currentResolvedMethod = null;
|
|
39
56
|
mockMppxCreate
|
|
40
57
|
.mockReturnValueOnce({ fetch: createdFetches[0] })
|
|
41
58
|
.mockReturnValueOnce({ fetch: createdFetches[1] })
|
|
42
|
-
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
59
|
+
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
60
|
+
.mockReturnValueOnce({ fetch: createdFetches[3] });
|
|
43
61
|
});
|
|
44
62
|
|
|
45
63
|
it("rebuilds the cached card fetch when the card config changes", async () => {
|
|
@@ -57,4 +75,48 @@ describe("card payment fetch cache", () => {
|
|
|
57
75
|
expect(firstFetch).not.toBe(secondFetch);
|
|
58
76
|
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
59
77
|
});
|
|
78
|
+
|
|
79
|
+
it("initializes only the Base method when base is requested", async () => {
|
|
80
|
+
const wallet = {
|
|
81
|
+
id: "aw-main",
|
|
82
|
+
keyType: "raw",
|
|
83
|
+
key: TEST_KEY,
|
|
84
|
+
chains: ["tempo", "base"],
|
|
85
|
+
defaultChain: "base",
|
|
86
|
+
};
|
|
87
|
+
currentDefaultWallet = wallet;
|
|
88
|
+
currentWallets = [wallet];
|
|
89
|
+
currentResolvedMethod = { wallet, chain: "base" };
|
|
90
|
+
|
|
91
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
92
|
+
await getPaymentFetch("base");
|
|
93
|
+
|
|
94
|
+
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(mockTempo).not.toHaveBeenCalled();
|
|
96
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
97
|
+
expect.objectContaining({ methods: ["base_method"] }),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("initializes only the Tempo method when tempo is requested", async () => {
|
|
102
|
+
const wallet = {
|
|
103
|
+
id: "aw-main",
|
|
104
|
+
keyType: "raw",
|
|
105
|
+
key: TEST_KEY,
|
|
106
|
+
chains: ["tempo", "base"],
|
|
107
|
+
defaultChain: "tempo",
|
|
108
|
+
};
|
|
109
|
+
currentDefaultWallet = wallet;
|
|
110
|
+
currentWallets = [wallet];
|
|
111
|
+
currentResolvedMethod = { wallet, chain: "tempo" };
|
|
112
|
+
|
|
113
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
114
|
+
await getPaymentFetch("tempo");
|
|
115
|
+
|
|
116
|
+
expect(mockTempo).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
118
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
119
|
+
expect.objectContaining({ methods: ["tempo_method"] }),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
60
122
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = {
|
|
4
|
+
policies: {} as Record<string, {
|
|
5
|
+
maxPerTxUsd?: number;
|
|
6
|
+
maxPerDayUsd?: number;
|
|
7
|
+
requireConfirmationAboveUsd?: number;
|
|
8
|
+
}>,
|
|
9
|
+
ledger: [] as Array<{ method: string; amountUsd: number; day: string; timestamp: string }>,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
vi.mock("../config.js", () => ({
|
|
13
|
+
getSpendPolicy: (method: string) => state.policies[method] ?? null,
|
|
14
|
+
getSpendLedger: () => state.ledger,
|
|
15
|
+
saveSpendLedger: (entries: Array<{ method: string; amountUsd: number; day: string; timestamp: string }>) => {
|
|
16
|
+
state.ledger = entries;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
canSpend,
|
|
22
|
+
getDailySpend,
|
|
23
|
+
recordSpend,
|
|
24
|
+
requiresPolicyConfirmation,
|
|
25
|
+
} from "../spend-policy.js";
|
|
26
|
+
|
|
27
|
+
describe("spend-policy", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
state.policies = {};
|
|
30
|
+
state.ledger = [];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("allows spend when no policy is set", () => {
|
|
34
|
+
expect(canSpend({ method: "wallet-1", amountUsd: 5 })).toEqual({ ok: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("blocks spends above max_per_tx", () => {
|
|
38
|
+
state.policies["wallet-1"] = { maxPerTxUsd: 2 };
|
|
39
|
+
const result = canSpend({ method: "wallet-1", amountUsd: 3 });
|
|
40
|
+
expect(result.ok).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("tracks daily spend totals per method", () => {
|
|
44
|
+
const now = new Date("2026-04-08T12:00:00.000Z");
|
|
45
|
+
recordSpend("wallet-1", 1.25, now);
|
|
46
|
+
recordSpend("wallet-1", 0.75, now);
|
|
47
|
+
recordSpend("wallet-2", 10, now);
|
|
48
|
+
|
|
49
|
+
expect(getDailySpend("wallet-1", now)).toBeCloseTo(2);
|
|
50
|
+
expect(getDailySpend("wallet-2", now)).toBeCloseTo(10);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("requires explicit confirmation above configured threshold", () => {
|
|
54
|
+
state.policies["wallet-1"] = { requireConfirmationAboveUsd: 2 };
|
|
55
|
+
expect(requiresPolicyConfirmation("wallet-1", 1.99)).toBe(false);
|
|
56
|
+
expect(requiresPolicyConfirmation("wallet-1", 2)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/core/base-charge.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Method, Credential, z } from "mppx";
|
|
8
8
|
import type { LocalAccount } from "viem/accounts";
|
|
9
|
+
import { toAtomicAmount } from "./amount-utils.js";
|
|
9
10
|
|
|
10
11
|
// Base USDC (Circle native)
|
|
11
12
|
const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
|
|
@@ -63,12 +64,12 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
63
64
|
return Method.toClient(baseChargeMethod as any, {
|
|
64
65
|
async createCredential({ challenge }: any) {
|
|
65
66
|
const { request } = challenge;
|
|
66
|
-
const amount =
|
|
67
|
+
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
67
68
|
const recipient = request.recipient as `0x${string}`;
|
|
68
69
|
const currency = (request.currency ?? BASE_USDC) as `0x${string}`;
|
|
69
70
|
|
|
70
71
|
// Dynamic imports to keep the module lightweight
|
|
71
|
-
const { createWalletClient, createPublicClient, http
|
|
72
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
72
73
|
const { base } = await import("viem/chains");
|
|
73
74
|
|
|
74
75
|
const rpcUrl = config.rpcUrl ?? "https://mainnet.base.org";
|
package/src/core/config.ts
CHANGED
|
@@ -21,6 +21,19 @@ export interface CardConfig {
|
|
|
21
21
|
brand: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface SpendPolicy {
|
|
25
|
+
maxPerTxUsd?: number;
|
|
26
|
+
maxPerDayUsd?: number;
|
|
27
|
+
requireConfirmationAboveUsd?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SpendLedgerEntry {
|
|
31
|
+
method: string;
|
|
32
|
+
amountUsd: number;
|
|
33
|
+
day: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export interface Config {
|
|
25
38
|
apiUrl: string;
|
|
26
39
|
apiKey: string | null;
|
|
@@ -35,6 +48,10 @@ export interface Config {
|
|
|
35
48
|
confirmBeforeSpend: boolean;
|
|
36
49
|
/** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
|
|
37
50
|
defaultTipAmount: number;
|
|
51
|
+
/** Optional per-method spend policies enforced client-side by MCP tools. */
|
|
52
|
+
spendPolicies?: Record<string, SpendPolicy>;
|
|
53
|
+
/** Daily spend ledger used to enforce max-per-day limits. */
|
|
54
|
+
spendLedger?: SpendLedgerEntry[];
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
/** All supported chain identifiers. */
|
|
@@ -82,6 +99,8 @@ interface LegacyConfig {
|
|
|
82
99
|
defaultWallet?: string | null;
|
|
83
100
|
card?: CardConfig | null;
|
|
84
101
|
pendingCardSetupToken?: string | null;
|
|
102
|
+
spendPolicies?: Record<string, SpendPolicy>;
|
|
103
|
+
spendLedger?: SpendLedgerEntry[];
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
/**
|
|
@@ -104,6 +123,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
104
123
|
favorites: r.favorites as string[] ?? [],
|
|
105
124
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
106
125
|
defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
|
|
126
|
+
spendPolicies: r.spendPolicies as Record<string, SpendPolicy> | undefined,
|
|
127
|
+
spendLedger: r.spendLedger as SpendLedgerEntry[] | undefined,
|
|
107
128
|
};
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -178,6 +199,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
178
199
|
favorites: [],
|
|
179
200
|
confirmBeforeSpend: true,
|
|
180
201
|
defaultTipAmount: 0,
|
|
202
|
+
spendPolicies: {},
|
|
203
|
+
spendLedger: [],
|
|
181
204
|
};
|
|
182
205
|
|
|
183
206
|
// Write migrated config (only if there was something to migrate)
|
|
@@ -203,6 +226,8 @@ export function getConfig(): Config {
|
|
|
203
226
|
favorites: [],
|
|
204
227
|
confirmBeforeSpend: true,
|
|
205
228
|
defaultTipAmount: 0,
|
|
229
|
+
spendPolicies: {},
|
|
230
|
+
spendLedger: [],
|
|
206
231
|
};
|
|
207
232
|
|
|
208
233
|
if (!existsSync(CONFIG_FILE)) {
|
|
@@ -249,6 +274,26 @@ export function getDefaultTipAmount(): number {
|
|
|
249
274
|
return getConfig().defaultTipAmount;
|
|
250
275
|
}
|
|
251
276
|
|
|
277
|
+
export function getSpendPolicy(method: string): SpendPolicy | null {
|
|
278
|
+
const policies = getConfig().spendPolicies ?? {};
|
|
279
|
+
return policies[method] ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function setSpendPolicy(method: string, policy: SpendPolicy): void {
|
|
283
|
+
const config = getConfig();
|
|
284
|
+
const policies = config.spendPolicies ?? {};
|
|
285
|
+
policies[method] = policy;
|
|
286
|
+
saveConfig({ spendPolicies: policies });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getSpendLedger(): SpendLedgerEntry[] {
|
|
290
|
+
return getConfig().spendLedger ?? [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function saveSpendLedger(entries: SpendLedgerEntry[]): void {
|
|
294
|
+
saveConfig({ spendLedger: entries });
|
|
295
|
+
}
|
|
296
|
+
|
|
252
297
|
// ── Wallet helpers ─────────────────────────────────────────────────
|
|
253
298
|
|
|
254
299
|
/**
|