@tensorfeed/mcp-server 1.39.0 → 2.0.1

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/index.js CHANGED
@@ -18,7 +18,7 @@ const SDK_VERSION = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
18
18
  // get_model_pricing). Each is a signal an agent gathers before choosing a
19
19
  // model, so each points at the one premium tool that fuses them into a signed
20
20
  // decision. Mirrors the hosted /api/mcp `next` field. Honest, zero em dashes.
21
- const ROUTE_VERDICT_FOOTER = '\n\nNext: route_verdict (1 credit, $0.02) fuses live pricing, contamination-discounted ' +
21
+ const ROUTE_VERDICT_FOOTER = '\n\nNext: route_verdict with tier="full" (1 credit, $0.02) fuses live pricing, contamination-discounted ' +
22
22
  'benchmarks, real production usage, measured p95 latency, incident state, and deprecation ' +
23
23
  'flags into a signed best-fit model decision with ranked runners-up and an AFTA-signed ' +
24
24
  'receipt. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits ' +
@@ -190,7 +190,23 @@ server.registerResource('latest_news', 'tensorfeed://news/latest', {
190
190
  registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a ranked list with title, source, URL, snippet, and publish time, filterable by category (e.g. "anthropic", "openai", "research", "tools"). Aggregates 15+ sources (Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more) into one normalized feed, so an agent reads one schema instead of polling each outlet. Free, no auth.', {
191
191
  category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
192
192
  limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
193
- }, async ({ category, limit }) => {
193
+ digest: z.boolean().optional().describe('When true, return a headline-only digest (title, source, URL per story) instead of the full article set.'),
194
+ }, async ({ category, limit, digest }) => {
195
+ // digest mode is a headline-only companion view of the same feed. It honors
196
+ // the same inputs as the full view: the category filter and the schema limit
197
+ // (default 5, max 50). Earlier this branch dropped category and capped at 20,
198
+ // silently ignoring declared params.
199
+ if (digest) {
200
+ const dParams = new URLSearchParams();
201
+ if (category)
202
+ dParams.set('category', category);
203
+ dParams.set('limit', String(limit || 5));
204
+ const data = await fetchJSON(`/news?${dParams}`);
205
+ const text = data.articles
206
+ .map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
207
+ .join('\n\n');
208
+ return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
209
+ }
194
210
  const params = new URLSearchParams();
195
211
  if (category)
196
212
  params.set('category', category);
@@ -201,6 +217,29 @@ registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a rank
201
217
  .join('\n\n');
202
218
  return { content: [{ type: 'text', text: text || 'No articles found.' }] };
203
219
  });
220
+ // ── Tool: find_tensorfeed_data (discovery, long-tail surface) ───────
221
+ registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers a data need. Describe what you want in plain language (e.g. "trending AI papers", "is OpenAI down", "model price history") and this returns the 2 to 3 best-matching TensorFeed endpoints, each with its HTTP path, what it returns, and whether it is free or paid. TensorFeed exposes 100+ AI-ecosystem data and signed-verdict endpoints; the core ones are also dedicated tools, but the full catalog is reachable here and callable over HTTP (paid ones via x402 or a credits token). Free, no auth. Use this first when no dedicated tool obviously fits.', {
222
+ query: z.string().describe('Plain-language description of the data or decision you need.'),
223
+ limit: z.number().int().min(1).max(5).optional().describe('Max endpoints to return (default 3).'),
224
+ }, async ({ query, limit }) => {
225
+ const { flattenCatalog, scoreEndpoints } = await import('./discovery.js');
226
+ let meta;
227
+ try {
228
+ meta = await fetchJSON('/meta');
229
+ }
230
+ catch {
231
+ const { readFileSync } = await import('node:fs');
232
+ const p = join(dirname(fileURLToPath(import.meta.url)), 'meta-snapshot.json');
233
+ meta = JSON.parse(readFileSync(p, 'utf-8'));
234
+ }
235
+ const rows = flattenCatalog(meta);
236
+ const top = scoreEndpoints(rows, query, limit ?? 3);
237
+ if (top.length === 0) {
238
+ return { content: [{ type: 'text', text: `No TensorFeed endpoint matched "${query}". Browse the full catalog at https://tensorfeed.ai/api/meta or https://tensorfeed.ai/developers.` }] };
239
+ }
240
+ const lines = top.map((r) => `- ${r.path}\n ${r.description}`).join('\n');
241
+ return { content: [{ type: 'text', text: `Top TensorFeed endpoints for "${query}":\n${lines}\n\nCall free endpoints directly over HTTP. Paid endpoints (marked with a credit cost) accept x402 payment or a TENSORFEED_TOKEN bearer; see https://tensorfeed.ai/developers/agent-payments.` }] };
242
+ });
204
243
  // ── Tool: get_ai_status ─────────────────────────────────────────────
205
244
  registerTool('get_ai_status', 'Get the real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face), including per-component breakdowns and an operational/degraded/down rollup per provider. One cross-provider status call instead of checking each vendor\'s status page, useful before an agent routes a request to a model that may be impaired. Free, no auth.', {}, async () => {
206
245
  const data = await fetchJSON('/status');
@@ -235,61 +274,6 @@ registerTool('is_service_down', 'Check whether one named AI service (e.g. "claud
235
274
  }]
236
275
  };
237
276
  });
238
- // Tool: check_agent_ready
239
- registerTool('check_agent_ready', 'Score how agent-ready a website is, 0 to 100, from its public surfaces: x402 payment manifest, /.well-known/agent.json, an OpenAPI spec, llms.txt, AI-bot crawlability, and ai.txt. Returns the score, the tier (closed, emerging, ready, or advanced), and exactly which surfaces the site exposes. Unlike single-site "is my site agent-ready" scanners, this reads from TensorFeed\'s daily-crawled cross-domain dataset, so the same call works for any tracked domain and the scoring rule is published and identical for every site. Free, no auth.', {
240
- domain: z.string().describe('Bare domain to score, e.g. "stripe.com" (no scheme, no path)'),
241
- }, async ({ domain }) => {
242
- const data = await fetchJSON(`/agent-ready/site?domain=${encodeURIComponent(domain)}`);
243
- if (!data.found || !data.readiness) {
244
- return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet (no agent-readiness score). About 500 curated domains are tracked.` }] };
245
- }
246
- const r = data.readiness;
247
- const present = Object.entries(r.surfaces).filter(([, v]) => v).map(([k]) => k);
248
- const text = `${data.domain}: agent-readiness ${r.score}/100 (${r.tier})\n` +
249
- `Surfaces present: ${present.length ? present.join(', ') : 'none'}\n` +
250
- `x402=${r.surfaces.x402} agent.json=${r.surfaces.agentJson} openapi=${r.surfaces.openapi} llms.txt=${r.surfaces.llmsTxt} crawlable=${r.surfaces.crawlable} ai.txt=${r.surfaces.aiTxt}\n` +
251
- `Last checked: ${data.captured_at ?? 'n/a'}`;
252
- return { content: [{ type: 'text', text }] };
253
- });
254
- // Tool: agent_ready_summary
255
- registerTool('agent_ready_summary', 'Get the state of the agentic web. Across about 500 curated domains, returns the share exposing each agent surface (x402, agent.json, OpenAPI, llms.txt, crawlable, ai.txt), the readiness-tier distribution, and a top-10 leaderboard of the most agent-ready sites. This aggregate, cross-domain view is exactly what a single-site scanner cannot give you. Free, no auth.', {}, async () => {
256
- const d = await fetchJSON('/agent-ready/summary.json');
257
- const a = d.adoption_pct;
258
- const lb = (d.leaderboard || []).slice(0, 10).map((e, i) => `${i + 1}. ${e.domain} (${e.score}, ${e.tier})`).join('\n');
259
- const text = `Agentic web readiness, ${d.profiled}/${d.domains_tracked} domains profiled (captured ${d.captured_at ?? 'n/a'}):\n` +
260
- `Surface adoption: x402 ${a.x402}%, agent.json ${a.agentJson}%, openapi ${a.openapi}%, llms.txt ${a.llmsTxt}%, crawlable ${a.crawlable}%, ai.txt ${a.aiTxt}%\n` +
261
- `Tiers: ${Object.entries(d.tier_distribution || {}).map(([t, n]) => `${t} ${n}`).join(', ')}\n` +
262
- `Most agent-ready:\n${lb}`;
263
- return { content: [{ type: 'text', text }] };
264
- });
265
- // Tool: check_crawler_access
266
- registerTool('check_crawler_access', 'Check which AI bots a website allows or blocks in its robots.txt: GPTBot, ClaudeBot, PerplexityBot, CCBot, Google-Extended, Bytespider, and more, with a per-bot verdict (allowed, blocked, partial, or unknown), plus whether the site publishes llms.txt and ai.txt. Reports the site\'s stated policy, not enforcement (robots.txt compliance is voluntary). Free, no auth.', {
267
- domain: z.string().describe('Bare domain to check, e.g. "nytimes.com" (no scheme, no path)'),
268
- }, async ({ domain }) => {
269
- const data = await fetchJSON(`/ai-crawler-access/site?domain=${encodeURIComponent(domain)}`);
270
- if (!data.found || !data.record) {
271
- return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet. About 500 curated domains are tracked for AI-crawler access.` }] };
272
- }
273
- const rec = data.record;
274
- const bots = Object.entries(rec.bots).map(([b, v]) => ` ${b}: ${v}`).join('\n');
275
- const text = `${data.domain} (robots.txt HTTP ${rec.robotsStatus ?? 'n/a'}) AI-bot access:\n${bots}\n` +
276
- `llms.txt: ${rec.hasLlmsTxt ? 'yes' : 'no'}, ai.txt: ${rec.hasAiTxt ? 'yes' : 'no'}\n` +
277
- `Stated policy only; compliance is voluntary. Last checked: ${data.captured_at ?? 'n/a'}`;
278
- return { content: [{ type: 'text', text }] };
279
- });
280
- // Tool: crawler_access_summary
281
- registerTool('crawler_access_summary', 'Get the aggregate AI-crawler access map across about 500 curated domains: the percentage of sites that block each AI bot (the publisher-versus-AI-crawler picture), plus llms.txt and ai.txt adoption rates. Unlike a single-site robots.txt checker, this is the cross-domain trend view of who is shutting AI crawlers out. Free, no auth.', {}, async () => {
282
- const d = await fetchJSON('/ai-crawler-access/summary.json');
283
- const blocked = Object.entries(d.bot_blocked_pct || {})
284
- .sort((x, y) => y[1] - x[1])
285
- .slice(0, 8)
286
- .map(([b, p]) => ` ${b}: ${p}% blocked`)
287
- .join('\n');
288
- const text = `AI-crawler access across ${d.domains_with_data}/${d.domains_tracked} tracked domains (captured ${d.captured_at ?? 'n/a'}):\n` +
289
- `Most-blocked AI bots:\n${blocked}\n` +
290
- `llms.txt adoption: ${d.llms_txt_adoption_pct}%, ai.txt adoption: ${d.ai_txt_adoption_pct}%`;
291
- return { content: [{ type: 'text', text }] };
292
- });
293
277
  // ── Tool: get_model_pricing ─────────────────────────────────────────
