agent402-mcp 0.13.2 → 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/README.md +3 -3
- package/index.js +87 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for [Agent402](https://agent402.tools), the applied layer of [Agentic Finance](https://agent402.tools/agentic-finance) - a catalog of **500+: 400+ pay-per-call web tools + 80+ curated multi-tool skill packs** for AI agents (every one tested, priced, and settled on-chain; every one earns its place), paid per call in USDC via the [x402 protocol](https://www.x402.org), **with compute (proof-of-work)** when no wallet is configured, or **by card** through a prepaid credits key. (The hosted API is dual-stack: it also accepts [MPP](https://agent402.tools/what-is-mpp) clients on the same 402, settling on Base/Celo or natively on Tempo; this package pays via x402 or credits.) Built by [Havok Holdings LLC](https://github.com/MikeyPetrillo/Agent402).
|
|
4
4
|
|
|
5
|
-
Your agent gets browser rendering, screenshots, PDF text extraction, URL→markdown, live web search **+ web answers with citations**, live **financial/crypto/macro data** (
|
|
5
|
+
Your agent gets browser rendering, screenshots, PDF text extraction, URL→markdown, live web search **+ web answers with citations**, live **financial/crypto/macro data** (US equity quotes and daily bars from Databento, CoinGecko, FRED, ECB FX, World Bank, yield curve), **SEC EDGAR filings** (10-K/10-Q text, XBRL, insider, 13F, IPO calendar), **deterministic stats & forecasting** (Pearson correlation, OLS, Holt-Winters), **compression** (gzip/brotli), DNS/TLS/WHOIS + email-deliverability checks, wallet-keyed shared memory, and 200+ deterministic pure-CPU utilities - plus 100+ **skill packs** like `security-audit`, `trend-analysis`, `structured-scrape`, `decode-blob`, and `forecasting-bake-off` callable as MCP prompts, and the **report products** (deep research, company dossier, 13F fund report, domain audit, token risk, FDA recall, insider flow, market brief) as ordinary catalog slugs. Payment handled invisibly underneath the MCP calls. No signup, no API key.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ wrapped with `mppx`'s `McpClient.wrap()` (USDC on Base/Celo, or native Tempo)
|
|
|
15
15
|
pays and retries on its own, receipt in `_meta`. For no rate limit, or to pay
|
|
16
16
|
over x402 or by card instead, run this package locally:
|
|
17
17
|
|
|
18
|
-
With a funded wallet (USDC on Base, Polygon, Arbitrum, Monad,
|
|
18
|
+
With a funded wallet (USDC on any EVM rail the server offers - Base, Polygon, Arbitrum, Monad, Celo, Avalanche, Sei, Optimism - or USDG on Robinhood Chain, or USDC on Solana via `SOLANA_AGENT_KEY`; the service also settles on Stellar and Algorand, which this package does not sign) - every tool available:
|
|
19
19
|
|
|
20
20
|
```json
|
|
21
21
|
{
|
|
@@ -98,7 +98,7 @@ so set `AGENT402_MAX_PER_CALL` if you want a hard per-call ceiling.
|
|
|
98
98
|
| ≤ $0.50 | $0.55 | `POST /api/route/execute-max` |
|
|
99
99
|
|
|
100
100
|
A tool priced above the tier's ceiling returns a self-correcting 409 naming its direct route.
|
|
101
|
-
- **Report products** are catalog slugs like any other, so `catalog.call` runs them with a wallet or a credits key: `research` ($0.
|
|
101
|
+
- **Report products** are catalog slugs like any other, so `catalog.call` runs them with a wallet or a credits key: `research` ($0.60), `research-pro` ($0.85), `research-max` ($1.10), `market-brief` ($0.85), `dossier` ($0.85), `dossier-max` ($1.10), `fund-report` ($0.60), `fund-report-max` ($0.85), `domain-audit` ($0.60), `domain-audit-pro` ($0.85), `token-risk` ($0.60), `token-risk-pro` ($0.85), `token-brief` ($0.60), `recall-report` ($0.60), `insider-report` ($0.60), `filing-report` ($0.85), `ticker-pack` ($2.00); the live figures are always at https://agent402.tools/api/pricing. The same reports are sold to people by card at https://agent402.tools/reports for $2 to $5 depending on depth: the card price includes payment processing, and an agent paying per call pays the lower tool price for the same report. The recurring monitors (domain, SEC filing, token, fund, recall, insider, IPO, research - $5/month) are at https://agent402.tools/monitors.
|
|
102
102
|
|
|
103
103
|
## Workflows (skill packs)
|
|
104
104
|
|
package/index.js
CHANGED
|
@@ -72,7 +72,7 @@ let spentUsd = 0;
|
|
|
72
72
|
|
|
73
73
|
const DEFAULT_CURATED = [
|
|
74
74
|
// Flagship demand set — keep aligned with src/mcp-flagship.js FLAGSHIP_SLUGS.
|
|
75
|
-
// Search/answer is the front door; long tail stays behind
|
|
75
|
+
// Search/answer is the front door; long tail stays behind catalog.search/catalog.call.
|
|
76
76
|
"search", "answer", "search-news", "render",
|
|
77
77
|
"stock-quote", "transcribe", "memory-read", "memory-write",
|
|
78
78
|
];
|
|
@@ -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
|
}
|
|
@@ -812,14 +888,14 @@ try {
|
|
|
812
888
|
// Don't hard-exit: starting with an empty catalog still lets the server
|
|
813
889
|
// connect and answer introspection (tools/list) — required to pass directory
|
|
814
890
|
// health checks (e.g. Glama) and more resilient if the catalog endpoint is
|
|
815
|
-
// briefly unreachable.
|
|
891
|
+
// briefly unreachable. catalog.search/catalog.call just return nothing until the
|
|
816
892
|
// catalog is reachable again.
|
|
817
893
|
log(`Could not load the catalog from ${BASE}: ${err.message} — starting with an empty catalog`);
|
|
818
894
|
}
|
|
819
895
|
const requested = (process.env.AGENT402_TOOLS || DEFAULT_CURATED.join(","))
|
|
820
896
|
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
821
897
|
curated = requested.map((slug) => catalog.get(slug)).filter(Boolean);
|
|
822
|
-
log(`catalog: ${catalog.size} tools from ${BASE}; ${curated.length} first-class, rest via
|
|
898
|
+
log(`catalog: ${catalog.size} tools from ${BASE}; ${curated.length} first-class, rest via catalog.search/catalog.call`);
|
|
823
899
|
log(
|
|
824
900
|
HAS_WALLET
|
|
825
901
|
? `payment: USDC via x402 (${[AGENT_KEY && "EVM", SOLANA_AGENT_KEY && "Solana"].filter(Boolean).join(" + ")} wallet configured; max/call ${MAX_PER_CALL === Infinity ? "unlimited" : `$${MAX_PER_CALL}`}, budget ${BUDGET === Infinity ? "unlimited" : `$${BUDGET}`})`
|
package/package.json
CHANGED