agent402-mcp 0.13.3 → 0.13.4
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/index.js +84 -8
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -201,6 +201,76 @@ function walletRequiredText(tool) {
|
|
|
201
201
|
].join(" ");
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// What THIS call will actually be asked to pay.
|
|
206
|
+
//
|
|
207
|
+
// The catalog price is the ROUTE's list price, and it is not always the amount
|
|
208
|
+
// in the challenge: a route may quote per request from the body (a token-
|
|
209
|
+
// metered route, or a chat route priced by the model named in it). A cap
|
|
210
|
+
// enforced against the list price is therefore not a cap at all, so both paid
|
|
211
|
+
// paths read the amount out of the 402 before spending anything.
|
|
212
|
+
//
|
|
213
|
+
// The preflight carries no payment header and no credits key, so it can only
|
|
214
|
+
// ever draw an unpaid 402 - it never signs and never debits. Only run when a
|
|
215
|
+
// ceiling is configured: with none, the number changes nothing and the round
|
|
216
|
+
// trip is pure cost.
|
|
217
|
+
const CAPS_CONFIGURED = Number.isFinite(MAX_PER_CALL) || Number.isFinite(BUDGET);
|
|
218
|
+
|
|
219
|
+
/** The accepts array, at the top level or under the envelope keys sellers wrap it in. */
|
|
220
|
+
function acceptsOf(obj) {
|
|
221
|
+
if (!obj || typeof obj !== "object") return null;
|
|
222
|
+
if (Array.isArray(obj.accepts) && obj.accepts.length) return obj.accepts;
|
|
223
|
+
for (const k of ["payment", "x402", "paymentRequired", "payment_required"]) {
|
|
224
|
+
const nested = obj[k];
|
|
225
|
+
if (nested && typeof nested === "object" && Array.isArray(nested.accepts) && nested.accepts.length) return nested.accepts;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The MAXIMUM across accepts: the wallet picks which chain to settle on and
|
|
231
|
+
* this server cannot know which, so the only sound ceiling is the dearest
|
|
232
|
+
* offer. An entry we cannot price makes the whole quote unreadable (null),
|
|
233
|
+
* never a guess. */
|
|
234
|
+
function usdFromAccepts(accepts) {
|
|
235
|
+
if (!Array.isArray(accepts) || !accepts.length) return null;
|
|
236
|
+
let maxUsd = null;
|
|
237
|
+
for (const a of accepts) {
|
|
238
|
+
// v2 names the base-unit amount `amount`; x402 v1 called it `maxAmountRequired`.
|
|
239
|
+
const atomic = Number(a && (a.amount ?? a.maxAmountRequired));
|
|
240
|
+
if (!Number.isFinite(atomic) || atomic < 0) return null;
|
|
241
|
+
// USDC is 6 decimals on every chain these rails settle; a wrong exponent is
|
|
242
|
+
// a 1000x pricing error, so an out-of-range hint reads as unreadable.
|
|
243
|
+
const declared = (a && a.extra && a.extra.decimals) ?? (a && a.decimals);
|
|
244
|
+
const decimals = declared == null ? 6 : Number(declared);
|
|
245
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 30) return null;
|
|
246
|
+
const usd = atomic / 10 ** decimals;
|
|
247
|
+
if (maxUsd == null || usd > maxUsd) maxUsd = usd;
|
|
248
|
+
}
|
|
249
|
+
return maxUsd;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The quoted price for this request, or `listUsd` when the 402 cannot be read.
|
|
253
|
+
* x402 v2 carries the challenge base64 in the PAYMENT-REQUIRED header and
|
|
254
|
+
* leaves the body `{}`; v1 sellers put it in the body. Header first. */
|
|
255
|
+
async function quotedUsd(url, init, listUsd) {
|
|
256
|
+
if (!CAPS_CONFIGURED) return listUsd;
|
|
257
|
+
try {
|
|
258
|
+
const pre = await fetch(url, init);
|
|
259
|
+
if (pre.status !== 402) return listUsd;
|
|
260
|
+
let quoted = null;
|
|
261
|
+
const header = pre.headers.get("payment-required");
|
|
262
|
+
if (header) {
|
|
263
|
+
try { quoted = usdFromAccepts(acceptsOf(JSON.parse(Buffer.from(header.trim(), "base64").toString("utf8")))); } catch { quoted = null; }
|
|
264
|
+
}
|
|
265
|
+
if (quoted == null) {
|
|
266
|
+
let body = null; try { body = await pre.json(); } catch { body = null; }
|
|
267
|
+
quoted = usdFromAccepts(acceptsOf(body));
|
|
268
|
+
}
|
|
269
|
+
// Larger of the two: an understated catalog must not lower the ceiling.
|
|
270
|
+
return quoted == null ? listUsd : Math.max(listUsd, quoted);
|
|
271
|
+
} catch { return listUsd; /* fail-open to the advertised price */ }
|
|
272
|
+
}
|
|
273
|
+
|
|
204
274
|
async function callEndpoint(tool, args = {}) {
|
|
205
275
|
const url = new URL(`${BASE}${tool.path}`);
|
|
206
276
|
const init = { method: tool.method, headers: { Accept: "application/json" } };
|
|
@@ -216,14 +286,16 @@ async function callEndpoint(tool, args = {}) {
|
|
|
216
286
|
|
|
217
287
|
let res;
|
|
218
288
|
if (!HAS_WALLET && HAS_CREDITS) {
|
|
219
|
-
// Card credits: same budget guards as the wallet path
|
|
220
|
-
// the
|
|
221
|
-
|
|
289
|
+
// Card credits: same budget guards as the wallet path, against the same
|
|
290
|
+
// number - the credits gate holds and debits what the 402 quotes for THIS
|
|
291
|
+
// body, not the route's list price.
|
|
292
|
+
const listUsd = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
|
|
293
|
+
const price = await quotedUsd(url, init, listUsd);
|
|
222
294
|
if (price > MAX_PER_CALL) {
|
|
223
|
-
return { content: [{ type: "text", text: `Refused: "${tool.slug}"
|
|
295
|
+
return { content: [{ type: "text", text: `Refused: "${tool.slug}" is quoted $${price} for this request${price !== listUsd ? ` (list price ${tool.price})` : ""}, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}.` }], isError: true };
|
|
224
296
|
}
|
|
225
297
|
if (spentUsd + price > BUDGET) {
|
|
226
|
-
return { content: [{ type: "text", text: `Refused: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}"
|
|
298
|
+
return { content: [{ type: "text", text: `Refused: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}" is quoted $${price} for this request).` }], isError: true };
|
|
227
299
|
}
|
|
228
300
|
res = await fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${CREDITS_KEY}` } });
|
|
229
301
|
if (res.ok) spentUsd += price;
|
|
@@ -232,16 +304,20 @@ async function callEndpoint(tool, args = {}) {
|
|
|
232
304
|
return { content: [{ type: "text", text: `Credits refused for "${tool.slug}": ${body.error || "payment required"}${body.balanceUsd != null ? ` (balance $${body.balanceUsd})` : ""}. Top up at ${body.topup || `${BASE}/credits`}.` }], isError: true };
|
|
233
305
|
}
|
|
234
306
|
} else if (HAS_WALLET) {
|
|
235
|
-
|
|
307
|
+
// The wallet signs whatever the challenge asks for (vendor spend controls
|
|
308
|
+
// are off - this package bounds spend itself), so the cap is checked
|
|
309
|
+
// against that amount, read from the 402 before anything is signed.
|
|
310
|
+
const listUsd = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
|
|
311
|
+
const price = await quotedUsd(url, init, listUsd);
|
|
236
312
|
if (price > MAX_PER_CALL) {
|
|
237
313
|
return {
|
|
238
|
-
content: [{ type: "text", text: `Refused without paying: "${tool.slug}"
|
|
314
|
+
content: [{ type: "text", text: `Refused without paying: "${tool.slug}" is quoted $${price} for this request${price !== listUsd ? ` (list price ${tool.price})` : ""}, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}. Raise the cap on this MCP server to allow it.` }],
|
|
239
315
|
isError: true,
|
|
240
316
|
};
|
|
241
317
|
}
|
|
242
318
|
if (spentUsd + price > BUDGET) {
|
|
243
319
|
return {
|
|
244
|
-
content: [{ type: "text", text: `Refused without paying: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}"
|
|
320
|
+
content: [{ type: "text", text: `Refused without paying: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}" is quoted $${price} for this request). Restart the MCP server or raise AGENT402_BUDGET.` }],
|
|
245
321
|
isError: true,
|
|
246
322
|
};
|
|
247
323
|
}
|
package/package.json
CHANGED