294
278
  registerTool('get_model_pricing', 'Get AI model pricing across major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere) in one normalized table: input and output price per 1M tokens, context window, and release date per model. One cross-provider comparison instead of scraping six pricing pages, so an agent can pick the cheapest model that fits its context and budget. Free, no auth.', {}, async () => {
295
279
  const data = await fetchJSON('/models');
@@ -310,363 +294,107 @@ registerTool('get_model_pricing', 'Get AI model pricing across major providers (
310
294
  .join('\n\n');
311
295
  return { content: [{ type: 'text', text: `AI Model Pricing (per 1M tokens):\n\n${text}${ROUTE_VERDICT_FOOTER}` }] };
312
296
  });
313
- // ── Tool: get_model_deprecations ────────────────────────────────────
314
- registerTool('get_model_deprecations', "Get the AI model deprecation calendar. Provider-by-provider registry of model retirements and deprecation announcements (OpenAI, Anthropic, Google, Cohere, and others). Each entry carries the announced/deprecation/sunset dates, the recommended replacement model, and a source URL pointing at the provider's own announcement. Useful for agents who need to know when a model they depend on will stop accepting traffic and what to migrate to.", {
315
- provider: z
316
- .string()
317
- .optional()
318
- .describe('Filter to a single provider (e.g. "OpenAI", "Anthropic", "Google", "Cohere"). Case-insensitive.'),
319
- status: z
320
- .enum(['announced', 'deprecated', 'sunsetted'])
321
- .optional()
322
- .describe('Filter by lifecycle stage. announced = deprecation announced but model still serves at full capacity; deprecated = past deprecation date, migration required; sunsetted = no longer accepts traffic.'),
323
- }, async ({ provider, status }) => {
324
- const params = new URLSearchParams();
325
- if (provider)
326
- params.set('provider', provider);
327
- if (status)
328
- params.set('status', status);
329
- const qs = params.toString();
330
- const data = await fetchJSON(`/model-deprecations${qs ? `?${qs}` : ''}`);
331
- const entries = data.deprecations ?? [];
332
- if (entries.length === 0) {
333
- return {
334
- content: [
335
- {
336
- type: 'text',
337
- text: 'No model deprecations matched the filter. The full calendar is at https://tensorfeed.ai/model-deprecations.',
338
- },
339
- ],
340
- };
341
- }
342
- // Group by provider for readable output.
343
- const byProvider = new Map();
344
- for (const d of entries) {
345
- const list = byProvider.get(d.provider) ?? [];
346
- list.push(d);
347
- byProvider.set(d.provider, list);
297
+ // ════════════════════════════════════════════════════════════════════
298
+ // PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
299
+ // ════════════════════════════════════════════════════════════════════
300
+ // ── Tool: account_status ────────────────────────────────────────────
301
+ // Merges the former get_account_balance and get_account_usage tools into
302
+ // one read. Both fetch calls and both output bodies are reused verbatim
303
+ // and concatenated into a single text response.
304
+ registerTool('account_status', 'Check the configured TensorFeed token: current credit balance plus recent per-endpoint usage (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
305
+ // Balance and usage are independent reads; fetch them in parallel so the
306
+ // merged tool costs one round-trip of wall-clock time, not two.
307
+ const [balance, usage] = (await Promise.all([
308
+ fetchJSON('/payment/balance', { auth: true }),
309
+ fetchJSON('/payment/usage', { auth: true }),
310
+ ]));
311
+ const balanceText = `Balance: ${balance.balance} credits\nTotal purchased: ${balance.total_purchased}\nCreated: ${balance.created}\nLast used: ${balance.last_used}`;
312
+ let usageText;
313
+ if (usage.total_calls === 0) {
314
+ usageText = 'No premium API calls on this token yet.';
348
315
  }
349
- const sections = Array.from(byProvider.entries())
350
- .sort(([a], [b]) => a.localeCompare(b))
351
- .map(([prov, list]) => {
352
- const rows = list
353
- .map(d => {
354
- const dates = [
355
- d.announcedDate ? `announced ${d.announcedDate}` : null,
356
- d.deprecationDate ? `deprecated ${d.deprecationDate}` : null,
357
- d.sunsetDate ? `sunset ${d.sunsetDate}` : null,
358
- ]
359
- .filter(Boolean)
360
- .join(', ');
361
- const display = d.modelDisplay ? ` (${d.modelDisplay})` : '';
362
- const repl = d.replacement ? `\n replacement: ${d.replacement}` : '';
363
- const note = d.notes ? `\n note: ${d.notes}` : '';
364
- return ` ${d.model}${display} [${d.status.toUpperCase()}]\n ${dates}${repl}${note}\n source: ${d.sourceUrl}`;
365
- })
316
+ else {
317
+ const rows = Object.entries(usage.by_endpoint)
318
+ .sort(([, a], [, b]) => b.calls - a.calls)
319
+ .map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
366
320
  .join('\n');
367
- return `${prov}:\n${rows}`;
368
- })
369
- .join('\n\n');
370
- const header = `AI Model Deprecation Calendar (${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}, last updated ${data.lastUpdated ?? 'recently'}):\n\n`;
371
- return { content: [{ type: 'text', text: header + sections }] };
372
- });
373
- // ── Tool: get_ai_today ──────────────────────────────────────────────
374
- registerTool('get_ai_today', 'Get a quick digest of the top AI stories from the latest TensorFeed news feed, each with title, source, and URL. A shorter, headline-only companion to get_ai_news for when an agent just needs a fast read on what is happening in AI right now rather than the full article set. Free, no auth.', {
375
- limit: z.number().min(1).max(20).optional().describe('Number of stories (default 5)'),
376
- }, async ({ limit }) => {
377
- const data = await fetchJSON(`/news?limit=${limit || 5}`);
378
- const text = data.articles
379
- .map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
380
- .join('\n\n');
381
- return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
382
- });
383
- // ── Tool: get_agent_activity ────────────────────────────────────────
384
- registerTool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Returns today\'s bot hit count, the most recent 50 hits with bot name + endpoint + timestamp, and a derived top-bots breakdown. Useful when an agent wants to see which crawlers are pulling AI ecosystem data and how that distribution shifts. Free, no auth.', {}, async () => {
385
- const data = (await fetchJSON('/agents/activity'));
386
- const tally = new Map();
387
- for (const hit of data.recent) {
388
- tally.set(hit.bot, (tally.get(hit.bot) || 0) + 1);
389
- }
390
- const breakdown = Array.from(tally.entries())
391
- .map(([bot, count]) => ` ${bot}: ${count}`)
392
- .sort()
393
- .join('\n');
394
- const tail = data.recent
395
- .slice(0, 10)
396
- .map(h => ` ${h.bot} ${h.endpoint} (${h.timestamp})`)
397
- .join('\n');
398
- return {
399
- content: [
400
- {
401
- type: 'text',
402
- text: `AI bot traffic on TensorFeed.ai\n\n` +
403
- `Today's bot hits: ${data.today_count}\n` +
404
- `Last updated: ${data.last_updated}\n\n` +
405
- `Bot breakdown (recent ${data.recent.length} hits):\n${breakdown || ' (none)'}\n\n` +
406
- `Recent tail (10 most recent):\n${tail || ' (none)'}\n\n` +
407
- `Public dashboard: https://tensorfeed.ai/agent-traffic`,
408
- },
409
- ],
410
- };
411
- });
412
- // ── Tool: get_ai_supply_chain_iocs ──────────────────────────────────
413
- registerTool('get_ai_supply_chain_iocs', 'Get the latest AI/ML supply-chain indicators-of-compromise from the TensorFeed defender feed. Daily-refreshed list of GitHub Security Advisory entries filtered by AI/ML/LLM/MCP keyword vocabulary across npm, PyPI, Go, Maven, and other ecosystems. Each entry includes the package name, ecosystem, GHSA advisory ID, severity, summary, vulnerable version range, publication date, and a link to the authoritative GHSA record. Pure data feed: TensorFeed re-publishes already-public advisories, does not detect malware, and does not attribute it. Always treat the linked GHSA record as authoritative. Useful for AI-tool maintainers, MCP-server reviewers, and supply-chain monitors that want a single feed of AI-relevant advisories rather than parsing all of GHSA.', {
414
- severity: z
415
- .enum(['critical', 'high', 'moderate', 'low'])
416
- .optional()
417
- .describe('Filter to a single GHSA severity tier'),
418
- ecosystem: z
419
- .string()
420
- .optional()
421
- .describe('Filter to a single package ecosystem (e.g. "npm", "pypi", "go", "maven")'),
422
- limit: z
423
- .number()
424
- .min(1)
425
- .max(100)
426
- .optional()
427
- .describe('Maximum number of advisories to return (default 25, max 100)'),
428
- }, async ({ severity, ecosystem, limit }) => {
429
- const data = (await fetchJSON('/security/ai-supply-chain-iocs.json'));
430
- const sevWanted = severity?.toLowerCase();
431
- const ecoWanted = ecosystem?.toLowerCase();
432
- const filtered = data.entries.filter((e) => {
433
- if (sevWanted && e.severity.toLowerCase() !== sevWanted)
434
- return false;
435
- if (ecoWanted && e.package.ecosystem.toLowerCase() !== ecoWanted)
436
- return false;
437
- return true;
438
- });
439
- const cap = Math.min(limit ?? 25, 100);
440
- const rows = filtered.slice(0, cap);
441
- if (rows.length === 0) {
442
- return {
443
- content: [
444
- {
445
- type: 'text',
446
- text: `No AI supply-chain IOCs matched (total in feed: ${data.total}, generated ${data.generated_at}).`,
447
- },
448
- ],
449
- };
450
- }
451
- const body = rows
452
- .map((e, i) => {
453
- const kw = e.ai_relevance?.matched_keywords?.length
454
- ? ` [matched: ${e.ai_relevance.matched_keywords.join(', ')}]`
455
- : '';
456
- return [
457
- `${i + 1}. ${e.package.ecosystem}: ${e.package.name} (${e.severity.toUpperCase()})`,
458
- ` ${e.summary}`,
459
- ` GHSA: ${e.advisory_id} ${e.url}`,
460
- ` Affected: ${e.vulnerable_version_range} Published: ${e.published_at}${kw}`,
461
- ].join('\n');
462
- })
463
- .join('\n\n');
464
- const header = `AI supply-chain IOCs (${rows.length} of ${filtered.length} matched, ${data.total} in feed, generated ${data.generated_at}):`;
465
- const footer = `\n\nPosture: ${data.posture}`;
466
- return { content: [{ type: 'text', text: `${header}\n\n${body}${footer}` }] };
467
- });
468
- // ── Tool: get_honeypot_iocs ─────────────────────────────────────────
469
- registerTool('get_honeypot_iocs', 'Get the TensorFeed honeypot indicators-of-compromise feed. Returns attacker IP addresses, user agents, and target paths observed against TensorFeed.ai trap endpoints over the last 30 days. Pure first-party observation; not a re-export of upstream threat intel. License CC0; downstream defenders may ingest and pre-block at their edge. Each row: ioc value, ioc type (ip / ua / path), first_seen, last_seen, hit_count. Useful as a low-noise honeypot signal layered on top of public IOC feeds.', {
470
- type: z
471
- .enum(['ip', 'ua', 'path'])
472
- .optional()
473
- .describe('Filter to one IOC type. Omit to return all types.'),
474
- limit: z
475
- .number()
476
- .min(1)
477
- .max(500)
478
- .optional()
479
- .describe('Maximum number of IOCs to return (default 50, max 500)'),
480
- }, async ({ type, limit }) => {
481
- const data = (await fetchJSON('/security/iocs.json'));
482
- const typeWanted = type?.toLowerCase();
483
- const filtered = data.iocs.filter((e) => !typeWanted || e.type.toLowerCase() === typeWanted);
484
- const cap = Math.min(limit ?? 50, 500);
485
- const rows = filtered.slice(0, cap);
486
- if (rows.length === 0) {
487
- return {
488
- content: [
489
- {
490
- type: 'text',
491
- text: `No honeypot IOCs matched (total in feed: ${data.total_iocs}, window ${data.window_hours}h, generated ${data.generated_at}).`,
492
- },
493
- ],
494
- };
321
+ usageText = `Total: ${usage.total_calls} calls, ${usage.total_credits_spent} credits\n\n${rows}`;
495
322
  }
496
- const body = rows
497
- .map((e, i) => `${i + 1}. [${e.type}] ${e.ioc}\n hits: ${e.hit_count} first: ${e.first_seen} last: ${e.last_seen}`)
498
- .join('\n');
499
- const header = `TensorFeed honeypot IOCs (${rows.length} of ${filtered.length} matched, ${data.total_iocs} in feed, ${data.window_hours}h window, generated ${data.generated_at}):`;
500
- const footer = `\n\nLicense: ${data.policy.license}. ${data.policy.description}`;
501
- return { content: [{ type: 'text', text: `${header}\n\n${body}${footer}` }] };
502
- });
503
- // ── Tool: check_afta_certification ──────────────────────────────────
504
- registerTool('check_afta_certification', 'Check whether a domain is AFTA-certified (Agent Fair-Trade Agreement). AFTA is an open standard: cryptographically-signed receipts, transparent pricing, no-charge guarantees for failed responses, on-chain settlement, and federated trust between participating sites. The check runs 6 deterministic probes against the target domain (e.g. tensorfeed.ai, terminalfeed.io): well-known endpoint, receipts endpoint, x402 payment manifest, pricing transparency, free trial availability, federation membership. Returns score, verdict, and which checks passed/failed.', {
505
- domain: z
506
- .string()
507
- .describe('Domain to certify (e.g. "tensorfeed.ai", "terminalfeed.io"). Bare host, no protocol.'),
508
- }, async ({ domain }) => {
509
- const data = (await fetchJSON(`/afta-certify/check?domain=${encodeURIComponent(domain)}`));
510
- const lines = data.checks.map((c) => ` ${c.passed ? 'PASS' : 'FAIL'} ${c.name}${c.details ? ` (${c.details})` : ''}`);
511
- const certified = data.afta_certified ? 'AFTA-CERTIFIED' : 'NOT certified';
512
- const next = data.next_step ? `\nNext step: ${data.next_step}` : '';
513
323
  return {
514
324
  content: [
515
325
  {
516
326
  type: 'text',
517
- text: `Domain: ${data.domain}\nScore: ${data.score}/${data.max}\nVerdict: ${data.verdict} (${certified})\nChecked: ${data.checked_at}\n\nChecks:\n${lines.join('\n')}${next}`,
327
+ text: `${balanceText}\n\nUsage:\n${usageText}`,
518
328
  },
519
329
  ],
520
330
  };
521
331
  });
522
- // ── Tool: get_agent_reputation_card ─────────────────────────────────
523
- registerTool('get_agent_reputation_card', 'Get a TensorFeed Agent Reputation Bureau card for an EVM wallet address or a tf_live_ token prefix. Cards rebuild daily at 04:50 UTC from on-chain receipts plus TF telemetry. Includes reliability score, paid-call activity, total USDC spent, longest paid streak, composite trust score (0-1), trust grade (S/A/B/C/D), flags (sybil_risk, abuse, paid_free_ratio_low), operator-claim status (wallet -> display name signed by EOA), and per-metric leaderboard ranks. Returns null/404 for unknown agents (TF only builds cards for wallets observed in production). Useful for assessing third-party agent trustworthiness before delegating high-value work.', {
524
- wallet: z
525
- .string()
526
- .optional()
527
- .describe('EVM wallet address (0x... 42 chars). Provide either this OR token_prefix.'),
528
- token_prefix: z
529
- .string()
530
- .optional()
531
- .describe('First N chars of an agent\'s tf_live_ bearer token. Useful when the agent has not signed an operator-claim yet.'),
532
- }, async ({ wallet, token_prefix }) => {
533
- if (!wallet && !token_prefix) {
332
+ registerTool('route_verdict', "TensorFeed's signed model-routing decision: the single best model for a task or named model, fused from live pricing, contamination-discounted benchmarks, real production usage, measured p95 latency, incident state, and deprecation flags, with the reasoning. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
333
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds runners-up, filters, signed receipt)."),
334
+ task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
335
+ model: z.string().optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
336
+ max_latency_p95_ms: z.number().optional().describe('Full tier only. Drop candidates whose measured p95 latency exceeds this value (ms).'),
337
+ budget: z.number().optional().describe('Full tier only. Max blended USD per 1M tokens.'),
338
+ min_quality: z.number().min(0).max(1).optional().describe('Full tier only. Minimum trust-discounted quality score in [0, 1].'),
339
+ require_operational: z.boolean().optional().describe('Full tier only. Default true. Set false to keep candidates known down or in failover.'),
340
+ exclude_deprecated: z.boolean().optional().describe('Full tier only. Default true. Set false to keep deprecated or sunsetted models.'),
341
+ }, async ({ tier, task, model, max_latency_p95_ms, budget, min_quality, require_operational, exclude_deprecated }) => {
342
+ if (!task && !model) {
534
343
  return {
535
344
  content: [
536
345
  {
537
346
  type: 'text',
538
- text: 'Provide either `wallet` (0x... EVM address) or `token_prefix` (tf_live_... token prefix). Both are accepted, one is required.',
347
+ text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a routing verdict.',
539
348
  },
540
349
  ],
541
350
  };
542
351
  }
543
- const path = wallet
544
- ? `/agents/reputation/${encodeURIComponent(wallet)}`
545
- : `/agents/reputation/by-token/${encodeURIComponent(token_prefix)}`;
546
- let data;
547
- try {
548
- data = await fetchJSON(path);
549
- }
550
- catch (e) {
551
- const msg = e instanceof Error ? e.message : String(e);
552
- if (msg.includes('404') || msg.toLowerCase().includes('not_found')) {
553
- return {
554
- content: [
555
- {
556
- type: 'text',
557
- text: `No reputation card for ${wallet ?? token_prefix}. TF only builds cards for wallets observed in production; if this is a real agent that has paid TF before, it should appear in the next daily rebuild (04:50 UTC).`,
558
- },
559
- ],
560
- };
561
- }
562
- throw e;
563
- }
564
- // The card schema is rich; select the documented top-level fields and
565
- // clamp any array so a large or inflated upstream card cannot blow the
566
- // agent's context. Unknown fields still pass through (so the agent can
567
- // reason over them) but every array is capped and the whole payload is
568
- // bounded by the downstream sanitize char cap as a backstop.
569
- const rawCard = data.card ?? data;
570
- const MAX_CARD_ARRAY = 25;
571
- const clamped = {};
572
- for (const [k, v] of Object.entries(rawCard)) {
573
- if (Array.isArray(v) && v.length > MAX_CARD_ARRAY) {
574
- clamped[k] = [...v.slice(0, MAX_CARD_ARRAY), `...[${v.length - MAX_CARD_ARRAY} more omitted]`];
575
- }
576
- else {
577
- clamped[k] = v;
578
- }
579
- }
580
- return {
581
- content: [
582
- {
583
- type: 'text',
584
- text: `Agent reputation card for ${wallet ?? `token_prefix=${token_prefix}`}:\n\n${JSON.stringify(clamped, null, 2)}`,
585
- },
586
- ],
587
- };
588
- });
589
- // ════════════════════════════════════════════════════════════════════
590
- // PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
591
- // ════════════════════════════════════════════════════════════════════
592
- // ── Tool: get_account_balance ───────────────────────────────────────
593
- registerTool('get_account_balance', 'Check the credit balance for the configured TensorFeed bearer token. Free, but requires TENSORFEED_TOKEN to be set.', {}, async () => {
594
- const data = (await fetchJSON('/payment/balance', { auth: true }));
595
- return {
596
- content: [
597
- {
598
- type: 'text',
599
- text: `Balance: ${data.balance} credits\nTotal purchased: ${data.total_purchased}\nCreated: ${data.created}\nLast used: ${data.last_used}`,
600
- },
601
- ],
602
- };
603
- });
604
- // ── Tool: get_account_usage ─────────────────────────────────────────
605
- registerTool('get_account_usage', 'Show per-endpoint usage for the configured TensorFeed token (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
606
- const data = (await fetchJSON('/payment/usage', { auth: true }));
607
- if (data.total_calls === 0) {
608
- return { content: [{ type: 'text', text: 'No premium API calls on this token yet.' }] };
609
- }
610
- const rows = Object.entries(data.by_endpoint)
611
- .sort(([, a], [, b]) => b.calls - a.calls)
612
- .map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
613
- .join('\n');
614
- return {
615
- content: [
616
- {
617
- type: 'text',
618
- text: `Total: ${data.total_calls} calls, ${data.total_credits_spent} credits\n\n${rows}`,
619
- },
620
- ],
621
- };
622
- });
623
- // ── Tool: premium_routing (1 credit) ────────────────────────────────
624
- registerTool('premium_routing', 'Get a ranked list of recommended AI models for a task (code, reasoning, creative, or general), each with a composite score plus the quality, availability, cost, and latency components that produced it, and the per-model input/output pricing. Constrain with budget, min_quality, and top_n so the ranking reflects your actual limits instead of a generic leaderboard. Costs 1 credit ($0.02), billed against your TENSORFEED_TOKEN. For a single signed routing decision with reasoning, see route_verdict.', {
625
- task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type the model needs to be good at (default: general)'),
626
- budget: z.number().optional().describe('Max blended USD per 1M tokens'),
627
- min_quality: z.number().min(0).max(1).optional().describe('Minimum quality score in [0, 1]'),
628
- top_n: z.number().min(1).max(10).optional().describe('How many models to return (default 5)'),
629
- }, async ({ task, budget, min_quality, top_n }) => {
630
352
  const params = new URLSearchParams();
631
353
  if (task)
632
354
  params.set('task', task);
633
- if (typeof budget === 'number')
634
- params.set('budget', String(budget));
635
- if (typeof min_quality === 'number')
636
- params.set('min_quality', String(min_quality));
637
- if (typeof top_n === 'number')
638
- params.set('top_n', String(top_n));
639
- const data = (await fetchJSON(`/premium/routing?${params}`, { auth: true }));
640
- const list = data.recommendations
641
- .map(r => {
642
- const c = r.components;
643
- return ` #${r.rank} ${r.model.name} (${r.model.provider}) score=${r.composite_score.toFixed(3)}\n in $${r.pricing.input}/1M, out $${r.pricing.output}/1M\n quality=${c.quality.toFixed(2)} avail=${c.availability.toFixed(2)} cost=${c.cost.toFixed(2)} latency=${c.latency.toFixed(2)}`;
644
- })
645
- .join('\n\n');
646
- const billing = data.billing
647
- ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
648
- : '';
649
- return { content: [{ type: 'text', text: `Routing for "${data.task}":\n\n${list}${billing}` }] };
650
- });
651
- registerTool('route_verdict_preview', "Free. TensorFeed's signed routing decision that fuses live pricing, contamination-discounted benchmarks, real production usage, MEASURED p95 latency, incident state, and deprecation flags into the single best model for a task or a named model, with the reasoning. For ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit, use route_verdict. 10 calls per day per IP.", {
652
- task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
653
- model: z.string().optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
654
- }, async ({ task, model }) => {
655
- if (!task && !model) {
355
+ if (model)
356
+ params.set('model', model);
357
+ if ((tier ?? 'preview') === 'full') {
358
+ if (typeof max_latency_p95_ms === 'number')
359
+ params.set('max_latency_p95_ms', String(max_latency_p95_ms));
360
+ if (typeof budget === 'number')
361
+ params.set('budget', String(budget));
362
+ if (typeof min_quality === 'number')
363
+ params.set('min_quality', String(min_quality));
364
+ if (typeof require_operational === 'boolean')
365
+ params.set('require_operational', String(require_operational));
366
+ if (typeof exclude_deprecated === 'boolean')
367
+ params.set('exclude_deprecated', String(exclude_deprecated));
368
+ const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
369
+ if (!data.verdict) {
370
+ return {
371
+ content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
372
+ };
373
+ }
374
+ const v = data.verdict;
375
+ const t = data.trust;
376
+ const f = data.filters_applied;
377
+ const runners = (data.runners_up ?? [])
378
+ .map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
379
+ .join('\n');
380
+ const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
381
+ const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
382
+ const billing = data.billing
383
+ ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
384
+ : '';
656
385
  return {
657
386
  content: [
658
387
  {
659
388
  type: 'text',
660
- text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a routing verdict.',
389
+ text: `Route Verdict: ${v.model.name} (${v.model.provider}) score ${v.composite_score}\n` +
390
+ `Why: ${v.why}\n` +
391
+ `Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n\n` +
392
+ `Runners-up:\n${runners || ' (none)'}\n\n` +
393
+ `${trustLine}\n${filterLine}\n\nClaim: ${data.claim}${billing}`,
661
394
  },
662
395
  ],
663
396
  };
664
397
  }
665
- const params = new URLSearchParams();
666
- if (task)
667
- params.set('task', task);
668
- if (model)
669
- params.set('model', model);
670
398
  const data = (await fetchJSON(`/preview/route-verdict?${params}`));
671
399
  if (!data.verdict) {
672
400
  return {
@@ -692,84 +420,15 @@ registerTool('route_verdict_preview', "Free. TensorFeed's signed routing decisio
692
420
  `Why: ${v.why}\n` +
693
421
  `Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n` +
694
422
  `${trustLine}${fresh ? `\nData freshness: ${fresh}` : ''}\n` +
695
- `Upgrade: route_verdict (1 credit, $0.02) adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`,
696
- },
697
- ],
698
- };
699
- });
700
- // ── Tool: route_verdict (1 credit) ──────────────────────────────────
701
- // The signed routing decision. Single best model plus ranked runners-up
702
- // for a task or named model, with constraint filters and an AFTA-signed
703
- // receipt over the exact inputs. Strict premium, no free trial. The free
704
- // route_verdict_preview is the no-auth taste of the same engine.
705
- registerTool('route_verdict', "Costs 1 credit ($0.02). The signed routing decision: TensorFeed's single best model plus ranked runners-up for your task or a named model, fused from live pricing, contamination-discounted benchmarks, real production usage, MEASURED p95 latency, incident state, and deprecation flags, with an AFTA-signed receipt over the exact inputs so you can prove why you routed. Versus the free route_verdict_preview it adds ranked runners-up, constraint filters (max p95 latency, budget, min quality, operational-only, exclude-deprecated), the signed receipt, and no rate limit. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
706
- task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
707
- model: z.string().optional().describe('Model id or display name to narrow the verdict to one model. Provide task or model.'),
708
- max_latency_p95_ms: z.number().optional().describe('Drop candidates whose measured p95 latency exceeds this value (ms).'),
709
- budget: z.number().optional().describe('Max blended USD per 1M tokens'),
710
- min_quality: z.number().min(0).max(1).optional().describe('Minimum trust-discounted quality score in [0, 1]'),
711
- require_operational: z.boolean().optional().describe('Default true. Set false to keep candidates known down or in failover.'),
712
- exclude_deprecated: z.boolean().optional().describe('Default true. Set false to keep deprecated or sunsetted models.'),
713
- }, async ({ task, model, max_latency_p95_ms, budget, min_quality, require_operational, exclude_deprecated }) => {
714
- if (!task && !model) {
715
- return {
716
- content: [
717
- {
718
- type: 'text',
719
- text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a signed routing verdict.',
720
- },
721
- ],
722
- };
723
- }
724
- const params = new URLSearchParams();
725
- if (task)
726
- params.set('task', task);
727
- if (model)
728
- params.set('model', model);
729
- if (typeof max_latency_p95_ms === 'number')
730
- params.set('max_latency_p95_ms', String(max_latency_p95_ms));
731
- if (typeof budget === 'number')
732
- params.set('budget', String(budget));
733
- if (typeof min_quality === 'number')
734
- params.set('min_quality', String(min_quality));
735
- if (typeof require_operational === 'boolean')
736
- params.set('require_operational', String(require_operational));
737
- if (typeof exclude_deprecated === 'boolean')
738
- params.set('exclude_deprecated', String(exclude_deprecated));
739
- const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
740
- if (!data.verdict) {
741
- return {
742
- content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
743
- };
744
- }
745
- const v = data.verdict;
746
- const t = data.trust;
747
- const f = data.filters_applied;
748
- const runners = (data.runners_up ?? [])
749
- .map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
750
- .join('\n');
751
- const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
752
- const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
753
- const billing = data.billing
754
- ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
755
- : '';
756
- return {
757
- content: [
758
- {
759
- type: 'text',
760
- text: `Route Verdict: ${v.model.name} (${v.model.provider}) score ${v.composite_score}\n` +
761
- `Why: ${v.why}\n` +
762
- `Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n\n` +
763
- `Runners-up:\n${runners || ' (none)'}\n\n` +
764
- `${trustLine}\n${filterLine}\n\nClaim: ${data.claim}${billing}`,
423
+ `Upgrade: route_verdict tier='full' (1 credit, $0.02) adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`,
765
424
  },
766
425
  ],
767
426
  };
768
427
  });
769
428
  // ════════════════════════════════════════════════════════════════════
770
- // Verdict family: 7 signed decisions, each a free preview tool plus a
771
- // 1-credit premium tool. Each preview calls fetchJSON('/preview/<x>')
772
- // (no auth, 10 calls per IP per day); each premium calls
429
+ // Verdict family: 7 signed decisions, each a single tier-parameterized
430
+ // tool. tier='preview' (default) calls fetchJSON('/preview/<x>') (no
431
+ // auth, 10 calls per IP per day); tier='full' calls
773
432
  // fetchJSON('/premium/<x>', { auth: true }) and burns 1 credit. The
774
433
  // shapes below mirror the worker builder result interfaces exactly.
775
434
  // ════════════════════════════════════════════════════════════════════
@@ -778,7 +437,7 @@ registerTool('route_verdict', "Costs 1 credit ($0.02). The signed routing decisi
778
437
  // paid tier adds over the free preview, `rl` is the optional remaining
779
438
  // calls suffix.
780
439
  function verdictUpsell(tool, adds, rl) {
781
- return (`Upgrade: ${tool} (1 credit, $0.02) adds ${adds} and an AFTA-signed receipt. ` +
440
+ return (`Upgrade: call ${tool} again with tier="full" (1 credit, $0.02) for ${adds} and an AFTA-signed receipt. ` +
782
441
  `No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`);
783
442
  }
784
443
  function verdictBilling(billing) {
@@ -787,7 +446,32 @@ function verdictBilling(billing) {
787
446
  function previewRateLine(rate) {
788
447
  return rate ? ` Preview: ${rate.remaining} of ${rate.limit} calls left today.` : '';
789
448
  }
790
- registerTool('provider_reliability_verdict_preview', "Free. TensorFeed's signed dependability ruling over its OWN measured latency and availability probes of the frontier AI providers. Names the single most-dependable provider to build on and the riskiest, scoring availability and tail consistency (p50 over p95) equally because an agent retry loop feels the tail, not the median. For the full per-provider ranking and an AFTA-signed receipt, use provider_reliability_verdict. 10 calls per day per IP.", {}, async () => {
449
+ registerTool('provider_reliability_verdict', "TensorFeed's signed dependability ruling over its OWN measured latency and availability probes of the frontier AI providers: the single most-dependable provider to build on and the riskiest, scoring availability and tail consistency (p50 over p95) equally because an agent retry loop feels the tail, not the median. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds the full per-provider ranking with measured availability and p50/p95/p99 and tail spread plus an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
450
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the full per-provider ranking and signed receipt)."),
451
+ }, async ({ tier }) => {
452
+ if ((tier ?? 'preview') === 'full') {
453
+ const data = (await fetchJSON('/premium/provider-reliability-verdict', { auth: true }));
454
+ const v = data.verdict;
455
+ if (!v || !v.most_dependable) {
456
+ return { content: [{ type: 'text', text: `No reliability verdict available right now.\n${data.claim ?? ''}` }] };
457
+ }
458
+ const ranking = (data.ranking ?? [])
459
+ .map((r) => ` #${r.rank} ${r.provider}: ${r.note}`)
460
+ .join('\n');
461
+ return {
462
+ content: [
463
+ {
464
+ type: 'text',
465
+ text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
466
+ (v.riskiest ? `, riskiest ${v.riskiest}` : '') +
467
+ '\n\n' +
468
+ `Ranking:\n${ranking || ' (none)'}\n\n` +
469
+ `Claim: ${data.claim}` +
470
+ verdictBilling(data.billing),
471
+ },
472
+ ],
473
+ };
474
+ }
791
475
  const data = (await fetchJSON('/preview/provider-reliability-verdict'));
792
476
  const v = data.verdict;
793
477
  if (!v || !v.most_dependable) {
@@ -811,31 +495,39 @@ registerTool('provider_reliability_verdict_preview', "Free. TensorFeed's signed
811
495
  ],
812
496
  };
813
497
  });
814
- // ── Tool: provider_reliability_verdict (1 credit) ───────────────────
815
- registerTool('provider_reliability_verdict', "Costs 1 credit ($0.02). The signed provider dependability ruling over TensorFeed's OWN measured probes: ranks every probed frontier provider by availability and tail consistency (p50 over p95), names the most dependable and the riskiest, and ships the full per-provider ranking with an AFTA-signed receipt over the measurements. Versus the free provider_reliability_verdict_preview it adds the complete ranking with each provider's measured availability, p50/p95/p99, and tail spread, plus the receipt and no rate limit. 30-minute freshness SLA, no-charge when stale. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {}, async () => {
816
- const data = (await fetchJSON('/premium/provider-reliability-verdict', { auth: true }));
817
- const v = data.verdict;
818
- if (!v || !v.most_dependable) {
819
- return { content: [{ type: 'text', text: `No reliability verdict available right now.\n${data.claim ?? ''}` }] };
498
+ registerTool('x402_settlement_verdict', "TensorFeed's signed ruling on the state of the x402 USDC settlement market on Base, computed over its OWN on-chain settlement index: market momentum versus the prior window of equal length, concentration, and the leading publisher. Covers the publishers TensorFeed indexes on Base, forward-only from launch. tier='preview' (default) is free (10 calls per day per IP), headline verdict only. tier='full' costs 1 credit ($0.02), adds the full per-publisher ranking with volume share, ecosystem totals, the Herfindahl concentration index, an optional window, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
499
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-publisher ranking, totals, window, signed receipt)."),
500
+ window: z.enum(['24h', '7d', '30d']).optional().describe('Full tier only. Settlement window to rule over (24h, 7d, 30d). Default 7d.'),
501
+ }, async ({ tier, window }) => {
502
+ if ((tier ?? 'preview') === 'full') {
503
+ const params = new URLSearchParams();
504
+ if (window)
505
+ params.set('window', window);
506
+ const qs = params.toString();
507
+ const data = (await fetchJSON(`/premium/x402-settlement-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
508
+ const v = data.verdict;
509
+ const e = data.ecosystem;
510
+ const ranking = (data.ranking ?? [])
511
+ .map((r) => ` #${r.rank} ${r.domain}: ${r.volume_usdc} USDC over ${r.count} settlements (${r.share_pct}% share)`)
512
+ .join('\n');
513
+ return {
514
+ content: [
515
+ {
516
+ type: 'text',
517
+ text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
518
+ (v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
519
+ '\n' +
520
+ `Ecosystem: ${e.volume_usdc} USDC over ${e.count} settlements across ${e.unique_publishers} publishers` +
521
+ (e.top_publisher_share_pct !== null ? `, top share ${e.top_publisher_share_pct}%` : '') +
522
+ (e.hhi !== null ? `, HHI ${e.hhi}` : '') +
523
+ '\n\n' +
524
+ `Ranking:\n${ranking || ' (none)'}\n\n` +
525
+ `Claim: ${data.claim}` +
526
+ verdictBilling(data.billing),
527
+ },
528
+ ],
529
+ };
820
530
  }
821
- const ranking = (data.ranking ?? [])
822
- .map((r) => ` #${r.rank} ${r.provider}: ${r.note}`)
823
- .join('\n');
824
- return {
825
- content: [
826
- {
827
- type: 'text',
828
- text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
829
- (v.riskiest ? `, riskiest ${v.riskiest}` : '') +
830
- '\n\n' +
831
- `Ranking:\n${ranking || ' (none)'}\n\n` +
832
- `Claim: ${data.claim}` +
833
- verdictBilling(data.billing),
834
- },
835
- ],
836
- };
837
- });
838
- registerTool('x402_settlement_verdict_preview', "Free. TensorFeed's signed ruling on the state of the x402 USDC settlement market on Base, computed over its OWN on-chain settlement index: market momentum versus the prior window, concentration, and the leading publisher. For the full per-publisher ranking, ecosystem totals, the Herfindahl concentration index, an optional window, and an AFTA-signed receipt, use x402_settlement_verdict. Covers the publishers TensorFeed indexes on Base, forward-only from launch. 10 calls per day per IP.", {}, async () => {
839
531
  const data = (await fetchJSON('/preview/x402-settlement-verdict'));
840
532
  const v = data.verdict;
841
533
  const rl = previewRateLine(data.rate_limit);
@@ -854,46 +546,34 @@ registerTool('x402_settlement_verdict_preview', "Free. TensorFeed's signed rulin
854
546
  ],
855
547
  };
856
548
  });
857
- // ── Tool: x402_settlement_verdict (1 credit) ────────────────────────
858
- registerTool('x402_settlement_verdict', "Costs 1 credit ($0.02). The signed ruling on the Base x402 USDC settlement market over TensorFeed's OWN settlement index: momentum versus the prior window of equal length, concentration by the Herfindahl index over publisher volume share, and the leading publisher, plus the full per-publisher ranking and an AFTA-signed receipt. Versus the free x402_settlement_verdict_preview it adds the per-publisher ranking with volume share, the ecosystem totals and concentration index, an optional window, and the receipt, with no rate limit. 10-minute freshness SLA, no-charge when stale. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
859
- window: z.enum(['24h', '7d', '30d']).optional().describe('Settlement window to rule over (24h, 7d, 30d). Default 7d.'),
860
- }, async ({ window }) => {
861
- const params = new URLSearchParams();
862
- if (window)
863
- params.set('window', window);
864
- const qs = params.toString();
865
- const data = (await fetchJSON(`/premium/x402-settlement-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
866
- const v = data.verdict;
867
- const e = data.ecosystem;
868
- const ranking = (data.ranking ?? [])
869
- .map((r) => ` #${r.rank} ${r.domain}: ${r.volume_usdc} USDC over ${r.count} settlements (${r.share_pct}% share)`)
870
- .join('\n');
871
- return {
872
- content: [
873
- {
874
- type: 'text',
875
- text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
876
- (v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
877
- '\n' +
878
- `Ecosystem: ${e.volume_usdc} USDC over ${e.count} settlements across ${e.unique_publishers} publishers` +
879
- (e.top_publisher_share_pct !== null ? `, top share ${e.top_publisher_share_pct}%` : '') +
880
- (e.hhi !== null ? `, HHI ${e.hhi}` : '') +
881
- '\n\n' +
882
- `Ranking:\n${ranking || ' (none)'}\n\n` +
883
- `Claim: ${data.claim}` +
884
- verdictBilling(data.billing),
885
- },
886
- ],
887
- };
888
- });
889
- // ── Tool: x402_publisher_verdict_preview (free, 10/IP/day) ───────────
890
- registerTool('x402_publisher_verdict_preview', "Free. TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. For the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence, and an AFTA-signed receipt, use x402_publisher_verdict. Requires a domain. 10 calls per day per IP.", {
549
+ // ── Tool: x402_publisher_verdict (preview free / full 1 credit) ──────
550
+ registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. Requires a domain. tier='preview' (default) is free (10 calls per day per IP), the trust verdict only. tier='full' costs 1 credit ($0.02), adds the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence (volume, count, last settled), and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
551
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds momentum, shared-wallet flag, evidence, signed receipt)."),
891
552
  domain: z.string().describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
892
- }, async ({ domain }) => {
553
+ }, async ({ tier, domain }) => {
893
554
  if (!domain || !domain.trim()) {
894
555
  return { content: [{ type: 'text', text: 'Provide a publisher domain (e.g. domain "x402.tavily.com") to get a trust verdict.' }] };
895
556
  }
896
557
  const params = new URLSearchParams({ domain: domain.trim() });
558
+ if ((tier ?? 'preview') === 'full') {
559
+ const data = (await fetchJSON(`/premium/x402-publisher-verdict?${params}`, { auth: true }));
560
+ const t = data.trust;
561
+ const ev = data.evidence;
562
+ return {
563
+ content: [
564
+ {
565
+ type: 'text',
566
+ text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
567
+ `Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
568
+ (t.last_settled ? `. Last settled ${t.last_settled}` : '') +
569
+ '\n' +
570
+ `Evidence (${ev.window_days}d): ${ev.volume_usdc} USDC over ${ev.count} settlements\n` +
571
+ `Claim: ${data.claim}` +
572
+ verdictBilling(data.billing),
573
+ },
574
+ ],
575
+ };
576
+ }
897
577
  const data = (await fetchJSON(`/preview/x402-publisher-verdict?${params}`));
898
578
  const rl = previewRateLine(data.rate_limit);
899
579
  return {
@@ -909,39 +589,38 @@ registerTool('x402_publisher_verdict_preview', "Free. TensorFeed's signed trust
909
589
  ],
910
590
  };
911
591
  });
912
- // ── Tool: x402_publisher_verdict (1 credit) ─────────────────────────
913
- registerTool('x402_publisher_verdict', "Costs 1 credit ($0.02). The signed trust verdict on a single x402 publisher over TensorFeed's OWN on-chain settlement index: whether its Base payTo is actively settling, its 30-day settlement momentum, a shared-wallet risk flag, and the settlement evidence, with an AFTA-signed receipt. Versus the free x402_publisher_verdict_preview it adds the momentum band, the shared-wallet flag, and the settlement evidence (volume, count, last settled, daily series), with no rate limit. A not-indexed domain is no-charge. 10-minute freshness SLA. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
914
- domain: z.string().describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
915
- }, async ({ domain }) => {
916
- if (!domain || !domain.trim()) {
917
- return { content: [{ type: 'text', text: 'Provide a publisher domain (e.g. domain "x402.tavily.com") to get a signed trust verdict.' }] };
918
- }
919
- const params = new URLSearchParams({ domain: domain.trim() });
920
- const data = (await fetchJSON(`/premium/x402-publisher-verdict?${params}`, { auth: true }));
921
- const t = data.trust;
922
- const ev = data.evidence;
923
- return {
924
- content: [
925
- {
926
- type: 'text',
927
- text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
928
- `Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
929
- (t.last_settled ? `. Last settled ${t.last_settled}` : '') +
930
- '\n' +
931
- `Evidence (${ev.window_days}d): ${ev.volume_usdc} USDC over ${ev.count} settlements\n` +
932
- `Claim: ${data.claim}` +
933
- verdictBilling(data.billing),
934
- },
935
- ],
936
- };
937
- });
938
- registerTool('stack_safety_verdict_preview', "Free. TensorFeed's deploy gate for an AI software stack: pass each package name (comma-separated name@version) and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. For the matched-CVE evidence (ids, ranges, fixes, KEV status), up to 10 packages, and an AFTA-signed receipt, use stack_safety_verdict. Capped at 3 packages, 10 calls per day per IP.", {
939
- packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3.'),
940
- }, async ({ packages }) => {
941
- if (!packages || !packages.trim()) {
942
- return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
592
+ registerTool('stack_safety_verdict', "TensorFeed's deploy gate for an AI software stack: pass each package as comma-separated name@version and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. tier='preview' (default) is free (10 calls per day per IP), caps at 3 packages, gate plus worst offender. tier='full' costs 1 credit ($0.02), raises the cap to 10 packages, adds the matched-CVE evidence (ids, affected ranges, fixed versions, KEV status) and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
593
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free, caps at 3 packages) or 'full' (1 credit; up to 10 packages, matched-CVE evidence, signed receipt)."),
594
+ packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3, full up to 10.'),
595
+ }, async ({ tier, packages }) => {
596
+ if (!packages || !packages.trim()) {
597
+ return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
943
598
  }
944
599
  const params = new URLSearchParams({ packages: packages.trim() });
600
+ if ((tier ?? 'preview') === 'full') {
601
+ const data = (await fetchJSON(`/premium/stack-safety-verdict?${params}`, { auth: true }));
602
+ const c = data.counts;
603
+ const lines = (data.packages ?? [])
604
+ .map((p) => {
605
+ const cves = p.matched_cves
606
+ .map((m) => `${m.cve_id}${m.on_kev ? ' (KEV)' : ''} ${m.severity_label}`)
607
+ .join('; ');
608
+ return ` ${p.package}${p.version ? `@${p.version}` : ''}: ${p.verdict}. ${p.reason}` + (cves ? `\n CVEs: ${cves}` : '');
609
+ })
610
+ .join('\n');
611
+ return {
612
+ content: [
613
+ {
614
+ type: 'text',
615
+ text: `Stack Safety Verdict: gate ${data.gate}\n` +
616
+ `Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n\n` +
617
+ `${lines || ' (no packages)'}\n\n` +
618
+ `Claim: ${data.claim}` +
619
+ verdictBilling(data.billing),
620
+ },
621
+ ],
622
+ };
623
+ }
945
624
  const data = (await fetchJSON(`/preview/stack-safety-verdict?${params}`));
946
625
  const c = data.counts;
947
626
  const worst = (data.packages ?? []).find((p) => p.verdict === 'BLOCK') ?? (data.packages ?? []).find((p) => p.verdict === 'HOLD') ?? null;
@@ -962,46 +641,39 @@ registerTool('stack_safety_verdict_preview', "Free. TensorFeed's deploy gate for
962
641
  ],
963
642
  };
964
643
  });
965
- registerTool('stack_safety_verdict', "Costs 1 credit ($0.02). The signed deploy gate for an AI software stack (up to 10 packages): the overall BLOCK / HOLD / PASS / UNKNOWN gate and a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog, plus the matched-CVE evidence and an AFTA-signed receipt. Versus the free stack_safety_verdict_preview it adds the matched-CVE ids, affected ranges, fixed versions, and KEV status per package, raises the cap to 10 packages, and ships the receipt, with no rate limit. Never-false-confirm: BLOCK only on exploited with no fix. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
966
- packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Up to 10.'),
967
- }, async ({ packages }) => {
968
- if (!packages || !packages.trim()) {
969
- return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a signed deploy gate.' }] };
970
- }
971
- const params = new URLSearchParams({ packages: packages.trim() });
972
- const data = (await fetchJSON(`/premium/stack-safety-verdict?${params}`, { auth: true }));
973
- const c = data.counts;
974
- const lines = (data.packages ?? [])
975
- .map((p) => {
976
- const cves = p.matched_cves
977
- .map((m) => `${m.cve_id}${m.on_kev ? ' (KEV)' : ''} ${m.severity_label}`)
978
- .join('; ');
979
- return ` ${p.package}${p.version ? `@${p.version}` : ''}: ${p.verdict}. ${p.reason}` + (cves ? `\n CVEs: ${cves}` : '');
980
- })
981
- .join('\n');
982
- return {
983
- content: [
984
- {
985
- type: 'text',
986
- text: `Stack Safety Verdict: gate ${data.gate}\n` +
987
- `Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n\n` +
988
- `${lines || ' (no packages)'}\n\n` +
989
- `Claim: ${data.claim}` +
990
- verdictBilling(data.billing),
991
- },
992
- ],
993
- };
994
- });
995
- registerTool('benchmark_trust_verdict_preview', "Free. TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted. Returns a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. For the per-signal detail (ceiling proximity, frontier compression, contamination) and the down-weight recommendation with an alternative, use benchmark_trust_verdict. 10 calls per day per IP.", {
644
+ registerTool('benchmark_trust_verdict', "TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted: a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. tier='preview' (default) is free (10 calls per day per IP), top verdict and bands only. tier='full' costs 1 credit ($0.02), adds the per-signal detail (ceiling proximity, frontier compression, contamination), a down-weight recommendation with an alternative benchmark, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
645
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-signal detail, recommendation, signed receipt)."),
996
646
  benchmark: z.string().optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
997
647
  category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
998
- }, async ({ benchmark, category }) => {
648
+ }, async ({ tier, benchmark, category }) => {
999
649
  const params = new URLSearchParams();
1000
650
  if (benchmark)
1001
651
  params.set('benchmark', benchmark);
1002
652
  if (category)
1003
653
  params.set('category', category);
1004
654
  const qs = params.toString();
655
+ if ((tier ?? 'preview') === 'full') {
656
+ const data = (await fetchJSON(`/premium/benchmark-trust-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
657
+ const verdicts = data.verdicts ?? [];
658
+ if (verdicts.length === 0) {
659
+ return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
660
+ }
661
+ const lines = verdicts
662
+ .slice(0, 8)
663
+ .map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100. Ceiling ${v.signals.ceiling_proximity}, frontier ${v.signals.frontier_compression}.\n ${v.recommendation}`)
664
+ .join('\n');
665
+ return {
666
+ content: [
667
+ {
668
+ type: 'text',
669
+ text: `Benchmark Trust Verdict: ${data.count} benchmark${data.count === 1 ? '' : 's'} ruled on\n\n` +
670
+ `${lines}\n\n` +
671
+ `Claim: ${data.claim}` +
672
+ verdictBilling(data.billing),
673
+ },
674
+ ],
675
+ };
676
+ }
1005
677
  const data = (await fetchJSON(`/preview/benchmark-trust-verdict${qs ? `?${qs}` : ''}`));
1006
678
  const verdicts = data.verdicts ?? [];
1007
679
  if (verdicts.length === 0) {
@@ -1025,47 +697,55 @@ registerTool('benchmark_trust_verdict_preview', "Free. TensorFeed's signed rulin
1025
697
  ],
1026
698
  };
1027
699
  });
1028
- registerTool('benchmark_trust_verdict', "Costs 1 credit ($0.02). The signed ruling on whether an AI benchmark is a trustworthy capability signal or saturated, contaminated, or near ceiling: a trust band and 0-100 score per benchmark, the per-signal detail, a down-weight recommendation with an alternative benchmark, and an AFTA-signed receipt. Versus the free benchmark_trust_verdict_preview it adds the ceiling proximity, frontier compression, and contamination signals, the recommendation, and the receipt, with no rate limit. Pass benchmark to narrow to one, or category to filter, or neither for the registry. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
1029
- benchmark: z.string().optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
1030
- category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
1031
- }, async ({ benchmark, category }) => {
1032
- const params = new URLSearchParams();
1033
- if (benchmark)
1034
- params.set('benchmark', benchmark);
1035
- if (category)
1036
- params.set('category', category);
1037
- const qs = params.toString();
1038
- const data = (await fetchJSON(`/premium/benchmark-trust-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
1039
- const verdicts = data.verdicts ?? [];
1040
- if (verdicts.length === 0) {
1041
- return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
1042
- }
1043
- const lines = verdicts
1044
- .slice(0, 8)
1045
- .map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100. Ceiling ${v.signals.ceiling_proximity}, frontier ${v.signals.frontier_compression}.\n ${v.recommendation}`)
1046
- .join('\n');
1047
- return {
1048
- content: [
1049
- {
1050
- type: 'text',
1051
- text: `Benchmark Trust Verdict: ${data.count} benchmark${data.count === 1 ? '' : 's'} ruled on\n\n` +
1052
- `${lines}\n\n` +
1053
- `Claim: ${data.claim}` +
1054
- verdictBilling(data.billing),
1055
- },
1056
- ],
1057
- };
1058
- });
1059
- registerTool('failover_verdict_preview', "Free. Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. For the full failover candidate (pricing, measured latency, quality), the ranked alternatives, and an AFTA-signed receipt, use failover_verdict. Requires from. 10 calls per day per IP.", {
700
+ registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. Requires from. tier='preview' (default) is free (10 calls per day per IP), the failover target only. tier='full' costs 1 credit ($0.02), adds the full candidate (pricing, measured p95 latency, quality), the ranked alternatives, the confirmed incident on A, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
701
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds candidate detail, ranked alternatives, incident, signed receipt)."),
1060
702
  from: z.string().describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
1061
703
  task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to optimize the failover target for. Optional.'),
1062
- }, async ({ from, task }) => {
704
+ }, async ({ tier, from, task }) => {
1063
705
  if (!from || !from.trim()) {
1064
706
  return { content: [{ type: 'text', text: 'Provide a from provider (the degraded provider to fail over from, e.g. from "anthropic") to get a failover verdict.' }] };
1065
707
  }
1066
708
  const params = new URLSearchParams({ from: from.trim() });
1067
709
  if (task)
1068
710
  params.set('task', task);
711
+ if ((tier ?? 'preview') === 'full') {
712
+ const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
713
+ const dest = data.failover_to;
714
+ const incidentLine = data.from.incident
715
+ ? `Incident on ${data.from.provider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
716
+ : '';
717
+ if (!dest) {
718
+ return {
719
+ content: [
720
+ {
721
+ type: 'text',
722
+ text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
723
+ incidentLine +
724
+ `Why: ${data.why}\nClaim: ${data.claim}` +
725
+ verdictBilling(data.billing),
726
+ },
727
+ ],
728
+ };
729
+ }
730
+ const alts = (data.alternatives ?? [])
731
+ .map((a) => ` #${a.rank} ${a.model.name} (${a.model.provider}) blended $${a.pricing.blended}/1M, p95 ${a.latency.measured_p95_ms ?? 'n/a'}ms, status ${a.operational.status}`)
732
+ .join('\n');
733
+ return {
734
+ content: [
735
+ {
736
+ type: 'text',
737
+ text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
738
+ `Why: ${dest.why}\n` +
739
+ `Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
740
+ incidentLine +
741
+ `Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n\n` +
742
+ `Alternatives:\n${alts || ' (none)'}\n\n` +
743
+ `Claim: ${data.claim}` +
744
+ verdictBilling(data.billing),
745
+ },
746
+ ],
747
+ };
748
+ }
1069
749
  const data = (await fetchJSON(`/preview/failover-verdict?${params}`));
1070
750
  const dest = data.failover_to;
1071
751
  const rl = previewRateLine(data.rate_limit);
@@ -1096,61 +776,38 @@ registerTool('failover_verdict_preview', "Free. Provider A is degraded; TensorFe
1096
776
  ],
1097
777
  };
1098
778
  });
1099
- registerTool('failover_verdict', "Costs 1 credit ($0.02). The signed failover ruling: provider A is degraded, this is the best operational provider to fail over to for a task right now, with the full candidate (pricing, measured p95 latency, quality), the ranked alternatives, the confirmed incident on A, and an AFTA-signed receipt. Versus the free failover_verdict_preview it adds the candidate's pricing, measured latency, and quality, the ranked alternatives, and the receipt, with no rate limit. Confirms A against live incident triage and excludes any provider already in failover. 30-minute operational freshness SLA. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
1100
- from: z.string().describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
1101
- task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to optimize the failover target for. Optional.'),
1102
- }, async ({ from, task }) => {
1103
- if (!from || !from.trim()) {
1104
- return { content: [{ type: 'text', text: 'Provide a from provider (the degraded provider to fail over from, e.g. from "anthropic") to get a signed failover verdict.' }] };
779
+ // ── Tool: ssvc_verdict (preview free / full 1 credit) ────────────────
780
+ registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision for one CVE, applying the CISA SSVC Coordinator decision tree to the recorded Vulnrichment decision points (exploitation, automatable, technical impact). Requires a CVE id. tier='preview' (default) is free (10 calls per day per IP): the three decision points and the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision. tier='full' costs 1 credit ($0.02), adds the computed decision across the full low/medium/high Mission and Well-being envelope, the per-level reasoning, a live CISA KEV cross-check, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
781
+ tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the computed decision, envelope, reasoning, KEV cross-check, signed receipt)."),
782
+ cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
783
+ }, async ({ tier, cve }) => {
784
+ if (!cve || !cve.trim()) {
785
+ return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
1105
786
  }
1106
- const params = new URLSearchParams({ from: from.trim() });
1107
- if (task)
1108
- params.set('task', task);
1109
- const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
1110
- const dest = data.failover_to;
1111
- const incidentLine = data.from.incident
1112
- ? `Incident on ${data.from.provider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
1113
- : '';
1114
- if (!dest) {
787
+ const params = new URLSearchParams({ cve: cve.trim() });
788
+ if ((tier ?? 'preview') === 'full') {
789
+ const data = (await fetchJSON(`/premium/security/ssvc-verdict?${params}`, { auth: true }));
790
+ const dp = data.decision_points;
791
+ const env = data.decision_envelope;
792
+ const kev = data.kev_cross_check;
793
+ const kevLine = kev && kev.checked
794
+ ? `KEV cross-check: ${kev.kev_listed ? 'listed on CISA KEV' : 'not on KEV'}${kev.flag && kev.flag !== 'none' ? ` (${kev.flag})` : ''}`
795
+ : 'KEV cross-check: unavailable';
1115
796
  return {
1116
797
  content: [
1117
798
  {
1118
799
  type: 'text',
1119
- text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
1120
- incidentLine +
1121
- `Why: ${data.why}\nClaim: ${data.claim}` +
800
+ text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
801
+ `Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
802
+ `Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
803
+ `${kevLine}\n` +
804
+ `Tree: ${data.tree.name} ${data.tree.version}` +
805
+ (data.scored_at ? `, scored ${data.scored_at}` : '') +
1122
806
  verdictBilling(data.billing),
1123
807
  },
1124
808
  ],
1125
809
  };
1126
810
  }
1127
- const alts = (data.alternatives ?? [])
1128
- .map((a) => ` #${a.rank} ${a.model.name} (${a.model.provider}) blended $${a.pricing.blended}/1M, p95 ${a.latency.measured_p95_ms ?? 'n/a'}ms, status ${a.operational.status}`)
1129
- .join('\n');
1130
- return {
1131
- content: [
1132
- {
1133
- type: 'text',
1134
- text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
1135
- `Why: ${dest.why}\n` +
1136
- `Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
1137
- incidentLine +
1138
- `Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n\n` +
1139
- `Alternatives:\n${alts || ' (none)'}\n\n` +
1140
- `Claim: ${data.claim}` +
1141
- verdictBilling(data.billing),
1142
- },
1143
- ],
1144
- };
1145
- });
1146
- // ── Tool: ssvc_verdict_preview (free, 10/IP/day) ────────────────────
1147
- registerTool('ssvc_verdict_preview', "Free. The CISA SSVC decision points for one CVE, read from CISA's Vulnrichment record: exploitation, automatable, and technical impact, plus the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision (that is the premium ruling). For the computed SSVC decision across the full Mission and Well-being envelope, the per-level reasoning, the live KEV cross-check, and an AFTA-signed receipt, use ssvc_verdict. Requires a CVE id. 10 calls per day per IP.", {
1148
- cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
1149
- }, async ({ cve }) => {
1150
- if (!cve || !cve.trim()) {
1151
- return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
1152
- }
1153
- const params = new URLSearchParams({ cve: cve.trim() });
1154
811
  const data = (await fetchJSON(`/preview/security/ssvc-verdict?${params}`));
1155
812
  const dp = data.decision_points;
1156
813
  const rl = previewRateLine(data.rate_limit);
@@ -1170,44 +827,37 @@ registerTool('ssvc_verdict_preview', "Free. The CISA SSVC decision points for on
1170
827
  ],
1171
828
  };
1172
829
  });
1173
- // ── Tool: ssvc_verdict (1 credit) ───────────────────────────────────
1174
- registerTool('ssvc_verdict', "Costs 1 credit ($0.02). The signed SSVC patch-urgency decision for one CVE: applies the CISA SSVC Coordinator decision tree to the three recorded decision points and returns the decision (Act, Attend, Track, or Track*) across the full low/medium/high Mission and Well-being envelope, the per-level reasoning, a live CISA KEV cross-check, and an AFTA-signed receipt. Versus the free ssvc_verdict_preview it adds the computed decision, the envelope, the reasoning trace, the KEV staleness overlay, and the receipt, with no rate limit. A CVE not in Vulnrichment is no-charge. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
1175
- cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
1176
- }, async ({ cve }) => {
1177
- if (!cve || !cve.trim()) {
1178
- return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get a signed SSVC decision.' }] };
1179
- }
1180
- const params = new URLSearchParams({ cve: cve.trim() });
1181
- const data = (await fetchJSON(`/premium/security/ssvc-verdict?${params}`, { auth: true }));
1182
- const dp = data.decision_points;
1183
- const env = data.decision_envelope;
1184
- const kev = data.kev_cross_check;
1185
- const kevLine = kev && kev.checked
1186
- ? `KEV cross-check: ${kev.kev_listed ? 'listed on CISA KEV' : 'not on KEV'}${kev.flag && kev.flag !== 'none' ? ` (${kev.flag})` : ''}`
1187
- : 'KEV cross-check: unavailable';
1188
- return {
1189
- content: [
1190
- {
1191
- type: 'text',
1192
- text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
1193
- `Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
1194
- `Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
1195
- `${kevLine}\n` +
1196
- `Tree: ${data.tree.name} ${data.tree.version}` +
1197
- (data.scored_at ? `, scored ${data.scored_at}` : '') +
1198
- verdictBilling(data.billing),
1199
- },
1200
- ],
1201
- };
1202
- });
1203
- // ── Tool: pricing_series_free (free, 7-day cap) ─────────────────────
1204
- registerTool('pricing_series_free', 'Daily price points for one AI model over the last 1 to 7 days, free. For windows up to 90 days use pricing_series (1 credit).', {
1205
- model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
1206
- days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
830
+ // Series window helper: convert a requested day count into a `from` date
831
+ // (today minus days-1, UTC) for the premium date-range endpoints, which
832
+ // take from/to rather than a days shorthand.
833
+ function seriesFromDate(days) {
834
+ const d = new Date();
835
+ d.setUTCDate(d.getUTCDate() - (days - 1));
836
+ return d.toISOString().slice(0, 10);
837
+ }
838
+ // ── Tool: pricing_series (free 1 to 7 days, paid 8 to 90 days) ──────
839
+ registerTool('pricing_series', 'Daily price points for one AI model over a window. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding the min/max/delta summary over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
840
+ model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7").'),
841
+ days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
1207
842
  }, async ({ model, days }) => {
1208
- const params = new URLSearchParams({ model });
1209
- if (typeof days === 'number')
1210
- params.set('days', String(days));
843
+ const d = days ?? 7;
844
+ if (d > 7) {
845
+ const params = new URLSearchParams({ model, from: seriesFromDate(d) });
846
+ const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
847
+ const s = data.summary;
848
+ const summary = s.first && s.latest
849
+ ? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
850
+ : 'no data points in range';
851
+ return {
852
+ content: [
853
+ {
854
+ type: 'text',
855
+ text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
856
+ },
857
+ ],
858
+ };
859
+ }
860
+ const params = new URLSearchParams({ model, days: String(d) });
1211
861
  const data = (await fetchJSON(`/history/pricing/series?${params}`));
1212
862
  const s = data.summary;
1213
863
  const summary = s.first && s.latest
@@ -1217,20 +867,35 @@ registerTool('pricing_series_free', 'Daily price points for one AI model over th
1217
867
  content: [
1218
868
  {
1219
869
  type: 'text',
1220
- text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, use the pricing_series tool (1 credit).`,
870
+ text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, pass days 8 to 90 (1 credit).`,
1221
871
  },
1222
872
  ],
1223
873
  };
1224
874
  });
1225
- // ── Tool: benchmark_series_free (free, 7-day cap) ───────────────────
1226
- registerTool('benchmark_series_free', 'Daily benchmark scores for one model+benchmark over the last 1 to 7 days, free. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. For windows up to 90 days use benchmark_series (1 credit).', {
1227
- model: z.string().describe('Model id or display name'),
1228
- benchmark: z.string().describe('Benchmark key'),
1229
- days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
875
+ // ── Tool: benchmark_series (free 1 to 7 days, paid 8 to 90 days) ────
876
+ registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark over a window. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, tracking score evolution over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
877
+ model: z.string().describe('Model id or display name.'),
878
+ benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval).'),
879
+ days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
1230
880
  }, async ({ model, benchmark, days }) => {
1231
- const params = new URLSearchParams({ model, benchmark });
1232
- if (typeof days === 'number')
1233
- params.set('days', String(days));
881
+ const d = days ?? 7;
882
+ if (d > 7) {
883
+ const params = new URLSearchParams({ model, benchmark, from: seriesFromDate(d) });
884
+ const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
885
+ const s = data.summary;
886
+ const summary = s.first && s.latest
887
+ ? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
888
+ : 'no data in range';
889
+ return {
890
+ content: [
891
+ {
892
+ type: 'text',
893
+ text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
894
+ },
895
+ ],
896
+ };
897
+ }
898
+ const params = new URLSearchParams({ model, benchmark, days: String(d) });
1234
899
  const data = (await fetchJSON(`/history/benchmarks/series?${params}`));
1235
900
  const s = data.summary;
1236
901
  const summary = s.first && s.latest
@@ -1245,14 +910,31 @@ registerTool('benchmark_series_free', 'Daily benchmark scores for one model+benc
1245
910
  ],
1246
911
  };
1247
912
  });
1248
- // ── Tool: status_uptime_free (free, 7-day cap) ──────────────────────
1249
- registerTool('status_uptime_free', 'Daily uptime rollup for one provider over the last 1 to 7 days, free. For windows up to 90 days use status_uptime (1 credit).', {
1250
- provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
1251
- days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
913
+ // ── Tool: status_uptime (free 1 to 7 days, paid 8 to 90 days) ───────
914
+ registerTool('status_uptime', 'Daily uptime rollup for one provider over a window with operational/degraded/down day counts and uptime % (degraded counts as half-credit). days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding per-incident-day detail over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
915
+ provider: z.string().describe('Provider name (e.g. anthropic, openai, google).'),
916
+ days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
1252
917
  }, async ({ provider, days }) => {
1253
- const params = new URLSearchParams({ provider });
1254
- if (typeof days === 'number')
1255
- params.set('days', String(days));
918
+ const d = days ?? 7;
919
+ if (d > 7) {
920
+ const params = new URLSearchParams({ provider, from: seriesFromDate(d) });
921
+ const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
922
+ const incidents = data.incident_days.length
923
+ ? '\n\nIncident days:\n' + data.incident_days.map(d2 => ` ${d2.date}: ${d2.status}`).join('\n')
924
+ : '';
925
+ return {
926
+ content: [
927
+ {
928
+ type: 'text',
929
+ text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
930
+ ` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
931
+ incidents +
932
+ `\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
933
+ },
934
+ ],
935
+ };
936
+ }
937
+ const params = new URLSearchParams({ provider, days: String(d) });
1256
938
  const data = (await fetchJSON(`/history/status/uptime?${params}`));
1257
939
  return {
1258
940
  content: [
@@ -1263,15 +945,31 @@ registerTool('status_uptime_free', 'Daily uptime rollup for one provider over th
1263
945
  ],
1264
946
  };
1265
947
  });
1266
- // ── Tool: status_leaderboard_free (free, 7-day cap) ─────────────────
1267
- registerTool('status_leaderboard_free', 'Cross-provider uptime leaderboard, ranked by uptime % DESC. Free, 7-day cap. Computed from minute-resolution counters (~720 samples per provider per day). For 90-day windows plus incident_count and mttr_minutes per provider use status_leaderboard (1 credit).', {
1268
- days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
948
+ // ── Tool: status_leaderboard (free 1 to 7 days, paid 8 to 90 days) ──
949
+ registerTool('status_leaderboard', 'Cross-provider uptime leaderboard ranked by uptime % DESC, computed from minute-resolution counters (~720 samples per provider per day). days 1 to 7 is free; days 8 to 90 costs 3 credits ($0.06) and needs a TENSORFEED_TOKEN, adding incident_count and mttr_minutes (mean time to recover) per provider over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
950
+ days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 3 credits.'),
1269
951
  }, async ({ days }) => {
1270
- const params = new URLSearchParams();
1271
- if (typeof days === 'number')
1272
- params.set('days', String(days));
1273
- const qs = params.toString();
1274
- const data = (await fetchJSON(`/status/leaderboard${qs ? `?${qs}` : ''}`));
952
+ const d = days ?? 7;
953
+ if (d > 7) {
954
+ const params = new URLSearchParams({ from: seriesFromDate(d) });
955
+ const data = (await fetchJSON(`/premium/status/leaderboard?${params}`, { auth: true }));
956
+ if (!data.ok) {
957
+ return {
958
+ content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
959
+ };
960
+ }
961
+ const lines = (data.entries ?? []).map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% uptime, ${e.downtime_minutes}m downtime (${e.hard_down_minutes}m hard-down), ${e.incident_count ?? 0} incidents, MTTR ${e.mttr_minutes ?? 'n/a'}m`);
962
+ return {
963
+ content: [
964
+ {
965
+ type: 'text',
966
+ text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
967
+ },
968
+ ],
969
+ };
970
+ }
971
+ const params = new URLSearchParams({ days: String(d) });
972
+ const data = (await fetchJSON(`/status/leaderboard?${params}`));
1275
973
  if (!data.ok) {
1276
974
  return {
1277
975
  content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
@@ -1289,145 +987,8 @@ registerTool('status_leaderboard_free', 'Cross-provider uptime leaderboard, rank
1289
987
  ],
1290
988
  };
1291
989
  });
1292
- // ── Tool: pricing_series (1 credit) ─────────────────────────────────
1293
- registerTool('pricing_series', 'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling pricing_series_free is the discovery option.', {
1294
- model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
1295
- from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
1296
- to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
1297
- }, async ({ model, from, to }) => {
1298
- const params = new URLSearchParams({ model });
1299
- if (from)
1300
- params.set('from', from);
1301
- if (to)
1302
- params.set('to', to);
1303
- const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
1304
- const s = data.summary;
1305
- const summary = s.first && s.latest
1306
- ? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
1307
- : 'no data points in range';
1308
- return {
1309
- content: [
1310
- {
1311
- type: 'text',
1312
- text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1313
- },
1314
- ],
1315
- };
1316
- });
1317
- // ── Tool: benchmark_series (1 credit) ───────────────────────────────
1318
- registerTool('benchmark_series', 'Score evolution for a single benchmark on one AI model. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling benchmark_series_free is the discovery option. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.', {
1319
- model: z.string().describe('Model id or display name'),
1320
- benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval)'),
1321
- from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
1322
- to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
1323
- }, async ({ model, benchmark, from, to }) => {
1324
- const params = new URLSearchParams({ model, benchmark });
1325
- if (from)
1326
- params.set('from', from);
1327
- if (to)
1328
- params.set('to', to);
1329
- const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
1330
- const s = data.summary;
1331
- const summary = s.first && s.latest
1332
- ? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
1333
- : 'no data in range';
1334
- return {
1335
- content: [
1336
- {
1337
- type: 'text',
1338
- text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1339
- },
1340
- ],
1341
- };
1342
- });
1343
- // ── Tool: status_uptime (1 credit) ──────────────────────────────────
1344
- registerTool('status_uptime', 'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling status_uptime_free is the discovery option.', {
1345
- provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
1346
- from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
1347
- to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
1348
- }, async ({ provider, from, to }) => {
1349
- const params = new URLSearchParams({ provider });
1350
- if (from)
1351
- params.set('from', from);
1352
- if (to)
1353
- params.set('to', to);
1354
- const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
1355
- const incidents = data.incident_days.length
1356
- ? '\n\nIncident days:\n' + data.incident_days.map(d => ` ${d.date}: ${d.status}`).join('\n')
1357
- : '';
1358
- return {
1359
- content: [
1360
- {
1361
- type: 'text',
1362
- text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
1363
- ` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
1364
- incidents +
1365
- `\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1366
- },
1367
- ],
1368
- };
1369
- });
1370
- // ── Tool: status_leaderboard (1 credit) ─────────────────────────────
1371
- registerTool('status_leaderboard', 'Cross-provider uptime leaderboard for a custom date range up to 90 days. Same minute-resolution counter source as status_leaderboard_free, but adds incident_count and mttr_minutes (mean time to recover) per provider. Sorted by uptime % DESC. Costs 3 credits ($0.06). Strict premium, no free trial; the 7-day-capped sibling status_leaderboard_free is the discovery option.', {
1372
- from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
1373
- to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
1374
- }, async ({ from, to }) => {
1375
- const params = new URLSearchParams();
1376
- if (from)
1377
- params.set('from', from);
1378
- if (to)
1379
- params.set('to', to);
1380
- const data = (await fetchJSON(`/premium/status/leaderboard?${params}`, { auth: true }));
1381
- if (!data.ok) {
1382
- return {
1383
- content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
1384
- };
1385
- }
1386
- const lines = (data.entries ?? []).map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% uptime, ${e.downtime_minutes}m downtime (${e.hard_down_minutes}m hard-down), ${e.incident_count ?? 0} incidents, MTTR ${e.mttr_minutes ?? 'n/a'}m`);
1387
- return {
1388
- content: [
1389
- {
1390
- type: 'text',
1391
- text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1392
- },
1393
- ],
1394
- };
1395
- });
1396
- // ── Tool: premium_agents_directory (1 credit) ───────────────────────
1397
- registerTool('premium_agents_directory', 'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit ($0.02).', {
1398
- category: z.string().optional().describe('coding, research, general, creative, frameworks'),
1399
- status: z.enum(['operational', 'degraded', 'down', 'unknown']).optional(),
1400
- open_source: z.boolean().optional(),
1401
- sort: z.enum(['trending', 'alphabetical', 'status', 'price_low', 'price_high', 'news_count']).optional(),
1402
- limit: z.number().min(1).max(100).optional(),
1403
- }, async ({ category, status, open_source, sort, limit }) => {
1404
- const params = new URLSearchParams();
1405
- if (category)
1406
- params.set('category', category);
1407
- if (status)
1408
- params.set('status', status);
1409
- if (typeof open_source === 'boolean')
1410
- params.set('open_source', String(open_source));
1411
- if (sort)
1412
- params.set('sort', sort);
1413
- if (typeof limit === 'number')
1414
- params.set('limit', String(limit));
1415
- const data = (await fetchJSON(`/premium/agents/directory?${params}`, { auth: true }));
1416
- const list = data.agents
1417
- .map(a => ` ${a.name} (${a.provider}) [${a.category}] status=${a.live_status} score=${a.trending_score} news=${a.recent_news_count}` +
1418
- (a.flagship_pricing ? ` flagship=${a.flagship_pricing.model} $${a.flagship_pricing.blended}/1M` : ''))
1419
- .join('\n');
1420
- return {
1421
- content: [
1422
- {
1423
- type: 'text',
1424
- text: `Agents (sort: ${data.sort}, ${data.returned} of ${data.total}):\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1425
- },
1426
- ],
1427
- };
1428
- });
1429
990
  // ── Tool: whats_new (1 credit) ──────────────────────────────────────
1430
- registerTool('whats_new', 'Agent morning brief: pricing changes, new/removed models, status incidents, and top news from the last 1-7 days. The tool to call when your agent boots up. Costs 1 credit ($0.02).', {
991
+ registerTool('whats_new', 'Catch up on everything that changed in AI in one call: pricing moves, new and removed models, status incidents, and top news from the last 1 to 7 days, so your agent boots with current context instead of stale assumptions. Costs 1 credit ($0.02).', {
1431
992
  days: z.number().min(1).max(7).optional().describe('Window length in days (default 1)'),
1432
993
  news_limit: z.number().min(1).max(25).optional().describe('Max news headlines (default 10)'),
1433
994
  }, async ({ days, news_limit }) => {
@@ -1463,8 +1024,8 @@ registerTool('whats_new', 'Agent morning brief: pricing changes, new/removed mod
1463
1024
  };
1464
1025
  });
1465
1026
  // ── Tool: compare_models (1 credit) ─────────────────────────────────
1466
- registerTool('compare_models', 'Side-by-side comparison of 2-5 AI models. Returns pricing, benchmarks (normalized to a union of keys with null for missing scores), provider status, and recent news per model, plus rankings (cheapest blended, most context, per-benchmark leaderboard). Costs 1 credit ($0.02).', {
1467
- ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.7,GPT-5.5,Gemini 3" or "opus-4-7,gpt-5-5"'),
1027
+ registerTool('compare_models', 'Pick between models in one call: pricing, benchmarks, status, and recent news for 2 to 5 models side by side, with cheapest-blended and per-benchmark rankings, so you choose without scraping each provider. Costs 1 credit ($0.02).', {
1028
+ ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.8,GPT-5.5,Gemini 3.5 Flash" or "opus-4-8,gpt-5-5"'),
1468
1029
  }, async ({ ids }) => {
1469
1030
  const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
1470
1031
  const lines = data.models.map(m => {
@@ -1490,7 +1051,7 @@ registerTool('compare_models', 'Side-by-side comparison of 2-5 AI models. Return
1490
1051
  };
1491
1052
  });
1492
1053
  // ── Tool: provider_deepdive (1 credit) ──────────────────────────────
1493
- registerTool('provider_deepdive', 'Everything about an AI provider in one call: live status, all models with pricing + tier + benchmark scores joined in, recent news mentions, and agent traffic. Costs 3 credits ($0.06). Strict premium, no free trial. Aggregation IS the value; doing this from free endpoints would take 4 round-trips.', {
1054
+ registerTool('provider_deepdive', 'Everything about one AI provider in a single call: live status, every model with pricing, tier, and benchmarks joined in, recent news, and agent traffic, replacing about four separate lookups. Costs 3 credits ($0.06). Strict premium, no free trial.', {
1494
1055
  provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
1495
1056
  }, async ({ provider }) => {
1496
1057
  const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
@@ -1519,84 +1080,11 @@ registerTool('provider_deepdive', 'Everything about an AI provider in one call:
1519
1080
  ],
1520
1081
  };
1521
1082
  });
1522
- // ── Tool: cost_projection (1 credit) ────────────────────────────────
1523
- registerTool('cost_projection', 'Project the cost of a token-usage workload across 1-10 AI models. Returns daily/weekly/monthly/yearly totals per model and a ranking by cheapest monthly. Costs 1 credit ($0.02).', {
1524
- models: z.string().describe('One model or comma-separated list of up to 10 (e.g. "Claude Opus 4.7,GPT-5.5,Gemini 3"). Names or ids both work.'),
1525
- input_tokens_per_day: z.number().min(0).describe('Expected daily input token volume'),
1526
- output_tokens_per_day: z.number().min(0).describe('Expected daily output token volume'),
1527
- horizon: z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().describe('Primary horizon to highlight (default monthly). All four are always computed.'),
1528
- }, async ({ models, input_tokens_per_day, output_tokens_per_day, horizon }) => {
1529
- const params = new URLSearchParams({
1530
- model: models,
1531
- input_tokens_per_day: String(input_tokens_per_day),
1532
- output_tokens_per_day: String(output_tokens_per_day),
1533
- });
1534
- if (horizon)
1535
- params.set('horizon', horizon);
1536
- const data = (await fetchJSON(`/premium/cost/projection?${params}`, { auth: true }));
1537
- const lines = data.projections.map(p => {
1538
- if (!p.matched)
1539
- return ` ${p.model}: not found in pricing catalog`;
1540
- return ` ${p.model} (${p.provider}): $${p.daily.total}/day, $${p.weekly_total}/wk, $${p.monthly_total}/mo, $${p.yearly_total}/yr (in $${p.rates.input_per_1m}/1M, out $${p.rates.output_per_1m}/1M)`;
1541
- }).join('\n');
1542
- const ranked = data.ranked_cheapest_monthly.length
1543
- ? '\n\nCheapest monthly:\n' +
1544
- data.ranked_cheapest_monthly
1545
- .map((r, i) => ` #${i + 1} ${r.model} (${r.provider}): $${r.monthly_total}/mo`)
1546
- .join('\n')
1547
- : '';
1548
- return {
1549
- content: [
1550
- {
1551
- type: 'text',
1552
- text: `Workload: ${data.workload.input_tokens_per_day} input + ${data.workload.output_tokens_per_day} output tokens/day\n\n${lines}${ranked}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1553
- },
1554
- ],
1555
- };
1556
- });
1557
- // ── Tool: news_search (1 credit) ────────────────────────────────────
1558
- registerTool('news_search', 'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit ($0.02).', {
1559
- q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
1560
- from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
1561
- to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
1562
- provider: z.string().optional().describe('Substring match against source name and domain (e.g. "anthropic", "openai", "techcrunch")'),
1563
- category: z.string().optional().describe('Substring match against article categories'),
1564
- limit: z.number().min(1).max(100).optional().describe('Max results (default 25, max 100)'),
1565
- }, async ({ q, from, to, provider, category, limit }) => {
1566
- const params = new URLSearchParams();
1567
- if (q)
1568
- params.set('q', q);
1569
- if (from)
1570
- params.set('from', from);
1571
- if (to)
1572
- params.set('to', to);
1573
- if (provider)
1574
- params.set('provider', provider);
1575
- if (category)
1576
- params.set('category', category);
1577
- if (typeof limit === 'number')
1578
- params.set('limit', String(limit));
1579
- const data = (await fetchJSON(`/premium/news/search?${params}`, { auth: true }));
1580
- if (data.results.length === 0) {
1581
- return { content: [{ type: 'text', text: `No articles matched. (matched: ${data.matched})` }] };
1582
- }
1583
- const list = data.results
1584
- .map((r, i) => `${i + 1}. ${r.title} (${r.source})\n ${r.url}\n ${r.published_at} | relevance ${r.relevance}${r.matched_terms.length ? ` | terms: ${r.matched_terms.join(', ')}` : ''}\n ${r.snippet}`)
1585
- .join('\n\n');
1586
- return {
1587
- content: [
1588
- {
1589
- type: 'text',
1590
- text: `${data.returned} of ${data.matched} matches${data.query ? ` for "${data.query}"` : ''}:\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1591
- },
1592
- ],
1593
- };
1594
- });
1595
- // ── Tool: list_watches ──────────────────────────────────────────────
1596
- registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
1597
- const data = (await fetchJSON('/premium/watches', { auth: true }));
1598
- if (data.count === 0) {
1599
- return { content: [{ type: 'text', text: 'No active watches.' }] };
1083
+ // ── Tool: list_watches ──────────────────────────────────────────────
1084
+ registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
1085
+ const data = (await fetchJSON('/premium/watches', { auth: true }));
1086
+ if (data.count === 0) {
1087
+ return { content: [{ type: 'text', text: 'No active watches.' }] };
1600
1088
  }
1601
1089
  const rows = data.watches
1602
1090
  .map(w => {
@@ -1608,924 +1096,144 @@ registerTool('list_watches', 'List the active webhook watches owned by the confi
1608
1096
  .join('\n\n');
1609
1097
  return { content: [{ type: 'text', text: `${data.count} active watches:\n\n${rows}` }] };
1610
1098
  });
1611
- // ── Tool: create_price_watch (1 credit) ─────────────────────────────
1612
- registerTool('create_price_watch', 'Register a webhook watch on a model price change. Costs 1 credit ($0.02). Watch lives 90 days. Each fire is an HMAC-signed POST to callback_url.', {
1613
- model: z.string().describe('Model name (e.g. "Claude Opus 4.7")'),
1614
- field: z.enum(['inputPrice', 'outputPrice', 'blended']).describe('Which price field to watch'),
1615
- op: z.enum(['lt', 'gt', 'changes']).describe('Trigger: lt = below threshold, gt = above, changes = any change'),
1616
- threshold: z.number().optional().describe('Required when op is lt or gt; ignored for changes'),
1617
- callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
1618
- secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies'),
1619
- }, async ({ model, field, op, threshold, callback_url, secret }) => {
1620
- const body = {
1621
- spec: { type: 'price', model, field, op, ...(typeof threshold === 'number' ? { threshold } : {}) },
1622
- callback_url,
1623
- };
1624
- if (secret !== undefined)
1625
- body.secret = secret;
1626
- const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1627
- return {
1628
- content: [
1629
- {
1630
- type: 'text',
1631
- text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1632
- },
1633
- ],
1634
- };
1635
- }, CREATE_TOOL);
1636
- // ── Tool: create_status_watch (1 credit) ────────────────────────────
1637
- registerTool('create_status_watch', 'Register a webhook watch on a service status transition (e.g. anthropic becomes down). Costs 1 credit ($0.02). Watch lives 90 days.', {
1638
- provider: z.string().describe('Provider name (e.g. anthropic, openai)'),
1639
- op: z.enum(['becomes', 'changes']).describe('becomes = transitions to a specific value; changes = any transition'),
1640
- value: z.enum(['operational', 'degraded', 'down']).optional().describe('Required when op is becomes'),
1641
- callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
1642
- secret: z.string().optional().describe('Optional HMAC shared secret'),
1643
- }, async ({ provider, op, value, callback_url, secret }) => {
1644
- const body = {
1645
- spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
1646
- callback_url,
1647
- };
1648
- if (secret !== undefined)
1649
- body.secret = secret;
1650
- const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1651
- return {
1652
- content: [
1653
- {
1654
- type: 'text',
1655
- text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1656
- },
1657
- ],
1658
- };
1659
- }, CREATE_TOOL);
1660
- // ── Tool: create_digest_watch (1 credit) ────────────────────────────
1661
- registerTool('create_digest_watch', 'Register a scheduled digest webhook that fires daily or weekly with a curated summary of pricing changes (regardless of whether anything dramatic happened). Costs 1 credit ($0.02). Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.', {
1662
- cadence: z.enum(['daily', 'weekly']).describe('How often the digest fires'),
1663
- callback_url: z.string().describe('HTTPS URL to POST to when the digest fires'),
1664
- secret: z.string().optional().describe('Optional HMAC shared secret'),
1665
- }, async ({ cadence, callback_url, secret }) => {
1666
- const body = {
1667
- spec: { type: 'digest', cadence },
1668
- callback_url,
1669
- };
1670
- if (secret !== undefined)
1671
- body.secret = secret;
1672
- const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1673
- return {
1674
- content: [
1675
- {
1676
- type: 'text',
1677
- text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1678
- },
1679
- ],
1680
- };
1681
- }, CREATE_TOOL);
1682
- // ── Tool: create_leaderboard_rank_watch (1 credit) ──────────────────
1683
- registerTool('create_leaderboard_rank_watch', 'Register a webhook that fires when a provider crosses a rank threshold on the cross-provider 7-day uptime leaderboard. Rank 1 = best (highest uptime). drops_below: was rank<=N, now rank>N (got worse). rises_above: was rank>=N, now rank<N (got better). changes: any rank movement. Costs 1 credit ($0.02) at registration. Watch lives 90 days.', {
1684
- provider: z.string().describe('Provider name or slug (case-insensitive). e.g. claude, openai, gemini, bedrock, azure'),
1685
- op: z.enum(['drops_below', 'rises_above', 'changes']).describe('Trigger condition'),
1686
- threshold: z.number().int().min(1).optional().describe('Required for drops_below / rises_above. Integer rank position.'),
1687
- callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
1688
- secret: z.string().optional().describe('Optional HMAC shared secret'),
1689
- }, async ({ provider, op, threshold, callback_url, secret }) => {
1690
- const spec = { type: 'leaderboard_rank', provider, op };
1691
- if (threshold !== undefined)
1692
- spec.threshold = threshold;
1693
- const body = { spec, callback_url };
1694
- if (secret !== undefined)
1695
- body.secret = secret;
1696
- const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1697
- const desc = op === 'changes'
1698
- ? `${provider} rank changes`
1699
- : `${provider} ${op.replace('_', ' ')} #${threshold}`;
1700
- return {
1701
- content: [
1702
- {
1703
- type: 'text',
1704
- text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1705
- },
1706
- ],
1707
- };
1708
- }, CREATE_TOOL);
1709
- // ── Tool: delete_watch ──────────────────────────────────────────────
1710
- registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
1711
- watch_id: z
1712
- .string()
1713
- .regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
1714
- .max(128)
1715
- .describe('The wat_... id from create_price_watch / create_status_watch / list_watches'),
1716
- }, async ({ watch_id }) => {
1717
- await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
1718
- return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
1719
- }, DELETE_TOOL);
1720
- // ── Tool: probe_latest (free) ───────────────────────────────────────
1721
- registerTool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and availability per provider (Anthropic, OpenAI, Google, Mistral, Cohere). TensorFeed pings each provider\'s chat completion endpoint every 15 min and records time-to-first-byte, total response time, and HTTP status. Returns per-provider success rate and ttfb/total p50/p95/p99 latency. The data is unique because we measure it ourselves, not self-reported by the providers. Useful when an agent needs to pick a provider whose SLA you can verify, or to detect ongoing incidents before they hit a status page. Free, no auth.', {}, async () => {
1722
- const data = (await fetchJSON('/probe/latest'));
1723
- const s = data.summary;
1724
- if (s.providers.length === 0) {
1725
- return { content: [{ type: 'text', text: 'No probe data yet. Probing starts as soon as TensorFeed has at least one provider key configured.' }] };
1726
- }
1727
- const rows = s.providers
1728
- .sort((a, b) => b.ok_pct - a.ok_pct)
1729
- .map(p => {
1730
- const okPct = (p.ok_pct * 100).toFixed(1);
1731
- const ttfb = p.ttfb.p50 !== null ? `p50 ${p.ttfb.p50}ms / p95 ${p.ttfb.p95}ms / p99 ${p.ttfb.p99}ms` : 'no successful probes';
1732
- const total = p.total.p50 !== null ? `p50 ${p.total.p50}ms / p95 ${p.total.p95}ms / p99 ${p.total.p99}ms` : 'no successful probes';
1733
- const err = p.last_error ? `\n last error: ${p.last_error}` : '';
1734
- return ` ${p.provider}: ${okPct}% ok over ${p.count} probes\n ttfb: ${ttfb}\n total: ${total}${err}`;
1735
- })
1736
- .join('\n\n');
1737
- return {
1738
- content: [
1739
- {
1740
- type: 'text',
1741
- text: `LLM Endpoint SLA (${s.window_label}, computed ${s.computed_at})\n\n${rows}\n\nSource: TensorFeed measured probes, not self-reported.`,
1742
- },
1743
- ],
1744
- };
1745
- });
1746
- // ── Tool: probe_series (1 credit) ───────────────────────────────────
1747
- registerTool('probe_series', 'Daily SLA series for one LLM provider, measured by TensorFeed. Returns per-day count, success rate, ttfb p50/p95/p99, total p50/p95/p99, and incident-hour count across the requested window. Provider status pages are politically managed; this is the measured truth. Pairs naturally with premium_routing for picking a model whose SLA you can verify. Costs 3 credits ($0.06). Strict premium, no free trial.', {
1748
- provider: z.enum(['anthropic', 'openai', 'google', 'mistral', 'cohere']).describe('LLM provider key'),
1749
- from: z.string().optional().describe('Inclusive start YYYY-MM-DD (default: 30 days before to)'),
1750
- to: z.string().optional().describe('Inclusive end YYYY-MM-DD (default: today UTC)'),
1751
- }, async ({ provider, from, to }) => {
1752
- const params = new URLSearchParams();
1753
- params.set('provider', provider);
1754
- if (from)
1755
- params.set('from', from);
1756
- if (to)
1757
- params.set('to', to);
1758
- const data = (await fetchJSON(`/premium/probe/series?${params}`, { auth: true }));
1759
- const lines = data.points
1760
- .map(p => p.has_data
1761
- ? ` ${p.date} ${(p.ok_pct * 100).toFixed(1)}% ok ttfb p50/p95: ${p.ttfb_p50}/${p.ttfb_p95}ms total p50/p95: ${p.total_p50}/${p.total_p95}ms incidents: ${p.incident_hours}h`
1762
- : ` ${p.date} (no data)`)
1763
- .join('\n');
1764
- const overall = data.summary.overall_uptime_pct !== null
1765
- ? `${(data.summary.overall_uptime_pct * 100).toFixed(2)}%`
1766
- : 'insufficient data';
1767
- const notes = data.notes.length ? '\nNotes: ' + data.notes.join('; ') : '';
1768
- return {
1769
- content: [
1770
- {
1771
- type: 'text',
1772
- text: `${data.provider} measured SLA, ${data.from} -> ${data.to}\nOverall uptime: ${overall} (${data.summary.days_with_data} days with data, ${data.summary.days_with_incidents} with incidents)\n\n${lines}${notes}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1773
- },
1774
- ],
1775
- };
1776
- });
1777
- // ── Tool: mcp_registry_snapshot (free) ──────────────────────────────
1778
- registerTool('mcp_registry_snapshot', 'Today\'s summary of the official Model Context Protocol server registry. Returns total servers, by-status breakdown, top namespaces, and 1-day deltas (newly added, reactivated, deprecated). Captured daily at 9:30 AM UTC from registry.modelcontextprotocol.io. Useful when an agent wants to see how the MCP ecosystem is growing or detect freshly-deprecated servers it may be using. Free, no auth.', {}, async () => {
1779
- const data = (await fetchJSON('/mcp/registry/snapshot'));
1780
- const s = data.summary;
1781
- const statusLine = Object.entries(s.by_status)
1782
- .map(([k, v]) => `${k}: ${v}`)
1783
- .join(', ');
1784
- const topNs = s.top_namespaces.slice(0, 10).map(n => ` ${n.namespace}: ${n.count}`).join('\n');
1785
- const delta = s.delta_vs_yesterday
1786
- ? `Day over day: +${s.delta_vs_yesterday.added} new, -${s.delta_vs_yesterday.removed} removed, net ${s.delta_vs_yesterday.net >= 0 ? '+' : ''}${s.delta_vs_yesterday.net}`
1787
- : 'Day over day: not available (first snapshot)';
1788
- const newToday = s.new_today.count > 0
1789
- ? `\nNewly listed today (${s.new_today.count}):\n` + s.new_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
1790
- : '';
1791
- const deprecatedToday = s.deprecated_today.count > 0
1792
- ? `\nDeprecated today (${s.deprecated_today.count}):\n` + s.deprecated_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
1793
- : '';
1794
- return {
1795
- content: [
1796
- {
1797
- type: 'text',
1798
- text: `MCP Server Registry, ${s.date}\n` +
1799
- `Total servers: ${s.total_servers} (across ${s.total_versions} versioned entries)\n` +
1800
- `Status: ${statusLine}\n${delta}\n\n` +
1801
- `Top namespaces:\n${topNs}` +
1802
- newToday + deprecatedToday +
1803
- `\n\nSource: registry.modelcontextprotocol.io`,
1804
- },
1805
- ],
1806
- };
1807
- });
1808
- // ── Tool: get_ai_papers_trending (free) ─────────────────────────────
1809
- registerTool('get_ai_papers_trending', 'Daily curated AI/ML research papers ranked by citation count. Sourced from Semantic Scholar across five fan-out queries (large language model, transformer, RLHF, AI agents, diffusion model), deduped by paperId, top 30 returned. Each paper carries title, abstract, authors, year, venue, citation count, arxivId, doi, and fields of study. Refreshed daily at 11:00 UTC. Use this when an agent wants the citation-ranked top of the AI research literature. Free, no auth.', {}, async () => {
1810
- const data = (await fetchJSON('/papers/ai-trending'));
1811
- const s = data.snapshot;
1812
- const lines = s.papers.map((p, i) => {
1813
- const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
1814
- const year = p.year ? ` (${p.year})` : '';
1815
- const venue = p.venue ? ` ${p.venue}.` : '';
1816
- const arxiv = p.arxivId ? ` arXiv:${p.arxivId}` : '';
1817
- const link = p.url ? `\n ${p.url}` : '';
1818
- return `${i + 1}. ${p.title}${year}\n ${authors}.${venue} Citations: ${p.citationCount}.${arxiv}${link}`;
1819
- }).join('\n\n');
1820
- const venues = s.summary.top_venues.slice(0, 5).map(v => `${v.venue} (${v.count})`).join(', ');
1821
- return {
1822
- content: [
1823
- {
1824
- type: 'text',
1825
- text: `AI Papers, citation-ranked. ${s.date}, ${s.total_papers} papers.\n` +
1826
- (venues ? `Top venues: ${venues}\n\n` : '\n') +
1827
- lines +
1828
- `\n\nSource: Semantic Scholar Graph API. Captured ${s.capturedAt}.`,
1829
- },
1830
- ],
1831
- };
1832
- });
1833
- // ── Tool: get_arxiv_recent (free) ───────────────────────────────────
1834
- registerTool('get_arxiv_recent', 'Most recent arXiv submissions in cs.AI / cs.LG / cs.CL / cs.CV. Single Atom API call, deduped by arxivId, top 50 by submission date. The firehose pair to get_ai_papers_trending: this shows what just dropped, ai_papers_trending ranks by citation count. Each paper carries arxivId, title, abstract, authors, primary category, publication date, html and pdf URLs. Refreshed daily at 11:30 UTC. Free, no auth.', {
1835
- limit: z.number().min(1).max(50).optional().describe('Max papers to include in the rendered list (default 25, max 50)'),
1836
- }, async ({ limit }) => {
1837
- const data = (await fetchJSON('/papers/arxiv-recent'));
1838
- const s = data.snapshot;
1839
- const cap = Math.min(limit ?? 25, s.papers.length);
1840
- const lines = s.papers.slice(0, cap).map((p, i) => {
1841
- const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
1842
- const cat = p.primaryCategory ? ` [${p.primaryCategory}]` : '';
1843
- const date = p.publishedAt ? ` ${p.publishedAt.slice(0, 10)}` : '';
1844
- return `${i + 1}. ${p.title}${cat}${date}\n ${authors}\n ${p.htmlUrl}`;
1845
- }).join('\n\n');
1846
- return {
1847
- content: [
1848
- {
1849
- type: 'text',
1850
- text: `arXiv recent submissions. ${s.date}, showing ${cap} of ${s.total_papers}.\n` +
1851
- `Categories: ${s.categories_queried.join(', ')}\n\n` +
1852
- lines +
1853
- `\n\nSource: arxiv.org. Captured ${s.capturedAt}.`,
1854
- },
1855
- ],
1856
- };
1857
- });
1858
- // ── Tool: get_hf_trending (free) ────────────────────────────────────
1859
- registerTool('get_hf_trending', 'Top Hugging Face models, datasets, and Spaces. Models + datasets ranked by downloads (top 30 each); Spaces ranked by likes (top 30, since downloads is meaningless for hosted apps). Captured daily at 12:00 UTC against the public HF API (no auth). Each model entry carries id, downloads, likes, pipeline_tag, tags, lastModified, private, gated. Each space entry carries id, author, sdk (gradio/streamlit/static/docker), likes, tags, lastModified, private, runtime_stage, hardware. Use this when an agent wants the current shape of the open model + dataset + app ecosystem. Once enough daily snapshots accumulate, day-over-day deltas become a real trending signal. Free, no auth.', {
1860
- section: z.enum(['models', 'datasets', 'spaces', 'all', 'both']).optional().describe('Which to include in the rendered output (default all). "both" is accepted as alias for "models+datasets" for backward compat.'),
1861
- limit: z.number().min(1).max(30).optional().describe('Max items per section (default 15, max 30)'),
1862
- }, async ({ section, limit }) => {
1863
- const data = (await fetchJSON('/hf/trending'));
1864
- const s = data.snapshot;
1865
- const sec = section ?? 'all';
1866
- const n = Math.min(limit ?? 15, 30);
1867
- const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M` : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
1868
- const modelLines = s.models.items.slice(0, n).map((m, i) => {
1869
- const tag = m.pipeline_tag ? ` [${m.pipeline_tag}]` : '';
1870
- return `${i + 1}. ${m.id}${tag} ${fmtCount(m.downloads)} downloads, ${fmtCount(m.likes)} likes`;
1871
- }).join('\n');
1872
- const datasetLines = s.datasets.items.slice(0, n).map((d, i) => `${i + 1}. ${d.id} ${fmtCount(d.downloads)} downloads, ${fmtCount(d.likes)} likes`).join('\n');
1873
- const spaceLines = (s.spaces?.items ?? []).slice(0, n).map((sp, i) => {
1874
- const sdk = sp.sdk ? ` [${sp.sdk}]` : '';
1875
- const hw = sp.hardware ? `, hw: ${sp.hardware}` : '';
1876
- const stage = sp.runtime_stage ? `, ${sp.runtime_stage.toLowerCase()}` : '';
1877
- return `${i + 1}. ${sp.id}${sdk} ${fmtCount(sp.likes)} likes${stage}${hw}`;
1878
- }).join('\n');
1879
- const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
1880
- const namespaces = s.summary.top_namespaces.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
1881
- const sdks = (s.summary.top_space_sdks ?? []).slice(0, 5).map(t => `${t.sdk} (${t.count})`).join(', ');
1882
- let text = `Hugging Face top assets. ${s.date}.\n`;
1883
- if (pipelines)
1884
- text += `Top pipeline tags (models): ${pipelines}\n`;
1885
- if (sdks)
1886
- text += `Top Space SDKs: ${sdks}\n`;
1887
- if (namespaces)
1888
- text += `Top namespaces: ${namespaces}\n`;
1889
- const includeModels = sec === 'models' || sec === 'all' || sec === 'both';
1890
- const includeDatasets = sec === 'datasets' || sec === 'all' || sec === 'both';
1891
- const includeSpaces = sec === 'spaces' || sec === 'all';
1892
- if (includeModels)
1893
- text += `\nModels (by downloads):\n${modelLines}\n`;
1894
- if (includeDatasets)
1895
- text += `\nDatasets (by downloads):\n${datasetLines}\n`;
1896
- if (includeSpaces)
1897
- text += `\nSpaces (by likes):\n${spaceLines || ' (none returned)'}\n`;
1898
- text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
1899
- return { content: [{ type: 'text', text }] };
1900
- });
1901
- // ── Tool: get_hot_issues (free) ─────────────────────────────────────
1902
- registerTool('get_hot_issues', 'Currently-hot GitHub issues across the AI ecosystem. Five fan-out keyword-phrase searches ("large language model", "AI agent", transformer, "machine learning", LLM) for is:issue is:open archived:false comments>=10 updated within the last 7 days. Deduped by URL, top 30 by comment count. Each issue carries url, repo, number, title, author, state, comments, reactions_total, labels, created_at, updated_at, and matched_topic (the keyword phrase that found it). Refreshed daily at 12:30 UTC. Companion to get_trending_repos: that shows which AI repos are gaining stars, this shows where the active conversations are. Free, no auth.', {
1903
- limit: z.number().min(1).max(30).optional().describe('Max issues to render (default 15, max 30)'),
1904
- }, async ({ limit }) => {
1905
- const data = (await fetchJSON('/issues/hot'));
1906
- const s = data.snapshot;
1907
- const cap = Math.min(limit ?? 15, s.issues.length);
1908
- const lines = s.issues.slice(0, cap).map((i, n) => {
1909
- const meta = `${i.comments} comments, ${i.reactions_total} reactions [${i.matched_topic}]`;
1910
- const author = i.author ? ` by ${i.author}` : '';
1911
- const updated = i.updated_at ? ` updated ${i.updated_at.slice(0, 10)}` : '';
1912
- return `${n + 1}. ${i.repo}: ${i.title}\n ${meta}${updated}${author}\n ${i.url}`;
1913
- }).join('\n\n');
1914
- const repos = s.summary.top_repos.slice(0, 5).map(r => `${r.repo} (${r.count})`).join(', ');
1915
- return {
1916
- content: [
1917
- {
1918
- type: 'text',
1919
- text: `Hot AI GitHub issues. ${s.date}, showing ${cap} of ${s.total_issues}.\n` +
1920
- `Window: last ${s.recent_window_days} days, comments threshold ${s.comments_threshold}.\n` +
1921
- (repos ? `Most-active repos: ${repos}\n\n` : '\n') +
1922
- lines +
1923
- `\n\nSource: GitHub Search API. Captured ${s.capturedAt}.`,
1924
- },
1925
- ],
1926
- };
1927
- });
1928
- // ── Tool: get_reddit_trending (free) ────────────────────────────────
1929
- registerTool('get_reddit_trending', 'Currently-hot threads in 7 AI-relevant subreddits (LocalLLaMA, MachineLearning, ClaudeAI, OpenAI, singularity, artificial, AI_Agents). Stickied and NSFW posts filtered, deduped by post id, ranked by score, top 30. Each post carries id, subreddit, title, author, score, upvote_ratio, num_comments, permalink, url, created_utc, flair, is_self, is_video. Refreshed daily at 13:00 UTC. Companion to get_hot_issues: that surfaces developer conversation on GitHub, this surfaces community conversation on Reddit. Titles are sanitized at capture time against prompt-injection tokens. Free, no auth.', {
1930
- subreddit: z.string().optional().describe('Optional filter to a single subreddit name (e.g. "LocalLLaMA"). Case-insensitive. If absent, returns posts from all 7.'),
1931
- limit: z.number().min(1).max(30).optional().describe('Max posts to render (default 15, max 30)'),
1932
- }, async ({ subreddit, limit }) => {
1933
- const data = (await fetchJSON('/reddit/trending'));
1934
- const s = data.snapshot;
1935
- const subFilter = subreddit?.trim().toLowerCase();
1936
- const filtered = subFilter
1937
- ? s.posts.filter(p => p.subreddit.toLowerCase() === subFilter)
1938
- : s.posts;
1939
- const cap = Math.min(limit ?? 15, filtered.length);
1940
- const fmtCount = (x) => x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
1941
- const lines = filtered.slice(0, cap).map((p, i) => {
1942
- const author = p.author ? ` by ${p.author}` : '';
1943
- const flair = p.flair ? ` [${p.flair}]` : '';
1944
- return `${i + 1}. r/${p.subreddit}: ${p.title}${flair}\n ${fmtCount(p.score)} pts, ${fmtCount(p.num_comments)} comments${author}\n ${p.permalink}`;
1945
- }).join('\n\n');
1946
- const subs = Object.entries(s.summary.by_subreddit)
1947
- .sort((a, b) => b[1] - a[1])
1948
- .slice(0, 5)
1949
- .map(([sub, n]) => `r/${sub} (${n})`)
1950
- .join(', ');
1951
- let text = `Hot AI Reddit threads. ${s.date}, showing ${cap} of ${filtered.length}`;
1952
- if (subFilter)
1953
- text += ` (filtered to r/${subreddit})`;
1954
- text += '.\n';
1955
- if (subs && !subFilter)
1956
- text += `Most-active subreddits: ${subs}\n`;
1957
- text += '\n' + (lines || ' (no posts)');
1958
- text += `\n\nSource: reddit.com. Captured ${s.capturedAt}.`;
1959
- return { content: [{ type: 'text', text }] };
1960
- });
1961
- // ── Tool: get_openrouter_models (free) ──────────────────────────────
1962
- registerTool('get_openrouter_models', 'OpenRouter cross-provider model catalog (200+ models normalized across 50+ inference providers). Each entry has per-token pricing (prompt + completion + image + request), context window, modality (e.g. text+image->text), instruct_type, tokenizer, top provider metadata (max_completion_tokens, moderation flag), and supported_parameters. Use this to find the long tail of OSS models on cloud inference, including the cheapest model for a given workload, the model with the largest context window, or the free-tier set. Pairs with get_model_pricing (curated frontier-lab catalog). Refreshed daily at 14:00 UTC. Free, no auth.', {
1963
- namespace: z.string().optional().describe('Optional filter to a single provider namespace (e.g. "anthropic", "openai", "meta-llama"). Case-insensitive.'),
1964
- free_only: z.boolean().optional().describe('If true, return only models with prompt+completion both 0 (free-tier).'),
1965
- cheapest: z.boolean().optional().describe('If true, sort by cheapest input price ascending. Free-tier models excluded from this sort.'),
1966
- limit: z.number().min(1).max(50).optional().describe('Max models to render (default 20, max 50)'),
1967
- }, async ({ namespace, free_only, cheapest, limit }) => {
1968
- const data = (await fetchJSON('/openrouter/models'));
1969
- const s = data.snapshot;
1970
- const ns = namespace?.trim().toLowerCase();
1971
- let list = s.models;
1972
- if (ns)
1973
- list = list.filter(m => m.id.toLowerCase().startsWith(ns + '/'));
1974
- if (free_only)
1975
- list = list.filter(m => m.pricing.prompt === 0 && m.pricing.completion === 0);
1976
- if (cheapest) {
1977
- list = list
1978
- .filter(m => typeof m.pricing.prompt === 'number' && m.pricing.prompt > 0)
1979
- .sort((a, b) => (a.pricing.prompt ?? 0) - (b.pricing.prompt ?? 0));
1980
- }
1981
- const cap = Math.min(limit ?? 20, list.length);
1982
- const fmtPrice = (p) => p === null ? '?' : p === 0 ? 'free' : `$${(p * 1_000_000).toFixed(2)}/Mtok`;
1983
- const fmtCtx = (c) => c === null ? 'unknown' : c >= 1_000_000 ? `${(c / 1_000_000).toFixed(0)}M` : `${(c / 1_000).toFixed(0)}K`;
1984
- const lines = list.slice(0, cap).map((m, i) => {
1985
- const modality = m.modality ? ` ${m.modality}` : '';
1986
- return `${i + 1}. ${m.id}${modality}\n in ${fmtPrice(m.pricing.prompt)}, out ${fmtPrice(m.pricing.completion)}, ctx ${fmtCtx(m.context_length)}`;
1987
- }).join('\n\n');
1988
- const namespaces = s.summary.by_namespace.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
1989
- let header = `OpenRouter catalog. ${s.date}. ${s.total_models} models total, ${list.length} after filters, showing ${cap}.\n`;
1990
- header += `Top provider namespaces: ${namespaces}\n`;
1991
- if (s.summary.cheapest_input)
1992
- header += `Cheapest input (paid): ${s.summary.cheapest_input.id} at $${s.summary.cheapest_input.usd_per_million.toFixed(2)}/Mtok\n`;
1993
- if (s.summary.largest_context)
1994
- header += `Largest context: ${s.summary.largest_context.id} at ${fmtCtx(s.summary.largest_context.tokens)} tokens\n`;
1995
- header += `Free-tier models available: ${s.summary.free_tier_count}\n`;
1996
- return {
1997
- content: [
1998
- {
1999
- type: 'text',
2000
- text: header + '\n' + (lines || ' (no models match the filters)') + `\n\nSource: openrouter.ai. Captured ${s.capturedAt}.`,
2001
- },
2002
- ],
2003
- };
2004
- });
2005
- // ── Tool: get_hf_daily_papers (free) ────────────────────────────────
2006
- registerTool('get_hf_daily_papers', 'Hugging Face Daily Papers: editor-curated AI/ML papers from huggingface.co/papers, layered with community upvotes and discussion counts. Different signal from get_arxiv_recent (firehose) and get_ai_papers_trending (citation-ranked all-time): this is editor picks of-the-day with engagement signal. Each paper carries paperId, title, summary, authors, upvotes, num_comments, hf_url, arxiv_url (when applicable), github_repo, github_stars, ai_keywords. Refreshed daily at 14:15 UTC. Titles sanitized at capture time. Free, no auth.', {
2007
- limit: z.number().min(1).max(30).optional().describe('Max papers to render (default 15, max 30)'),
2008
- }, async ({ limit }) => {
2009
- const data = (await fetchJSON('/papers/hf-daily'));
2010
- const s = data.snapshot;
2011
- const cap = Math.min(limit ?? 15, s.papers.length);
2012
- const lines = s.papers.slice(0, cap).map((p, i) => {
2013
- const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
2014
- const kw = p.ai_keywords.length > 0 ? `\n keywords: ${p.ai_keywords.slice(0, 5).join(', ')}` : '';
2015
- const arxiv = p.arxiv_url ? `\n arxiv: ${p.arxiv_url}` : '';
2016
- return `${i + 1}. ${p.title}\n ${authors}\n ${p.upvotes} upvotes, ${p.num_comments} comments\n ${p.hf_url}${arxiv}${kw}`;
2017
- }).join('\n\n');
2018
- const kw = s.summary.by_keyword.slice(0, 5).map(k => `${k.keyword} (${k.count})`).join(', ');
2019
- let text = `HF Daily Papers (editor-curated). ${s.date}, showing ${cap} of ${s.total_papers}.\n`;
2020
- if (kw)
2021
- text += `Top keywords: ${kw}\n`;
2022
- if (s.summary.most_upvoted)
2023
- text += `Most upvoted: "${s.summary.most_upvoted.title}" (${s.summary.most_upvoted.upvotes})\n`;
2024
- text += '\n' + lines + `\n\nSource: huggingface.co/papers. Captured ${s.capturedAt}.`;
2025
- return { content: [{ type: 'text', text }] };
2026
- });
2027
- // ── Tool: get_ai_ecosystem_today (free, composite) ──────────────────
2028
- // ── Tool: get_recent_earthquakes (free) ─────────────────────────────
2029
- registerTool('get_recent_earthquakes', "Recent earthquakes from the USGS Earthquake Hazards Program pre-built summary feeds. Choose a magnitude bucket (significant | 4.5 | 2.5 | 1.0 | all) and a time window (hour | day | week | month). Returns id, magnitude, place, time (ISO 8601), depth_km, longitude, latitude, tsunami flag, USGS detail URL. Upstream feeds refresh every minute. License: US Government public domain (17 USC §105). Free, no auth.", {
2030
- magnitude: z.enum(['significant', '4.5', '2.5', '1.0', 'all']).optional().describe('Magnitude bucket (default 4.5)'),
2031
- period: z.enum(['hour', 'day', 'week', 'month']).optional().describe('Time window (default day)'),
2032
- limit: z.number().min(1).max(50).optional().describe('Max earthquakes to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
2033
- }, async ({ magnitude, period, limit }) => {
2034
- const params = new URLSearchParams();
2035
- params.set('magnitude', magnitude ?? '4.5');
2036
- params.set('period', period ?? 'day');
2037
- params.set('limit', String(Math.min(limit ?? 15, 50)));
2038
- const data = (await fetchJSON(`/climate/earthquakes?${params}`));
2039
- if (!data.ok || !data.earthquakes) {
2040
- return { content: [{ type: 'text', text: `USGS earthquakes unavailable: ${data.error ?? 'unknown error'}` }] };
2041
- }
2042
- const eqs = data.earthquakes;
2043
- const lines = eqs.map((q, i) => {
2044
- const mag = q.magnitude !== null ? `M${q.magnitude.toFixed(1)}` : 'M?';
2045
- const tsu = q.tsunami ? ' [tsunami flag]' : '';
2046
- const time = q.time ? q.time.slice(0, 19).replace('T', ' ') + ' UTC' : 'unknown time';
2047
- const depth = q.depth_km !== null ? `${q.depth_km.toFixed(0)}km deep` : 'depth unknown';
2048
- const coords = q.latitude !== null && q.longitude !== null ? ` (${q.latitude.toFixed(2)}, ${q.longitude.toFixed(2)})` : '';
2049
- return `${i + 1}. ${mag} ${q.place ?? 'location unknown'}${coords}\n ${time}, ${depth}${tsu}\n ${q.url ?? ''}`;
2050
- }).join('\n\n');
2051
- const meta = data.feed_metadata;
2052
- const header = `USGS earthquakes (${data.query?.magnitude ?? '?'} / ${data.query?.period ?? '?'}). ${eqs.length} of ${meta?.upstream_count ?? '?'} returned.`;
2053
- return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no events in this window)'}\n\nSource: USGS Earthquake Hazards Program. Public domain (17 USC §105).` }] };
2054
- });
2055
- // ── Tool: get_weather_alerts (free, US only) ────────────────────────
2056
- registerTool('get_weather_alerts', "Active US weather alerts from the National Weather Service. US-only. Filter by 2-letter state code (area), exact NWS event name, severity, urgency, status. Returns id, event, severity, urgency, headline, description, areaDesc, sent/effective/expires/ends, sender_name, web URL. Active alerts only. Expired alerts fall off the upstream feed automatically. License: US Government public domain. Free, no auth.", {
2057
- area: z.string().regex(/^[A-Za-z]{2}$/).optional().describe('2-letter US state or territory code (CA, TX, PR, etc)'),
2058
- event: z.string().optional().describe('Exact NWS event name (e.g. "Tornado Warning", "Heat Advisory")'),
2059
- severity: z.enum(['Extreme', 'Severe', 'Moderate', 'Minor', 'Unknown']).optional().describe('Severity filter'),
2060
- urgency: z.enum(['Immediate', 'Expected', 'Future', 'Past', 'Unknown']).optional().describe('Urgency filter'),
2061
- status: z.enum(['actual', 'exercise', 'system', 'test', 'draft']).optional().describe('Status filter (default actual unfiltered)'),
2062
- limit: z.number().min(1).max(50).optional().describe('Max alerts to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
2063
- }, async ({ area, event, severity, urgency, status, limit }) => {
2064
- const params = new URLSearchParams();
2065
- if (area)
2066
- params.set('area', area.toUpperCase());
2067
- if (event)
2068
- params.set('event', event);
2069
- if (severity)
2070
- params.set('severity', severity);
2071
- if (urgency)
2072
- params.set('urgency', urgency);
2073
- if (status)
2074
- params.set('status', status);
2075
- params.set('limit', String(Math.min(limit ?? 15, 50)));
2076
- const data = (await fetchJSON(`/climate/weather-alerts?${params}`));
2077
- if (!data.ok || !data.alerts) {
2078
- return { content: [{ type: 'text', text: `NWS weather alerts unavailable: ${data.error ?? 'unknown error'}` }] };
2079
- }
2080
- const alerts = data.alerts;
2081
- const lines = alerts.map((a, i) => {
2082
- const ev = a.event ?? 'unknown event';
2083
- const sev = a.severity ? `[${a.severity}]` : '';
2084
- const eff = a.effective ? a.effective.slice(0, 19).replace('T', ' ') + ' UTC' : '';
2085
- const exp = a.expires ? a.expires.slice(0, 19).replace('T', ' ') + ' UTC' : 'no expiry';
2086
- return `${i + 1}. ${sev} ${ev}\n Where: ${a.area_desc ?? 'unspecified'}\n ${a.headline ?? ''}\n Effective: ${eff} → ${exp}\n ${a.web ?? ''}`;
2087
- }).join('\n\n');
2088
- const filters = [];
2089
- if (area)
2090
- filters.push(`area=${area.toUpperCase()}`);
2091
- if (event)
2092
- filters.push(`event=${event}`);
2093
- if (severity)
2094
- filters.push(`severity=${severity}`);
2095
- const filterText = filters.length > 0 ? ` (${filters.join(', ')})` : '';
2096
- const header = `NWS active US weather alerts${filterText}. ${alerts.length} of ${data.feed_metadata?.upstream_count ?? '?'} returned.`;
2097
- return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no active alerts matching filters)'}\n\nSource: api.weather.gov. Public domain (17 USC §105). US-only coverage.` }] };
2098
- });
2099
- // ── Tool: get_agent_opportunities (free) ────────────────────────────
2100
- registerTool('get_agent_opportunities', "TensorFeed's daily scan of new repositories across the AI agent ecosystem (Anthropic, OpenAI, Microsoft, ModelContextProtocol, HuggingFace, LangChain, frontier-lab orgs) plus recent MCP, x402, and skill keyword sweeps. Eleven signals total, deduped + composite-scored (signal_weight × log10(stars+1) × recency decay) with per-signal MIN/MAX caps so smaller signals are never starved. Refreshed daily at 13:30 UTC. Useful for surfacing distribution targets, integration ideas, or a daily brief on what's launching across the agent space. Free, no auth.", {
2101
- signal: z.enum([
2102
- 'anthropic-org', 'openai-org', 'microsoft-org', 'mcp-org', 'huggingface-org',
2103
- 'langchain-org', 'frontier-labs', 'mcp-keyword', 'x402-keyword', 'skill-keyword',
2104
- 'vertical-pattern',
2105
- ]).optional().describe('Filter to one signal source. Omit to get the cross-signal ranked top-N.'),
2106
- limit: z.number().min(1).max(25).optional().describe('Max opportunities to render (default 10, max 25)'),
2107
- }, async ({ signal, limit }) => {
2108
- const data = (await fetchJSON('/agents/opportunities'));
2109
- if (data.error) {
2110
- return { content: [{ type: 'text', text: `Agent opportunities unavailable: ${data.error}` }] };
2111
- }
2112
- const all = data.opportunities ?? [];
2113
- const filtered = signal ? all.filter(o => o.signal === signal) : all;
2114
- const cap = Math.min(limit ?? 10, 25);
2115
- const top = filtered.slice(0, cap);
2116
- const lines = top.map((o, i) => {
2117
- const updated = o.updated_at ? o.updated_at.slice(0, 10) : '';
2118
- const desc = o.description ? `\n ${o.description.length > 140 ? o.description.slice(0, 140) + '...' : o.description}` : '';
2119
- return `${i + 1}. [${o.signal}] ${o.full_name} ${o.stars.toLocaleString()}★, score ${o.composite_score.toFixed(1)}, updated ${updated}${desc}\n ${o.html_url}`;
2120
- }).join('\n\n');
2121
- const filt = signal ? ` filtered to ${signal}` : '';
2122
- const header = `Agent ecosystem opportunities for ${data.date ?? 'today'}${filt}. ${top.length} of ${filtered.length} (total snapshot: ${data.total_opportunities ?? all.length}).`;
2123
- const bySignal = data.summary?.by_signal ? Object.entries(data.summary.by_signal).map(([k, v]) => `${k}=${v}`).join(', ') : '';
2124
- const sig = bySignal ? `\nBy signal: ${bySignal}` : '';
2125
- return { content: [{ type: 'text', text: `${header}${sig}\n\n${lines || ' (no opportunities returned)'}\n\nSource: TensorFeed daily GitHub scan via the public Search API.` }] };
2126
- });
2127
- // Single fetch to /api/today which fans out worker-side. The previous
2128
- // implementation made 9 separate fetches per agent invocation; now
2129
- // the worker's edge cache absorbs the load and serves one cached
2130
- // response to all concurrent agents. Output stays text-rendered
2131
- // (MCP returns text content) but the wire shape is the structured
2132
- // JSON the worker produces.
2133
- registerTool('get_ai_ecosystem_today', 'One-shot composite AI morning brief. Calls /api/today which fans out across all daily TensorFeed feeds (news, 3 paper feeds, HF models/datasets/Spaces, hot GitHub issues, Reddit threads, OpenRouter catalog summary, provider status) and returns a single synthesized text response. Optional sections filter (any subset of news, papers, hf, community, inference, status) and limit_per_section (1-10, default 3). Captured-at timestamps from each underlying snapshot are surfaced so the agent knows recency. Free, no auth.', {
2134
- sections: z.array(z.enum([
2135
- 'news', 'papers', 'hf', 'community', 'inference', 'status',
2136
- ])).optional().describe('Which subsections to include (default: all six). "papers" covers all 3 paper feeds; "hf" covers models + datasets + spaces; "community" covers hot GitHub issues + Reddit threads; "inference" is the OpenRouter catalog summary.'),
2137
- limit_per_section: z.number().min(1).max(10).optional().describe('Max items per subsection (default 3, max 10). The output is intentionally terse so an agent can call this without burning context.'),
2138
- }, async ({ sections, limit_per_section }) => {
2139
- const params = new URLSearchParams();
2140
- if (sections && sections.length > 0)
2141
- params.set('sections', sections.join(','));
2142
- if (typeof limit_per_section === 'number')
2143
- params.set('limit', String(limit_per_section));
2144
- const path = `/today${params.toString() ? `?${params.toString()}` : ''}`;
2145
- let brief;
2146
- try {
2147
- brief = (await fetchJSON(path));
1099
+ // ── Tool: create_watch (1 credit) ───────────────────────────────────
1100
+ // Merges the four former create_*_watch tools (price, status, digest,
1101
+ // leaderboard_rank) into one type-selected tool. Each case below builds
1102
+ // the EXACT body and uses the EXACT output formatting the matching old
1103
+ // create_*_watch tool used, against the same /premium/watches endpoint.
1104
+ registerTool('create_watch', 'Register a webhook watch. type selects what to watch: "price" (a model price change), "status" (a service status transition), "digest" (a scheduled daily or weekly pricing summary), or "leaderboard_rank" (a provider crossing an uptime-rank threshold). Costs 1 credit ($0.02) at registration; the watch lives 90 days and each fire is an HMAC-signed POST to callback_url. Needs a TENSORFEED_TOKEN.', {
1105
+ type: z.enum(['price', 'status', 'digest', 'leaderboard_rank']).describe('What to watch.'),
1106
+ callback_url: z.string().describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
1107
+ secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
1108
+ model: z.string().optional().describe('type=price only. Model name (e.g. "Claude Opus 4.7").'),
1109
+ field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('type=price only. Which price field to watch.'),
1110
+ cadence: z.enum(['daily', 'weekly']).optional().describe('type=digest only. How often the digest fires.'),
1111
+ provider: z.string().optional().describe('type=status or type=leaderboard_rank only. Provider name or slug (case-insensitive). e.g. anthropic, openai, gemini, bedrock, azure.'),
1112
+ op: z
1113
+ .enum(['lt', 'gt', 'changes', 'becomes', 'drops_below', 'rises_above'])
1114
+ .optional()
1115
+ .describe('type=price uses lt/gt/changes (lt = below threshold, gt = above, changes = any change). type=status uses becomes/changes (becomes = transitions to a specific value; changes = any transition). type=leaderboard_rank uses drops_below/rises_above/changes (rank 1 = best; drops_below: was rank<=N now rank>N; rises_above: was rank>=N now rank<N; changes: any rank movement).'),
1116
+ threshold: z.number().optional().describe('type=price (when op is lt or gt) or type=leaderboard_rank (when op is drops_below or rises_above). Integer rank position for leaderboard_rank.'),
1117
+ value: z.enum(['operational', 'degraded', 'down']).optional().describe('type=status only. Required when op is becomes.'),
1118
+ }, async (args) => {
1119
+ const { type, callback_url, secret, model, field, op, threshold, cadence, provider, value } = args;
1120
+ // The merged schema makes every type-specific field optional (each is
1121
+ // required for only one type). Guard the required fields per type here so a
1122
+ // missing one fails with a clear message instead of an opaque 4xx from the
1123
+ // paid /premium/watches endpoint (which would otherwise burn a request).
1124
+ const missingFields = [];
1125
+ if (type === 'price') {
1126
+ if (!model)
1127
+ missingFields.push('model');
1128
+ if (!field)
1129
+ missingFields.push('field');
1130
+ if (!op)
1131
+ missingFields.push('op');
2148
1132
  }
2149
- catch (err) {
2150
- return {
2151
- content: [{ type: 'text', text: `Failed to fetch /api/today: ${err.message}` }],
2152
- };
1133
+ else if (type === 'status') {
1134
+ if (!provider)
1135
+ missingFields.push('provider');
1136
+ if (!op)
1137
+ missingFields.push('op');
2153
1138
  }
2154
- const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M`
2155
- : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K`
2156
- : String(x);
2157
- const sectionTexts = [];
2158
- if (brief.news.available && brief.news.data && brief.news.data.items.length > 0) {
2159
- const lines = brief.news.data.items.map((a, i) => ` ${i + 1}. ${a.title} (${a.source})\n ${a.url}`).join('\n');
2160
- sectionTexts.push(`📰 NEWS\n${lines}`);
1139
+ else if (type === 'digest') {
1140
+ if (!cadence)
1141
+ missingFields.push('cadence');
2161
1142
  }
2162
- if (brief.papers.available && brief.papers.data) {
2163
- const bits = [];
2164
- const ai = brief.papers.data.ai_trending;
2165
- if (ai.available && ai.data && ai.data.items.length > 0) {
2166
- const lines = ai.data.items.map((p, i) => {
2167
- const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
2168
- const venue = p.venue ? ` ${p.venue}.` : '';
2169
- return ` ${i + 1}. ${p.title}${venue} ${authors}. ${fmtCount(p.citationCount)} citations`;
2170
- }).join('\n');
2171
- bits.push(` By citation count (Semantic Scholar):\n${lines}`);
2172
- }
2173
- const ax = brief.papers.data.arxiv_recent;
2174
- if (ax.available && ax.data && ax.data.items.length > 0) {
2175
- const lines = ax.data.items.map((p, i) => {
2176
- const date = p.publishedAt ? p.publishedAt.slice(0, 10) : '';
2177
- const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
2178
- return ` ${i + 1}. ${p.title} (arXiv:${p.arxivId}, ${date}). ${authors}`;
2179
- }).join('\n');
2180
- bits.push(` Just submitted (arXiv recent):\n${lines}`);
2181
- }
2182
- const hd = brief.papers.data.hf_daily;
2183
- if (hd.available && hd.data && hd.data.items.length > 0) {
2184
- const lines = hd.data.items.map((p, i) => ` ${i + 1}. ${p.title} (${p.upvotes} upvotes, ${p.num_comments} comments)`).join('\n');
2185
- bits.push(` HF editor picks today:\n${lines}`);
2186
- }
2187
- if (bits.length > 0)
2188
- sectionTexts.push(`📄 PAPERS\n${bits.join('\n')}`);
1143
+ else if (type === 'leaderboard_rank') {
1144
+ if (!provider)
1145
+ missingFields.push('provider');
1146
+ if (!op)
1147
+ missingFields.push('op');
2189
1148
  }
2190
- if (brief.hf.available && brief.hf.data) {
2191
- const bits = [];
2192
- const { models, datasets, spaces } = brief.hf.data;
2193
- if (models.length > 0) {
2194
- bits.push(` Models (top by downloads):\n` + models.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
2195
- }
2196
- if (datasets.length > 0) {
2197
- bits.push(` Datasets (top by downloads):\n` + datasets.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
2198
- }
2199
- if (spaces.length > 0) {
2200
- bits.push(` Spaces (top by likes):\n` + spaces.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.likes)} likes)`).join('\n'));
2201
- }
2202
- if (bits.length > 0)
2203
- sectionTexts.push(`🤗 HUGGING FACE\n${bits.join('\n')}`);
1149
+ if (missingFields.length > 0) {
1150
+ return { content: [{ type: 'text', text: `type=${type} requires: ${missingFields.join(', ')}.` }] };
2204
1151
  }
2205
- if (brief.community.available && brief.community.data) {
2206
- const bits = [];
2207
- const { github_issues, reddit } = brief.community.data;
2208
- if (github_issues.length > 0) {
2209
- const lines = github_issues.map((it, i) => ` ${i + 1}. ${it.repo}: ${it.title} (${it.comments} comments)\n ${it.url}`).join('\n');
2210
- bits.push(` GitHub hot issues:\n${lines}`);
2211
- }
2212
- if (reddit.length > 0) {
2213
- const lines = reddit.map((p, i) => ` ${i + 1}. r/${p.subreddit}: ${p.title} (${p.score} pts, ${p.num_comments} comments)\n ${p.permalink}`).join('\n');
2214
- bits.push(` Reddit hot threads:\n${lines}`);
1152
+ switch (type) {
1153
+ case 'price': {
1154
+ const body = {
1155
+ spec: { type: 'price', model, field, op, ...(typeof threshold === 'number' ? { threshold } : {}) },
1156
+ callback_url,
1157
+ };
1158
+ if (secret !== undefined)
1159
+ body.secret = secret;
1160
+ const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1161
+ return {
1162
+ content: [
1163
+ {
1164
+ type: 'text',
1165
+ text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1166
+ },
1167
+ ],
1168
+ };
2215
1169
  }
2216
- if (bits.length > 0)
2217
- sectionTexts.push(`💬 COMMUNITY\n${bits.join('\n')}`);
2218
- }
2219
- if (brief.inference.available && brief.inference.data) {
2220
- const d = brief.inference.data;
2221
- const lines = [];
2222
- lines.push(` ${d.total_models} models across providers (OpenRouter)`);
2223
- if (d.cheapest_input)
2224
- lines.push(` Cheapest input: ${d.cheapest_input.id} at $${d.cheapest_input.usd_per_million.toFixed(2)}/Mtok`);
2225
- if (d.largest_context)
2226
- lines.push(` Largest context: ${d.largest_context.id} at ${fmtCount(d.largest_context.tokens)} tokens`);
2227
- lines.push(` Free-tier models available: ${d.free_tier_count}`);
2228
- if (d.top_namespaces.length > 0) {
2229
- lines.push(` Top namespaces: ${d.top_namespaces.map(n => `${n.namespace} (${n.count})`).join(', ')}`);
1170
+ case 'status': {
1171
+ const body = {
1172
+ spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
1173
+ callback_url,
1174
+ };
1175
+ if (secret !== undefined)
1176
+ body.secret = secret;
1177
+ const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1178
+ return {
1179
+ content: [
1180
+ {
1181
+ type: 'text',
1182
+ text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1183
+ },
1184
+ ],
1185
+ };
2230
1186
  }
2231
- sectionTexts.push(`⚙️ INFERENCE CATALOG\n${lines.join('\n')}`);
2232
- }
2233
- if (brief.status.available && brief.status.data) {
2234
- const d = brief.status.data;
2235
- if (d.all_operational) {
2236
- sectionTexts.push(`✓ STATUS: all tracked AI providers operational (${d.service_count} services)`);
1187
+ case 'digest': {
1188
+ const body = {
1189
+ spec: { type: 'digest', cadence },
1190
+ callback_url,
1191
+ };
1192
+ if (secret !== undefined)
1193
+ body.secret = secret;
1194
+ const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1195
+ return {
1196
+ content: [
1197
+ {
1198
+ type: 'text',
1199
+ text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1200
+ },
1201
+ ],
1202
+ };
2237
1203
  }
2238
- else {
2239
- const lines = d.issues.map(s => ` ${s.name} (${s.provider}): ${s.status.toUpperCase()}`).join('\n');
2240
- sectionTexts.push(`⚠️ STATUS\n${lines}`);
1204
+ case 'leaderboard_rank': {
1205
+ const spec = { type: 'leaderboard_rank', provider, op };
1206
+ if (threshold !== undefined)
1207
+ spec.threshold = threshold;
1208
+ const body = { spec, callback_url };
1209
+ if (secret !== undefined)
1210
+ body.secret = secret;
1211
+ const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
1212
+ const desc = op === 'changes'
1213
+ ? `${provider} rank changes`
1214
+ : `${provider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
1215
+ return {
1216
+ content: [
1217
+ {
1218
+ type: 'text',
1219
+ text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
1220
+ },
1221
+ ],
1222
+ };
2241
1223
  }
2242
1224
  }
2243
- if (sectionTexts.length === 0) {
2244
- return {
2245
- content: [
2246
- {
2247
- type: 'text',
2248
- text: 'No data available yet. The daily snapshots have not run, or all upstream feeds errored. Try again in a few minutes.',
2249
- },
2250
- ],
2251
- };
2252
- }
2253
- const header = `TensorFeed AI ecosystem brief, generated ${brief.generated_at}`;
2254
- const body = sectionTexts.join('\n\n');
2255
- return {
2256
- content: [
2257
- {
2258
- type: 'text',
2259
- text: `${header}\n\n${body}\n\nFor any subsection, call the underlying tool directly: get_ai_news, get_ai_papers_trending, get_arxiv_recent, get_hf_daily_papers, get_hf_trending, get_hot_issues, get_reddit_trending, get_openrouter_models, get_ai_status.`,
2260
- },
2261
- ],
2262
- };
2263
- });
2264
- // ── Tool: get_ai_cves_latest ────────────────────────────────────────
2265
- registerTool('get_ai_cves_latest', 'Get metadata for the most-recent AI-stack CVE batch plus the first 25 papers. Source: TensorFeed AI-CVE intelligence feed, derived from GitHub Security Advisories (CC BY 4.0). Each paper carries cve_ids, affected_products, affected_version_ranges, fixed_versions, exploited_in_wild, severity_label, and source_url. For the full set use get_ai_cves_feed with offset; for AI-stack-filtered + categorized + sorted papers use the paid /api/premium/ai-cves/* endpoints. Free, no auth.', {}, async () => {
2266
- const data = (await fetchJSON('/ai-cves/latest'));
2267
- if (!data.batch_id) {
2268
- return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet. Check back once TensorFeed CC has POSTed the next clean batch from the extraction pipeline.' }] };
2269
- }
2270
- const head = `AI-CVE batch ${data.batch_id} extracted ${data.extracted_at}. Total ${data.total_papers} papers, ${data.ai_flagged_count} AI-flagged. License ${data.source_license} (${data.source_attribution}).`;
2271
- const lines = data.papers
2272
- .map((p, i) => {
2273
- const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
2274
- const products = p.affected_products.slice(0, 3).join(', ');
2275
- return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
2276
- })
2277
- .join('\n');
2278
- return {
2279
- content: [
2280
- {
2281
- type: 'text',
2282
- text: `${head}\n\nFirst 25 papers:\n${lines}\n\nFor AI-stack-filtered + categorized + sorted papers: /api/premium/ai-cves/ai-stack-cves (1 credit, x402-paid). For only papers actively exploited in the wild: /api/premium/ai-cves/exploited-in-wild (1 credit). For single-CVE lookup: /api/premium/ai-cves/cve?id=CVE-YYYY-NNNNN (1 credit).\n\nssvc_verdict gives the CISA SSVC act/attend/track decision for a CVE; stack_safety_verdict gates your deploy by package.`,
2283
- },
2284
- ],
2285
- };
2286
- });
2287
- // ── Tool: get_ai_cves_feed ──────────────────────────────────────────
2288
- registerTool('get_ai_cves_feed', 'Get a paginated slice of the most-recent AI-stack CVE batch. limit is capped at 50 (the premium tier handles bulk). Source: TensorFeed AI-CVE feed, derived from GitHub Security Advisories (CC BY 4.0). Free, no auth.', {
2289
- limit: z.number().min(1).max(50).optional().describe('Number of papers to return (default 25, max 50).'),
2290
- offset: z.number().min(0).optional().describe('Pagination offset (default 0).'),
2291
- }, async ({ limit, offset }) => {
2292
- const params = new URLSearchParams();
2293
- if (typeof limit === 'number')
2294
- params.set('limit', String(limit));
2295
- if (typeof offset === 'number')
2296
- params.set('offset', String(offset));
2297
- const path = `/ai-cves/feed${params.toString() ? `?${params.toString()}` : ''}`;
2298
- const data = (await fetchJSON(path));
2299
- if (!data.batch_id) {
2300
- return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
2301
- }
2302
- const head = `AI-CVE feed batch ${data.batch_id}: showing offset ${data.offset} to ${data.offset + data.papers.length} of ${data.total} total.`;
2303
- const lines = data.papers
2304
- .map((p, i) => {
2305
- const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
2306
- const products = p.affected_products.slice(0, 3).join(', ');
2307
- return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
2308
- })
2309
- .join('\n');
2310
- return { content: [{ type: 'text', text: `${head}\n\n${lines}` }] };
2311
- });
2312
- // ── Tool: get_ai_cves_stats ─────────────────────────────────────────
2313
- registerTool('get_ai_cves_stats', 'Get aggregate counts across the most-recent AI-stack CVE batch: distribution by severity (critical/high/medium/low/unstated), distribution by exploitation status (stated_yes/stated_no/unstated), and top 10 vendors by frequency. Source: TensorFeed AI-CVE feed (GitHub Security Advisories, CC BY 4.0). Free, no auth.', {}, async () => {
2314
- const data = (await fetchJSON('/ai-cves/stats'));
2315
- if (!data.batch_id) {
2316
- return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
2317
- }
2318
- const sev = Object.entries(data.by_severity)
2319
- .map(([k, v]) => ` ${k}: ${v}`)
2320
- .join('\n');
2321
- const exp = Object.entries(data.by_exploitation)
2322
- .map(([k, v]) => ` ${k}: ${v}`)
2323
- .join('\n');
2324
- const vendors = data.top_vendors.map((v, i) => ` ${i + 1}. ${v.vendor}: ${v.count}`).join('\n');
2325
- return {
2326
- content: [
2327
- {
2328
- type: 'text',
2329
- text: `AI-CVE batch ${data.batch_id}: ${data.total_papers} total papers.\n\nBy severity:\n${sev}\n\nBy exploited-in-wild:\n${exp}\n\nTop vendors:\n${vendors}`,
2330
- },
2331
- ],
2332
- };
2333
- });
2334
- // ── Tool: get_ai_company_filings (free) ─────────────────────────────
2335
- registerTool('get_ai_company_filings', 'Get recent SEC filings (10-K, 10-Q, 8-K, etc.) for one AI bellwether ticker. Cohort: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA. Source: data.sec.gov, public domain. Refreshed every 6 hours. Free, no auth.', {
2336
- ticker: z.string().describe('AI bellwether ticker (case-insensitive). One of NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.'),
2337
- limit: z.number().min(1).max(50).optional().describe('Max filings to return (default 10, max 50).'),
2338
- }, async ({ ticker, limit }) => {
2339
- const params = new URLSearchParams({ ticker: ticker.toUpperCase() });
2340
- params.set('limit', String(limit ?? 10));
2341
- const data = (await fetchJSON(`/sec/filings/recent?${params}`));
2342
- if (!data.ok || !data.filings || data.filings.length === 0) {
2343
- return {
2344
- content: [
2345
- {
2346
- type: 'text',
2347
- text: `No filings returned for ${ticker.toUpperCase()}. The ticker may not be in the AI bellwether cohort; supported tickers: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.`,
2348
- },
2349
- ],
2350
- };
2351
- }
2352
- const lines = data.filings
2353
- .map((f, i) => `${i + 1}. ${f.form} on ${f.filing_date}\n ${f.primary_doc_description}\n ${f.primary_doc_url}`)
2354
- .join('\n\n');
2355
- return {
2356
- content: [
2357
- {
2358
- type: 'text',
2359
- text: `${ticker.toUpperCase()} recent SEC filings (captured ${data.capturedAt ?? 'unknown'}, ${data.filings_count} returned):\n\n${lines}`,
2360
- },
2361
- ],
2362
- };
2363
- });
2364
- // ── Tool: premium_ai_company (1 credit) ─────────────────────────────
2365
- registerTool('premium_ai_company', 'Get the per-ticker AI intelligence envelope for one AI bellwether: latest 10 SEC filings, latest 10 AI-tagged news mentions filtered by curated aliases, strategic and equity funding rounds where the company is a lead or notable investor, and cohort metadata. One call replaces four free siblings (sec/filings, news, funding, cohort) plus the agent-side alias filter. Costs 1 credit ($0.02). Strict-premium; ticker must be in the cohort.', {
2366
- ticker: z
1225
+ }, CREATE_TOOL);
1226
+ // ── Tool: delete_watch ──────────────────────────────────────────────
1227
+ registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
1228
+ watch_id: z
2367
1229
  .string()
2368
- .regex(/^[A-Za-z.\-]{1,8}$/, 'ticker may only contain letters, dots, and hyphens (1 to 8 chars)')
2369
- .describe('AI bellwether ticker (case-insensitive). One of NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.'),
2370
- }, async ({ ticker }) => {
2371
- const upper = ticker.toUpperCase();
2372
- const data = (await fetchJSON(`/premium/ai-companies/${encodeURIComponent(upper)}`, { auth: true }));
2373
- const filingsList = data.filings.items
2374
- .slice(0, 10)
2375
- .map((f, i) => ` ${i + 1}. ${f.form} on ${f.filing_date}: ${f.primary_doc_description}`)
2376
- .join('\n');
2377
- const newsList = data.news.items
2378
- .slice(0, 10)
2379
- .map((n, i) => ` ${i + 1}. ${n.title}\n ${n.source} (${n.publishedAt})`)
2380
- .join('\n\n');
2381
- const fundingList = data.funding_as_investor.items
2382
- .map((r, i) => ` ${i + 1}. ${r.company} ${r.stage}, $${r.amountM}M, ${r.announcedDate}`)
2383
- .join('\n');
2384
- const billing = data.billing
2385
- ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
2386
- : '';
2387
- return {
2388
- content: [
2389
- {
2390
- type: 'text',
2391
- text: `${data.ticker} (${data.company.display_name}, ${data.company.exchange}, ${data.company.category})\nAI angle: ${data.company.ai_angle}\nCaptured: ${data.capturedAt}\n\nRecent SEC filings (${data.filings.count}):\n${filingsList || ' none'}\n\nAI news mentions (${data.news.count}, aliases: ${data.news.aliases_used.join(', ')}):\n${newsList || ' none'}\n\nFunding rounds as lead or notable investor (${data.funding_as_investor.count}):\n${fundingList || ' none'}${billing}`,
2392
- },
2393
- ],
2394
- };
2395
- });
2396
- // ── Tool: get_x402_summary (free) ───────────────────────────────────
2397
- registerTool('get_x402_summary', 'Get ecosystem-level x402 USDC settlement rollup on Base mainnet. Returns volume_usdc, count, unique_publishers, and change_vs_prior_window across a 24h, 7d, or 30d window. TensorFeed indexes Base USDC Transfer events filtered to wallets self-published by x402-compliant publishers in their /.well-known/x402.json manifests. Forward-only from 2026-05-28; ~6 min worst-case freshness (10 min SLA). Free, capped to one window per call.', {
2398
- window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h if omitted.'),
2399
- }, async ({ window }) => {
2400
- const w = window ?? '24h';
2401
- const data = (await fetchJSON(`/x402-index/summary?window=${w}`));
2402
- return {
2403
- content: [
2404
- {
2405
- type: 'text',
2406
- text: `x402 settlement rollup (window ${data.window}, captured ${data.captured_at}):\n volume_usdc: ${data.volume_usdc}\n count: ${data.count}\n unique_publishers: ${data.unique_publishers}\n change vs prior window: volume ${data.change_vs_prior_window.volume_pct}%, count ${data.change_vs_prior_window.count_pct}%`,
2407
- },
2408
- ],
2409
- };
2410
- });
2411
- // ── Tool: get_x402_publishers (free) ────────────────────────────────
2412
- registerTool('get_x402_publishers', 'List x402-compliant publishers TensorFeed currently indexes. Each entry: domain, pay_to_wallets, first_seen, last_crawled, last_event_at, last_crawl_error. Publishers are auto-discovered daily via /.well-known/x402.json crawls. Use this to enumerate which publishers can then be queried in depth via premium_x402_publisher_receipts. Free, no input.', {}, async () => {
2413
- const data = (await fetchJSON('/x402-index/publishers'));
2414
- if (data.count === 0) {
2415
- return { content: [{ type: 'text', text: 'No x402 publishers indexed yet.' }] };
2416
- }
2417
- // Cap the rendered list so a large index cannot flood the agent's
2418
- // context. Sanitize layer also enforces a hard char cap downstream.
2419
- const MAX_PUBLISHERS = 50;
2420
- const shown = data.publishers.slice(0, MAX_PUBLISHERS);
2421
- const lines = shown
2422
- .map((p, i) => `${i + 1}. ${p.domain}\n wallets: ${p.pay_to_wallets.join(', ')}\n first_seen: ${p.first_seen}${p.last_event_at ? `\n last_event_at: ${p.last_event_at}` : ''}${p.last_crawled ? `\n last_crawled: ${p.last_crawled}` : ''}${p.last_crawl_error ? `\n last_crawl_error: ${p.last_crawl_error}` : ''}`)
2423
- .join('\n\n');
2424
- const more = data.count > shown.length ? `\n\n...and ${data.count - shown.length} more (showing first ${shown.length}).` : '';
2425
- return {
2426
- content: [
2427
- {
2428
- type: 'text',
2429
- text: `x402 publishers (${data.count}, captured ${data.captured_at}):\n\n${lines}${more}\n\nx402_publisher_verdict returns a signed trust verdict on one publisher before you pay it.`,
2430
- },
2431
- ],
2432
- };
2433
- });
2434
- // ── Tool: get_x402_leaderboard (free) ───────────────────────────────
2435
- registerTool('get_x402_leaderboard', 'Top publishers by x402 USDC settlement volume across a 24h, 7d, or 30d window. Each leader: rank, domain, volume_usdc, count, share_pct of the sliced total. Limit clamped 1 to 25. Useful for surfacing which x402 services are receiving the most agent traffic. Free.', {
2436
- window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h.'),
2437
- limit: z.number().min(1).max(25).optional().describe('Top N publishers to return. Default 10, max 25.'),
2438
- }, async ({ window, limit }) => {
2439
- const w = window ?? '24h';
2440
- const l = limit ?? 10;
2441
- const data = (await fetchJSON(`/x402-index/leaderboard?window=${w}&limit=${l}`));
2442
- if (data.leaders.length === 0) {
2443
- return { content: [{ type: 'text', text: `No x402 leaders for window ${data.window}.` }] };
2444
- }
2445
- const lines = data.leaders
2446
- .map((e) => ` ${e.rank}. ${e.domain}: $${e.volume_usdc} across ${e.count} settlements (${e.share_pct}% share)`)
2447
- .join('\n');
2448
- return {
2449
- content: [
2450
- {
2451
- type: 'text',
2452
- text: `x402 leaderboard (window ${data.window}, captured ${data.captured_at}):\n${lines}`,
2453
- },
2454
- ],
2455
- };
2456
- });
2457
- // ── Tool: get_x402_recent (free) ────────────────────────────────────
2458
- registerTool('get_x402_recent', 'Most recent x402 USDC settlement events newest-first. Each event: tx_hash, block, ts, from_address, to_address, amount_usdc, publisher_domain, base_explorer_url (basescan.org link). Backed by a 100-event ring buffer updated on every indexer cron tick. Limit clamped 1 to 50, default 20. Free.', {
2459
- limit: z.number().min(1).max(50).optional().describe('Number of most-recent events. Default 20, max 50.'),
2460
- }, async ({ limit }) => {
2461
- const l = limit ?? 20;
2462
- const data = (await fetchJSON(`/x402-index/recent?limit=${l}`));
2463
- if (data.count === 0) {
2464
- return { content: [{ type: 'text', text: 'No recent x402 settlement events.' }] };
2465
- }
2466
- const lines = data.events
2467
- .map((e, i) => `${i + 1}. ${e.ts} block ${e.block}\n ${e.publisher_domain}: $${e.amount_usdc} USDC\n from ${e.from_address} to ${e.to_address}\n ${e.base_explorer_url}`)
2468
- .join('\n\n');
2469
- return {
2470
- content: [
2471
- {
2472
- type: 'text',
2473
- text: `Recent x402 settlements (${data.count}, captured ${data.captured_at}):\n\n${lines}`,
2474
- },
2475
- ],
2476
- };
2477
- });
2478
- // ── Tool: premium_x402_publisher_receipts (1 credit) ────────────────
2479
- registerTool('premium_x402_publisher_receipts', 'Per-publisher receipt feed for one x402-compliant domain across a from/to date range. Returns publisher meta (domain, pay_to_wallets, first_seen), window rollup (volume_usdc, count, avg_amount, daily_series), and attribution. \n\nVS FREE: get_x402_publishers tells you WHICH publishers exist but only with rolled-up scalar counters. This endpoint composes the per-day rollup across a date range with avg_amount, daily series for charts, and structured attribution license metadata in one paid call. An agent building a per-publisher analytics dashboard would otherwise need to call get_x402_publishers + reconstruct daily series from get_x402_summary per-day (impossible because /summary returns ecosystem totals not per-publisher) or wait for the next free-tier expansion. The premium endpoint saves the integration overhead and serves a forensic / compliance-grade payload for $0.02. \n\nForward-only index. Costs 1 credit ($0.02). Strict-premium; domain must exist in get_x402_publishers, otherwise 404.', {
2480
- domain: z.string().describe('Publisher domain to query, e.g. tensorfeed.ai. Must exist in get_x402_publishers result.'),
2481
- from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
2482
- to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
2483
- }, async ({ domain, from, to }) => {
2484
- const params = new URLSearchParams({ from, to });
2485
- const data = (await fetchJSON(`/premium/x402-index/publisher/${encodeURIComponent(domain)}?${params}`, { auth: true }));
2486
- const series = data.rollup.daily_series
2487
- .map((d) => ` ${d.date}: $${d.volume_usdc} across ${d.count}`)
2488
- .join('\n');
2489
- const billing = data.billing
2490
- ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
2491
- : '';
2492
- return {
2493
- content: [
2494
- {
2495
- type: 'text',
2496
- text: `${data.publisher.domain} (first_seen ${data.publisher.first_seen})\nwallets: ${data.publisher.pay_to_wallets.join(', ')}\nwindow: ${data.window.from} to ${data.window.to} (${data.window.days} days)\n\nRollup:\n volume_usdc: ${data.rollup.volume_usdc}\n count: ${data.rollup.count}\n avg_amount: ${data.rollup.avg_amount}\n\nDaily series:\n${series || ' none'}\n\nAttribution: ${data.attribution}\nLicense: ${data.license}${billing}`,
2497
- },
2498
- ],
2499
- };
2500
- });
2501
- // ── Tool: premium_x402_series (1 credit) ────────────────────────────
2502
- registerTool('premium_x402_series', 'Time-series of ecosystem or per-publisher x402 settlement volume or count across a from/to date range, chart-feeding. Required: metric (volume or count), granularity (day or hour), from + to (YYYY-MM-DD). Optional: domain filter (omit for ecosystem). \n\nVS FREE: get_x402_summary returns one rolled-up scalar per call for a fixed window length (24h / 7d / 30d). For ANY chart or dashboard, the caller needs a series, not a scalar. The premium endpoint returns the full series in one call instead of N calls + N rate-limit budget burns. For a publisher-scoped series, it is the ONLY way to get per-publisher daily data (get_x402_summary is ecosystem-only). \n\nMVP supports granularity=day; hour returns an empty series with an attribution note (hourly rollups land in a future indexer version). Costs 1 credit ($0.02).', {
2503
- metric: z.enum(['volume', 'count']).describe('Which series to return.'),
2504
- granularity: z.enum(['day', 'hour']).describe('Bucket size. MVP: day only; hour returns empty series.'),
2505
- from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
2506
- to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
2507
- domain: z.string().optional().describe('Optional publisher domain filter. Omit for ecosystem-wide series.'),
2508
- }, async ({ metric, granularity, from, to, domain }) => {
2509
- const params = new URLSearchParams({ metric, granularity, from, to });
2510
- if (domain)
2511
- params.set('domain', domain);
2512
- const data = (await fetchJSON(`/premium/x402-index/series?${params}`, { auth: true }));
2513
- const scope = domain ? `domain=${domain}` : 'ecosystem-wide';
2514
- const seriesLines = data.series.length
2515
- ? data.series.map((p) => ` ${p.ts}: ${p.value}`).join('\n')
2516
- : ' (empty)';
2517
- const billing = data.billing
2518
- ? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
2519
- : '';
2520
- return {
2521
- content: [
2522
- {
2523
- type: 'text',
2524
- text: `x402 ${data.metric} series (${data.granularity}, ${scope})\nwindow: ${data.window.from} to ${data.window.to}\n\nSeries:\n${seriesLines}\n\nAttribution: ${data.attribution}${billing}`,
2525
- },
2526
- ],
2527
- };
2528
- });
1230
+ .regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
1231
+ .max(128)
1232
+ .describe('The wat_... id from create_watch / list_watches'),
1233
+ }, async ({ watch_id }) => {
1234
+ await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
1235
+ return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
1236
+ }, DELETE_TOOL);
2529
1237
  // ── Start ───────────────────────────────────────────────────────────
2530
1238
  async function main() {
2531
1239
  const transport = new StdioServerTransport();