@wickedlabs/wicked-mcp 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server for the whole [Wicked](https://wickedapi.com) suite — trading data, DeFi protocol health, token safety, brokerage sync, agent identity, reputation, a tool-reliability registry, hallucination checks, persistent agent memory, and x402 paywalls, in one server.
4
4
 
5
- **105 tools** across 12 live services. The first four are generated automatically from each service's own live `openapi.json`, so the list always matches what's deployed; the rest are hand-written because they use wallet signatures and non-`/v1` routes:
5
+ **100+ tools** across 12 live services. The first four are generated automatically from each service's own live `openapi.json`, so the list always matches what's deployed; the rest are hand-written because they use wallet signatures and non-`/v1` routes:
6
6
 
7
7
  | Service | Tools | Auth |
8
8
  | --- | --- | --- |
@@ -19,13 +19,13 @@ MCP server for the whole [Wicked](https://wickedapi.com) suite — trading data,
19
19
  | [Wicked Memory](https://memory.wickedapi.com) | 8 (`memory_prepare`, `memory_store`, `memory_search`, …) | wallet signature on every call; only search is metered |
20
20
  | [x402 Paywall](https://paywall.wickedapi.com) | 8 (`paywall_*`) | `paywall_signup` is open; the rest need the tenant key |
21
21
 
22
- Plus **6 resources** (the platform directory, `llms.txt`, and each service's live endpoint catalog) and **7 prompts** (`rug_check`, `vet_dependency`, `morning_briefing`, `fact_check`, `verify_agent`, `find_reliable_tool`, `agent_memory_guide`) that chain tools together the way the [cookbook](../cookbooks/wickedapi-cookbook.md) does.
22
+ Plus **6 resources** (the platform directory, `llms.txt`, and each service's live endpoint catalog) and **10 prompts** (`rug_check`, `vet_dependency`, `morning_briefing`, `fact_check`, `verify_agent`, `find_reliable_tool`, `agent_memory_guide`) that chain tools together the way the [cookbook](../cookbooks/wickedapi-cookbook.md) does.
23
23
 
24
24
  These tools never sign anything or hold a key. Agents with no wallet can still try Wicked Identity end to end through a sandbox identity (`identity_sandbox_register`, then pass `sandbox_token` instead of a signature); sandbox results are for testing and are never reported as verified. Identity (real wallet), Registry registration and Memory take a signature you produce with your own wallet (`identity_get_nonce` / `memory_prepare` return the exact message to sign). Calls that need x402 payment return the real `402` challenge (decoded under `payment_required` for x402 v2) rather than auto-paying — `WICKEDAPI_PAYER_PRIVATE_KEY` auto-pay applies to the Trading, Protocol Health, Token Risk and Broker Sync tools.
25
25
 
26
26
  ## Remote endpoint (no install)
27
27
 
28
- The same 105 tools are served over streamable HTTP at **`https://mcp.wickedapi.com/mcp`** (stateless, rate-limited per IP; the public deployment carries no personal keys, so paid calls return the x402 challenge). Point any streamable-HTTP MCP client at it. To self-host, build the `Dockerfile` (or `npm run build && npm run start:http`); see `.env.example` for `RATE_LIMIT_PER_MINUTE`, `ALLOWED_HOSTS` and `TRUSTED_CLIENT_IP_HEADER`.
28
+ The same 107 tools are served over streamable HTTP at **`https://mcp.wickedapi.com/mcp`** (stateless, rate-limited per IP; the public deployment carries no personal keys, so paid calls return the x402 challenge). Point any streamable-HTTP MCP client at it. To self-host, build the `Dockerfile` and run it with `node dist/http.js` (the image defaults to the stdio entry point), or `npm run build && npm run start:http`; see `.env.example` for `RATE_LIMIT_PER_MINUTE`, `ALLOWED_HOSTS` and `TRUSTED_CLIENT_IP_HEADER`.
29
29
 
30
30
  ## No signup required
31
31
 
@@ -97,6 +97,9 @@ One-click workflows that chain real tool calls (Claude Desktop and other prompt-
97
97
  - **`verify_agent`** (`wallet`) — Identity assertion + Reputation stake/tier before you transact with an agent.
98
98
  - **`find_reliable_tool`** (`need`, optional `category`, `min_score`) — Registry search by real uptime/latency/schema scores.
99
99
  - **`agent_memory_guide`** (`wallet`, `task`) — the two-step signed Memory flow, explained to the model.
100
+ - **`counterparty_check`** (`address`) — OFAC screening and wallet risk (plus Identity/Reputation) before you pay someone.
101
+ - **`insider_watch`** (`ticker`, optional `days`) — insider trades, insider summary and 8-K events for a stock.
102
+ - **`trade_idea_check`** (`symbol`, optional `timeframe`) — verdict, volatility, positioning and news for a trade idea.
100
103
 
101
104
  ## Environment variables
102
105
 
package/dist/fair.js ADDED
@@ -0,0 +1,90 @@
1
+ // Fair-use plumbing for the public HTTP deployment: who is calling (per request), a small TTL cache so
2
+ // repeated questions cost nothing upstream, a per-client daily cap on the one tool family with a real
3
+ // per-call vendor cost (Sanity open mode runs a paid web search per claim), and aggregate counters.
4
+ // Counters hold counts only: no IPs, claims, wallets or results. In-memory like the rate limiter.
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ const clientStore = new AsyncLocalStorage();
8
+ /** Run `fn` with the caller's rate-limit key attached, so tool handlers can apply per-client limits. */
9
+ export function withClient(key, fn) {
10
+ noteClient(key);
11
+ return clientStore.run(key, fn);
12
+ }
13
+ export function currentClient() {
14
+ return clientStore.getStore();
15
+ }
16
+ // ---- TTL cache (bounded, oldest-first eviction) ---------------------------------------------------
17
+ const MAX_ENTRIES = Number(process.env.CACHE_MAX_ENTRIES || 2000);
18
+ const cache = new Map();
19
+ const stats = { cache_hits: 0, cache_misses: 0, open_mode_refused: 0 };
20
+ export function cacheGet(key, now = Date.now()) {
21
+ const e = cache.get(key);
22
+ if (!e || e.exp <= now) {
23
+ if (e)
24
+ cache.delete(key);
25
+ stats.cache_misses++;
26
+ return undefined;
27
+ }
28
+ stats.cache_hits++;
29
+ return e.value;
30
+ }
31
+ export function cacheSet(key, value, ttlMs, now = Date.now()) {
32
+ if (ttlMs <= 0)
33
+ return;
34
+ if (cache.size >= MAX_ENTRIES) {
35
+ for (const [k, v] of cache)
36
+ if (v.exp <= now)
37
+ cache.delete(k);
38
+ while (cache.size >= MAX_ENTRIES)
39
+ cache.delete(cache.keys().next().value);
40
+ }
41
+ cache.set(key, { exp: now + ttlMs, value });
42
+ }
43
+ export const cacheKeyOf = (...parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex");
44
+ // ---- per-client daily cap on open-mode (paid web search) claims ------------------------------------
45
+ const OPEN_PER_CLIENT_PER_DAY = () => Number(process.env.OPEN_MODE_PER_CLIENT_PER_DAY || 5);
46
+ const day = { key: "", open: new Map(), clients: new Set() };
47
+ const salt = randomBytes(8).toString("hex"); // per-process: unique-client counts cannot be reversed or linked across restarts
48
+ function rollDay(now = Date.now()) {
49
+ const k = new Date(now).toISOString().slice(0, 10);
50
+ if (day.key !== k) {
51
+ day.key = k;
52
+ day.open.clear();
53
+ day.clients.clear();
54
+ }
55
+ }
56
+ function noteClient(key) {
57
+ rollDay();
58
+ day.clients.add(createHash("sha256").update(salt + key).digest("hex").slice(0, 16));
59
+ }
60
+ /** Reserve `claims` open-mode claims for this client today. Returns null if allowed, else a refusal body. */
61
+ export function reserveOpenMode(claims, client = currentClient()) {
62
+ rollDay();
63
+ const cap = OPEN_PER_CLIENT_PER_DAY();
64
+ if (!client || cap <= 0)
65
+ return null;
66
+ const used = day.open.get(client) ?? 0;
67
+ if (used + claims > cap) {
68
+ stats.open_mode_refused++;
69
+ return {
70
+ http_status: 429,
71
+ error: `Open-mode checks are limited to ${cap} claims per client per day on this shared server (each one runs a paid live web search). Grounded mode (pass your source text as context) is not limited this way.`,
72
+ hint: "Use mode \"grounded\" with a context, set your own SANITY_API_KEY, pay per call over x402, or run the MCP server locally.",
73
+ remaining_today: Math.max(0, cap - used),
74
+ source: "mcp",
75
+ };
76
+ }
77
+ day.open.set(client, used + claims);
78
+ return null;
79
+ }
80
+ export function fairSnapshot() {
81
+ rollDay();
82
+ return { day: day.key, unique_clients_today: day.clients.size, ...stats, cache_entries: cache.size };
83
+ }
84
+ export function _resetFairForTests() {
85
+ cache.clear();
86
+ day.key = "";
87
+ day.open.clear();
88
+ day.clients.clear();
89
+ stats.cache_hits = stats.cache_misses = stats.open_mode_refused = 0;
90
+ }
package/dist/http.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { createServer } from "node:http";
8
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
9
  import { clientKey } from "./clientIp.js";
10
+ import { fairSnapshot, withClient } from "./fair.js";
10
11
  import { rateLimited } from "./rateLimit.js";
11
12
  import { landingHtml, llmsTxt, serverCard } from "./landing.js";
12
13
  import { knownTools, usageHooks, usageSnapshot } from "./usage.js";
@@ -67,7 +68,7 @@ async function main() {
67
68
  return send(res, 200, serverCard(tools, SERVER_VERSION), { "cache-control": "public, max-age=300" });
68
69
  }
69
70
  if (url.pathname === "/stats")
70
- return send(res, 200, usageSnapshot());
71
+ return send(res, 200, { ...usageSnapshot(), fair_use: fairSnapshot() });
71
72
  }
72
73
  if (url.pathname !== "/mcp")
73
74
  return send(res, 404, { error: "Not found. The MCP endpoint is /mcp." });
@@ -86,7 +87,8 @@ async function main() {
86
87
  void server.close();
87
88
  });
88
89
  await server.connect(transport);
89
- await transport.handleRequest(req, res, body);
90
+ // Attach the caller's rate-limit key so tool handlers can apply per-client fair-use limits.
91
+ await withClient(clientKey(req), () => transport.handleRequest(req, res, body));
90
92
  }
91
93
  catch (err) {
92
94
  const status = err.status ?? 500;
package/dist/landing.js CHANGED
@@ -16,7 +16,7 @@ const GROUPS = [
16
16
  { id: "wallet", label: "Wallet Oracle", prefix: "wallet_", blurb: "Counterparty check: OFAC sanctions, malicious-address flags and on-chain profile for any EVM wallet." },
17
17
  { id: "paywall", label: "x402 Paywall", prefix: "paywall_", blurb: "Put any endpoint behind pay-per-call USDC." },
18
18
  ];
19
- export const PROMPT_NAMES = ["rug_check", "vet_dependency", "morning_briefing", "fact_check", "verify_agent", "find_reliable_tool", "agent_memory_guide"];
19
+ export const PROMPT_NAMES = ["rug_check", "vet_dependency", "morning_briefing", "fact_check", "verify_agent", "find_reliable_tool", "agent_memory_guide", "counterparty_check", "insider_watch", "trade_idea_check"];
20
20
  function grouped(tools) {
21
21
  const claimed = new Set();
22
22
  const out = GROUPS.map((g) => {
package/dist/prompts.js CHANGED
@@ -82,4 +82,74 @@ export function registerSuitePrompts(server) {
82
82
  `Report exactly what the tools returned.`),
83
83
  ],
84
84
  }));
85
+ server.registerPrompt("counterparty_check", {
86
+ title: "Check a counterparty before you pay",
87
+ description: "Screen a wallet or contract before sending funds: OFAC sanctions, malicious-address flags, and (if it is an agent) its identity and stake.",
88
+ argsSchema: { address: z.string().describe("The 0x wallet or contract address you are about to transact with.") },
89
+ }, ({ address }) => ({
90
+ description: "Counterparty screening",
91
+ messages: [
92
+ text(`Before I send anything to ${address}, screen it with real data - never guess:
93
+
94
+ ` +
95
+ `1. Call \`wallet_sanctions_check\` (free) with the address. Report whether it is on the OFAC SDN list and the sanctioned entity if so.
96
+ ` +
97
+ `2. Call \`wallet_check\` for the broader pre-transaction check (malicious-address flags and on-chain signals). If it returns http_status 402, show me the price from payment_required and continue with what is free.
98
+ ` +
99
+ `3. If this address is an agent I might deal with repeatedly, also call \`identity_status\` and \`reputation_status\` for it (a 404 from reputation means never registered, not an error).
100
+
101
+ ` +
102
+ `Summarize: sanctioned or not, any flags, and what is unknown. These are signals, not a guarantee.`),
103
+ ],
104
+ }));
105
+ server.registerPrompt("insider_watch", {
106
+ title: "Insider and filings watch",
107
+ description: "For a stock ticker, combine recent insider trading, an insider summary and classified 8-K events from SEC filings.",
108
+ argsSchema: {
109
+ ticker: z.string().describe('US stock ticker, e.g. "NVDA".'),
110
+ days: z.string().optional().describe("Lookback window in days (default 30)."),
111
+ },
112
+ }, ({ ticker, days }) => ({
113
+ description: "Insider and filings watch",
114
+ messages: [
115
+ text(`Build an insider and filings view of ${ticker} over the last ${days || "30"} days using real data only:
116
+
117
+ ` +
118
+ `1. \`filings_insider_summary\` for the ticker: open-market buy and sell counts and dollar totals.
119
+ ` +
120
+ `2. \`filings_insider_trades\` for the individual Form 4 trades (insider, role, code, size). Report exact values.
121
+ ` +
122
+ `3. \`filings_8k_events\` for material events (earnings, executive changes, agreements).
123
+ ` +
124
+ `4. Optionally \`earnings_calendar\` for the next report date.
125
+
126
+ ` +
127
+ `If any call returns http_status 402, show the price from payment_required rather than guessing. Then summarize what the filings actually show; do not speculate beyond them.`),
128
+ ],
129
+ }));
130
+ server.registerPrompt("trade_idea_check", {
131
+ title: "Check a trade idea",
132
+ description: "Pressure-test a trade on one asset with the verdict engine, volatility, positioning and fresh news, without inventing numbers.",
133
+ argsSchema: {
134
+ symbol: z.string().describe("Asset symbol, e.g. BTC, ETH, SOL, NVDA."),
135
+ timeframe: z.string().optional().describe("Timeframe such as 1h or 1D (default 1h)."),
136
+ },
137
+ }, ({ symbol, timeframe }) => ({
138
+ description: "Trade idea check",
139
+ messages: [
140
+ text(`Pressure-test a trade on ${symbol} (${timeframe || "1h"}) from real tool output only:
141
+
142
+ ` +
143
+ `1. \`verdict\` for the bias, confidence, thesis, entry, invalidation and targets.
144
+ ` +
145
+ `2. \`atr\` for volatility and regime (size the stop from it).
146
+ ` +
147
+ `3. For crypto: \`funding_oi\` and \`long_short_ratio\` for crowding; \`liquidity_levels\` for nearby pools.
148
+ ` +
149
+ `4. \`crypto_news\` for fresh headlines that could invalidate the idea.
150
+
151
+ ` +
152
+ `Quote exact values. If a call returns 402, show the price instead of guessing. End with a plain verdict and what would invalidate it. This is analysis from tools, not financial advice.`),
153
+ ],
154
+ }));
85
155
  }
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { registerSuiteTools } from "./suite.js";
4
4
  import { registerSuitePrompts } from "./prompts.js";
5
5
  import { instrument } from "./usage.js";
6
+ import { decorateRegistry } from "./toolMeta.js";
6
7
  function envUrl(name, fallback) {
7
8
  return (process.env[name] || fallback).replace(/\/$/, "");
8
9
  }
@@ -102,7 +103,28 @@ async function loadServiceSpec(service) {
102
103
  return null;
103
104
  }
104
105
  }
105
- function registerServiceTools(server, service, doc, doFetch) {
106
+ async function loadStatusCatalog(service) {
107
+ try {
108
+ const res = await fetch(`${service.baseUrl}/v1/status`, { signal: AbortSignal.timeout(10_000) });
109
+ if (!res.ok)
110
+ return {};
111
+ const body = (await res.json());
112
+ const endpoints = (body.data ?? body).endpoints ?? [];
113
+ const out = {};
114
+ for (const e of endpoints) {
115
+ if (typeof e.path !== "string")
116
+ continue;
117
+ out[`${String(e.method || "GET").toUpperCase()} ${e.path.replace(/:([A-Za-z_]+)/g, "{$1}")}`] = { auth: e.auth, params: e.params };
118
+ }
119
+ return out;
120
+ }
121
+ catch {
122
+ return {};
123
+ }
124
+ }
125
+ // tool name -> true when the endpoint is priced/keyed, false when free. Read by toolMeta.
126
+ const PRICED = new Map();
127
+ function registerServiceTools(server, service, doc, doFetch, status = {}) {
106
128
  let registered = 0;
107
129
  for (const [openapiPath, methods] of Object.entries(doc.paths)) {
108
130
  if (!openapiPath.startsWith("/v1/") || openapiPath === "/v1/status")
@@ -114,14 +136,16 @@ function registerServiceTools(server, service, doc, doFetch) {
114
136
  const queryParamNames = [];
115
137
  const bodyPropNames = [];
116
138
  const shape = {};
139
+ const st = status[`${method.toUpperCase()} ${openapiPath}`];
140
+ const pdoc = (name, d) => d || st?.params?.[name];
117
141
  for (const p of op.parameters ?? []) {
118
142
  if (p.in === "path") {
119
143
  pathParamNames.push(p.name);
120
- shape[p.name] = zodFor({ type: p.schema?.type, required: true, description: p.description });
144
+ shape[p.name] = zodFor({ type: p.schema?.type, required: true, description: pdoc(p.name, p.description) });
121
145
  }
122
146
  else if (p.in === "query") {
123
147
  queryParamNames.push(p.name);
124
- shape[p.name] = zodFor({ type: p.schema?.type, required: p.required, description: p.description });
148
+ shape[p.name] = zodFor({ type: p.schema?.type, required: p.required, description: pdoc(p.name, p.description) });
125
149
  }
126
150
  }
127
151
  const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
@@ -129,12 +153,18 @@ function registerServiceTools(server, service, doc, doFetch) {
129
153
  const required = new Set(bodySchema.required ?? []);
130
154
  for (const [name, prop] of Object.entries(bodySchema.properties ?? {})) {
131
155
  bodyPropNames.push(name);
132
- shape[name] = zodFor({ type: prop.type, required: required.has(name), description: prop.description });
156
+ shape[name] = zodFor({ type: prop.type, required: required.has(name), description: pdoc(name, prop.description) });
133
157
  }
134
158
  }
135
159
  const name = toolNameFor(service, method, openapiPath);
160
+ if (typeof st?.auth === "boolean")
161
+ PRICED.set(name, st.auth);
162
+ // Specs often repeat the summary at the start of the description; say it once.
163
+ const summary = (op.summary ?? "").trim();
164
+ const detail = (op.description ?? "").trim();
165
+ const text = !detail ? summary : !summary || detail.toLowerCase().startsWith(summary.toLowerCase().replace(/[.\s]+$/, "")) ? detail : `${summary} - ${detail}`;
136
166
  server.registerTool(name, {
137
- description: `[${service.name}] ${[op.summary, op.description].filter(Boolean).join(" — ")}`.slice(0, 1000),
167
+ description: `[${service.name}] ${text}`.slice(0, 900),
138
168
  inputSchema: shape,
139
169
  }, async (args) => {
140
170
  let urlPath = openapiPath;
@@ -331,14 +361,14 @@ function registerPrompts(server) {
331
361
  };
332
362
  });
333
363
  }
334
- export const SERVER_VERSION = "0.7.1";
364
+ export const SERVER_VERSION = "0.7.3";
335
365
  export async function loadWorld() {
336
366
  const doFetch = await buildFetcher();
337
367
  const specs = [];
338
368
  for (const service of SERVICES) {
339
369
  const doc = await loadServiceSpec(service);
340
370
  if (doc)
341
- specs.push({ service, doc });
371
+ specs.push({ service, doc, status: await loadStatusCatalog(service) });
342
372
  }
343
373
  if (specs.length === 0) {
344
374
  throw new Error("No service specs loaded - check network access and WICKEDAPI_*_URL overrides.");
@@ -347,12 +377,13 @@ export async function loadWorld() {
347
377
  }
348
378
  export function buildServer(world, hooks) {
349
379
  const server = new McpServer({ name: "wickedapi", version: SERVER_VERSION });
380
+ decorateRegistry(server, (tool) => PRICED.get(tool));
350
381
  if (hooks)
351
382
  instrument(server, hooks);
352
383
  let totalTools = 0;
353
384
  const loaded = [];
354
- for (const { service, doc } of world.specs) {
355
- totalTools += registerServiceTools(server, service, doc, world.doFetch);
385
+ for (const { service, doc, status } of world.specs) {
386
+ totalTools += registerServiceTools(server, service, doc, world.doFetch, status);
356
387
  loaded.push(service.name);
357
388
  }
358
389
  // Reputation, Registry, Identity, Sanity, Memory and Paywall use hand-written
package/dist/suite.js CHANGED
@@ -9,6 +9,11 @@
9
9
  // registration and Memory take a signature the caller produced themselves.
10
10
  import { createHash, randomBytes } from "node:crypto";
11
11
  import { z } from "zod";
12
+ import { cacheGet, cacheKeyOf, cacheSet, reserveOpenMode } from "./fair.js";
13
+ const MIN = 60_000;
14
+ // Grounded verdicts depend only on the claim and the context supplied, so they stay valid longer than open-mode
15
+ // ones, which reflect live web evidence.
16
+ const sanityTtl = (mode) => ((mode ?? "grounded") === "open" ? 10 * MIN : 60 * MIN);
12
17
  function envUrl(name, fallback) {
13
18
  return (process.env[name] || fallback).replace(/\/$/, "");
14
19
  }
@@ -17,7 +22,9 @@ const REGISTRY_URL = envUrl("REGISTRY_BASE_URL", "https://registry.wickedapi.com
17
22
  const IDENTITY_URL = envUrl("IDENTITY_BASE_URL", "https://verify.wickedapi.com");
18
23
  const SANITY_URL = envUrl("SANITY_BASE_URL", "https://sanity.wickedapi.com");
19
24
  const MEMORY_URL = envUrl("MEMORY_BASE_URL", "https://memory.wickedapi.com");
20
- const PAYWALL_URL = envUrl("PAYWALL_BASE_URL", "https://paywall.wickedapi.com");
25
+ // The tenant/config API lives on paywall-admin; paywall.wickedapi.com is the gateway that serves the paid routes.
26
+ const PAYWALL_URL = envUrl("PAYWALL_BASE_URL", "https://paywall-admin.wickedapi.com");
27
+ const PAYWALL_GATEWAY_URL = envUrl("PAYWALL_GATEWAY_URL", "https://paywall.wickedapi.com");
21
28
  const REGISTRY_API_KEY = process.env.REGISTRY_API_KEY || "";
22
29
  const IDENTITY_API_KEY = process.env.IDENTITY_API_KEY || "";
23
30
  const SANITY_API_KEY = process.env.SANITY_API_KEY || "";
@@ -87,8 +94,19 @@ async function call(method, base, path, opts = {}) {
87
94
  headers["content-type"] = "application/json";
88
95
  init.body = JSON.stringify(clean(opts.json));
89
96
  }
97
+ const cacheKey = opts.cacheTtlMs ? cacheKeyOf(method, url.toString(), init.body ?? null) : "";
98
+ if (cacheKey) {
99
+ const hit = cacheGet(cacheKey);
100
+ if (hit)
101
+ return { ...hit, cached: true };
102
+ }
103
+ const refused = opts.gate?.();
104
+ if (refused)
105
+ return refused;
90
106
  try {
91
107
  const body = await finalize(await fetch(url.toString(), init));
108
+ if (cacheKey && body.http_status === 200)
109
+ cacheSet(cacheKey, body, opts.cacheTtlMs);
92
110
  // These services answer 402 both when no key is sent and when a key is
93
111
  // rejected, so the status alone can't say which. Say what THIS server did
94
112
  // (never the key itself), so an agent or operator can tell the cases apart.
@@ -234,7 +252,7 @@ export function registerSuiteTools(server) {
234
252
  };
235
253
  const wallet = z.string().describe("The 0x wallet address.");
236
254
  // ---- Wicked Reputation (all public, free) ----
237
- tool("reputation_status", "[Wicked Reputation] An agent's current reputation: tier, active/unbonding stake, score, slash count, founding-member badge. http_status 404 means the wallet never registered — treat as tier 'unranked'.", { wallet }, ({ wallet }) => call("GET", REPUTATION_URL, `/agents/${addr(wallet)}/status`));
255
+ tool("reputation_status", "[Wicked Reputation] An agent's current reputation: tier, active/unbonding stake, score, slash count, founding-member badge. http_status 404 means the wallet never registered — treat as tier 'unranked'.", { wallet }, ({ wallet }) => call("GET", REPUTATION_URL, `/agents/${addr(wallet)}/status`, { cacheTtlMs: 2 * MIN }));
238
256
  tool("reputation_statement", "[Wicked Reputation] An agent's full position plus real event ledger (stakes, unstakes, withdrawals, slashes). Does not include WickedAPI call volume or fees — see the `note` field.", { wallet }, ({ wallet }) => call("GET", REPUTATION_URL, `/agents/${addr(wallet)}/statement`));
239
257
  tool("reputation_query", "[Wicked Reputation] Search/rank registered agents by tier, stake, reputation or slash history. Every filter is a typed query param.", {
240
258
  tier: z.string().optional().describe('Exact tier name, e.g. "gold".'),
@@ -279,7 +297,7 @@ export function registerSuiteTools(server) {
279
297
  endpoint: z.string().optional().describe("Exact endpoint URL of the tool you are about to call."),
280
298
  category: z.string().optional(),
281
299
  limit: z.coerce.number().optional().describe("1-25, default 10."),
282
- }, (a) => call("GET", REGISTRY_URL, "/lookup", { query: clean(a) }));
300
+ }, (a) => call("GET", REGISTRY_URL, "/lookup", { query: clean(a), cacheTtlMs: 5 * MIN }));
283
301
  tool("registry_onboarding_message", "[Wicked Registry] Step 1 of self-onboarding. Returns the exact message YOU must sign (EIP-191 personal_sign) plus its timestamp, valid for about an hour. action=api_key (needs wallet) for a free API key, or action=register (needs name and endpoint_url) to list your own tool. This tool never signs anything.", {
284
302
  action: z.enum(["api_key", "register"]),
285
303
  wallet: z.string().optional().describe("Your 0x wallet address (required for api_key)."),
@@ -294,8 +312,8 @@ export function registerSuiteTools(server) {
294
312
  }, (a) => call("POST", REGISTRY_URL, "/keys", { json: { wallet: a.wallet, timestamp: a.timestamp, signature: a.signature, label: a.label } }));
295
313
  tool("registry_key_info", "[Wicked Registry] Metadata and usage for a Registry API key (wr_...): label, created time, call counts. Pass api_key, or set REGISTRY_API_KEY.", { api_key: z.string().optional().describe("Registry API key to inspect. Overrides REGISTRY_API_KEY.") }, ({ api_key }) => call("GET", REGISTRY_URL, "/keys/self", { headers: keyHeader(api_key || REGISTRY_API_KEY) }));
296
314
  tool("registry_revoke_key", "[Wicked Registry] PERMANENTLY revoke a Registry API key (the one presented). Use if a key leaked. Pass api_key, or set REGISTRY_API_KEY.", { api_key: z.string().optional().describe("Registry API key to revoke. Overrides REGISTRY_API_KEY.") }, ({ api_key }) => call("DELETE", REGISTRY_URL, "/keys/self", { headers: keyHeader(api_key || REGISTRY_API_KEY) }));
297
- tool("registry_featured_tools", "[Wicked Registry] Top scored active tools. Public and free.", {}, () => call("GET", REGISTRY_URL, "/featured-tools"));
298
- tool("registry_tool_badge", "[Wicked Registry] A tool's current composite reliability score. Public and free.", { tool_id: z.string().describe("The tool's Wicked Registry id.") }, ({ tool_id }) => call("GET", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}/badge`));
315
+ tool("registry_featured_tools", "[Wicked Registry] Top scored active tools. Public and free.", {}, () => call("GET", REGISTRY_URL, "/featured-tools", { cacheTtlMs: 5 * MIN }));
316
+ tool("registry_tool_badge", "[Wicked Registry] A tool's current composite reliability score. Public and free.", { tool_id: z.string().describe("The tool's Wicked Registry id.") }, ({ tool_id }) => call("GET", REGISTRY_URL, `/tools/${encodeURIComponent(tool_id)}/badge`, { cacheTtlMs: 5 * MIN }));
299
317
  tool("registry_report_tool", "[Wicked Registry] File a complaint about a tool's reliability or behaviour. Public, rate-limited; reviewed manually, never auto-suspends.", {
300
318
  tool_id: z.string().describe("The tool's Wicked Registry id."),
301
319
  reporter: z.string().describe("Your wallet address or another best-effort identifier."),
@@ -354,8 +372,8 @@ export function registerSuiteTools(server) {
354
372
  .optional()
355
373
  .describe('Source text the claim(s) must be faithful to. Required for mode "grounded".');
356
374
  const modeShape = z.string().optional().describe('"grounded" (default; check against context) or "open" (check against live web evidence — consistency with the web, not objective truth).');
357
- tool("sanity_check", '[Wicked Sanity] Verify one claim before acting on it. Verdicts: supported | contradicted | unsupported | insufficient_evidence (see `data`). confidence_score is raw consistency (a "contradicted" verdict scores near 0). Policy: act on supported; treat unsupported/insufficient_evidence as unverified; reject contradicted. x402 or SANITY_API_KEY; 429 means a rate limit — see retry_after.', { claim: z.string().describe("The single statement to verify (max 5,000 chars)."), context: contextShape, mode: modeShape }, ({ claim, context, mode }) => call("POST", SANITY_URL, "/check", { headers: keyHeader(SANITY_API_KEY), json: { claim, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS }));
358
- tool("sanity_check_batch", "[Wicked Sanity] Verify up to 50 claims against one shared context in a single call — prefer this over one sanity_check per sentence. `data` is a list in the same order as `claims`.", { claims: z.array(z.string()).min(1).max(50).describe("1-50 statements, each up to 5,000 chars."), context: contextShape, mode: modeShape }, ({ claims, context, mode }) => call("POST", SANITY_URL, "/check/batch", { headers: keyHeader(SANITY_API_KEY), json: { claims, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS }));
375
+ tool("sanity_check", '[Wicked Sanity] Verify one claim before acting on it. Verdicts: supported | contradicted | unsupported | insufficient_evidence (see `data`). confidence_score is raw consistency (a "contradicted" verdict scores near 0). Policy: act on supported; treat unsupported/insufficient_evidence as unverified; reject contradicted. x402 or SANITY_API_KEY; 429 means a rate limit — see retry_after.', { claim: z.string().describe("The single statement to verify (max 5,000 chars)."), context: contextShape, mode: modeShape }, ({ claim, context, mode }) => call("POST", SANITY_URL, "/check", { headers: keyHeader(SANITY_API_KEY), json: { claim, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS, cacheTtlMs: sanityTtl(mode), gate: () => ((mode ?? "grounded") === "open" ? reserveOpenMode(1) : null) }));
376
+ tool("sanity_check_batch", "[Wicked Sanity] Verify up to 50 claims against one shared context in a single call — prefer this over one sanity_check per sentence. `data` is a list in the same order as `claims`.", { claims: z.array(z.string()).min(1).max(50).describe("1-50 statements, each up to 5,000 chars."), context: contextShape, mode: modeShape }, ({ claims, context, mode }) => call("POST", SANITY_URL, "/check/batch", { headers: keyHeader(SANITY_API_KEY), json: { claims, context, mode: mode ?? "grounded" }, timeoutMs: SANITY_TIMEOUT_MS, cacheTtlMs: sanityTtl(mode), gate: () => ((mode ?? "grounded") === "open" ? reserveOpenMode(claims.length) : null) }));
359
377
  tool("sanity_get_check", "[Wicked Sanity] Fetch a previously run check by its check_id (returned by sanity_check / sanity_check_batch). x402 or SANITY_API_KEY like the other Sanity calls.", { check_id: z.string().describe("The check_id from an earlier sanity_check result.") }, ({ check_id }) => call("GET", SANITY_URL, `/checks/${encodeURIComponent(check_id)}`, { headers: keyHeader(SANITY_API_KEY) }));
360
378
  // ---- Wicked Memory (wallet-scoped, signature per call) ----
361
379
  tool("memory_prepare", 'Wicked Memory step 1 of every call: returns the exact `message_to_sign`. Sign it with your own wallet (EIP-191 personal_sign), then call the matching memory_* tool with the SAME arguments plus the returned timestamp, nonce and your signature. Timestamp must be within 5 minutes; nonce is single-use, so prepare again for every call.', {
@@ -434,6 +452,7 @@ export function registerSuiteTools(server) {
434
452
  description: z.string().optional().describe("Up to 200 chars: letters, digits, space . _ -"),
435
453
  upstreamUrl: z.string().optional().describe("Your https backend. Omit for the demo confirmation payload."),
436
454
  };
455
+ tool("paywall_demo_weather", "[x402 Paywall] Call the live demo route on the paywall gateway (GET /wickedapi/weather) to see a real x402 challenge: unpaid, it returns HTTP 402 with the decoded price, network and payTo in payment_required. A safe way to learn what a paywalled endpoint looks like before creating your own.", {}, () => call("GET", PAYWALL_GATEWAY_URL, "/wickedapi/weather"));
437
456
  tool("paywall_signup", "[x402 Paywall] Create a tenant account. Returns a Bearer apiKey ONCE — store it and set PAYWALL_API_KEY. Rate-limited to 5/hour per IP.", {
438
457
  name: z.string().describe("Letters, digits, space . _ - (max 100)."),
439
458
  slug: z.string().describe("Lowercase a-z0-9 and hyphens, 2-40 chars; becomes your mount path prefix /{slug}."),
@@ -0,0 +1,172 @@
1
+ const USAGE = {
2
+ // --- Trading Data API (generated) ---
3
+ price: { u: "you need the current spot price of one crypto asset or US stock/ETF.", n: "history or candles (use ohlcv), or a trading view (use momentum_score or verdict)." },
4
+ ohlcv: { u: "you need historical candles to chart or compute indicators yourself.", n: "only the latest price (use price) or a ready-made indicator (atr, fvg, liquidity_levels)." },
5
+ atr: { u: "you need volatility for position sizing or stops: Wilder ATR plus a low/normal/high/extreme regime.", n: "implied volatility for BTC/ETH (use volatility)." },
6
+ fvg: { u: "you want Fair Value Gap zones (ICT-style imbalances) with fill status and distance from price.", n: "swing-high/low liquidity pools (use liquidity_levels)." },
7
+ liquidity_levels: { u: "you want buy-side/sell-side liquidity levels and whether they were swept.", n: "gap zones (use fvg) or actual liquidation events (use liquidations)." },
8
+ momentum_score: { u: "you want a ready-made LONG/SHORT/NEUTRAL view with confidence, thesis and entry/invalidation/targets for a supported asset.", n: "raw inputs (use price, atr, funding_oi). Same data as verdict, which is the newer name." },
9
+ verdict: { u: "you want one call that returns a LONG/SHORT/NEUTRAL bias with a 0-10 confidence and a one-line thesis.", n: "the full structured levels (use momentum_score, an alias of the same engine) or raw inputs (use price, atr)." },
10
+ funding_oi: { u: "you want perp funding rates and open interest aggregated from Binance and Bybit to gauge leverage and crowding.", n: "Hyperliquid-specific data (use hyperliquid) or spot-vs-perp carry (use basis)." },
11
+ long_short_ratio: { u: "you want Binance positioning ratios (global accounts, top traders, taker buy/sell).", n: "funding or open interest (use funding_oi)." },
12
+ orderbook: { u: "you need top-of-book depth and spread (bps) on Binance, Bybit or Hyperliquid.", n: "a prediction-market book (use kalshi_market)." },
13
+ basis: { u: "you want the spot-vs-perp premium and funding-implied annualized carry for a basis trade.", n: "raw funding data (use funding_oi)." },
14
+ hyperliquid: { u: "you need Hyperliquid perp context: funding, open interest, mark/oracle price, premium and volume.", n: "Binance/Bybit aggregates (use funding_oi)." },
15
+ volatility: { u: "you want Deribit implied volatility (DVOL) next to realized volatility for BTC or ETH.", n: "other assets or ATR-style sizing (use atr)." },
16
+ sentiment: { u: "you want the crypto Fear & Greed reading as a quick market-mood signal.", n: "asset-specific direction (use momentum_score)." },
17
+ defi_tvl: { u: "you need DeFi TVL by chain, stablecoin supply or yield pools from DefiLlama.", n: "the health or risk of one protocol (use protocol_health_score)." },
18
+ rates: { u: "you need the US Treasury yield curve and recession spreads (10y-2y, 10y-3m).", n: "event dates (use macro_calendar)." },
19
+ market_clock: { u: "you need to know whether US equity markets are open and when the next session opens or closes.", n: "crypto, which trades 24/7." },
20
+ market_overview: { u: "you want global crypto stats: total market cap, 24h volume, BTC and ETH dominance.", n: "a single asset's price (use price)." },
21
+ gas: { u: "you need current Ethereum gas prices (safe, standard, fast) and the base fee before sending a transaction.", n: "other chains." },
22
+ macro_calendar: { u: "you want upcoming US macro events (FOMC, CPI, jobs) with verified dates.", n: "company earnings (use earnings_calendar)." },
23
+ earnings_calendar: { u: "you want upcoming US earnings dates, timing and analyst estimates.", n: "macro events (use macro_calendar) or filed reports (use filings_search)." },
24
+ kalshi_probability: { u: "you want live Kalshi prediction-market probabilities by category or ticker.", n: "a single market's depth (use kalshi_market) or Polymarket (use polymarket)." },
25
+ kalshi_market: { u: "you need one Kalshi market's orderbook and recent trades by ticker.", n: "browsing markets by category (use kalshi_probability)." },
26
+ polymarket: { u: "you want Polymarket markets with live per-outcome probabilities and volume.", n: "Kalshi markets (use kalshi_probability)." },
27
+ chain_balance: { u: "you need the native-token balance (ETH/POL) of an address or ENS name on Ethereum, Base, Arbitrum, Optimism or Polygon.", n: "token balances (use chain_token_balance) or a full holdings view (use wallet_portfolio)." },
28
+ chain_token_balance: { u: "you need the balance of one ERC-20 token for an address or ENS name.", n: "native balance (chain_balance) or everything a wallet holds (wallet_portfolio)." },
29
+ chain_nonce: { u: "you need an address's confirmed and pending transaction count before building a transaction.", n: "balances." },
30
+ chain_block: { u: "you need a block header (number, hash, time, miner, gas, base fee), latest by default.", n: "a single transaction (use chain_tx)." },
31
+ chain_ens: { u: "you need to resolve an ENS name to an address, or an address to its primary ENS name.", n: "non-ENS lookups." },
32
+ chain_tx: { u: "you need the status and details of a transaction by hash.", n: "account-level data (use chain_balance or chain_nonce)." },
33
+ wallet_portfolio: { u: "you want what a wallet holds in one call: native balance plus major ERC-20 tokens across supported chains.", n: "one specific token (use chain_token_balance) or counterparty risk (use wallet_check)." },
34
+ web_search: { u: "an agent needs fresh web results with titles, URLs and snippets.", n: "verifying a claim (use sanity_check with mode open)." },
35
+ crypto_news: { u: "you want the latest crypto headlines from major outlets, newest first.", n: "market data." },
36
+ liquidations: { u: "you want recent forced liquidations on OKX USDT perpetuals for a coin, with side and size.", n: "liquidity pools (use liquidity_levels)." },
37
+ // --- Protocol Health Oracle (generated) ---
38
+ protocol_health_scoreboard: { u: "you want the free public ranking of every tracked DeFi protocol: name, score and verdict only.", n: "the component breakdown for one protocol (use protocol_health_score)." },
39
+ protocol_health_list_protocols: { u: "you need every tracked protocol with its latest score and verdict.", n: "one protocol's details (use protocol_health_score)." },
40
+ protocol_health_score: { u: "you are vetting one protocol before integrating it and want GitHub activity, TVL trend and treasury components.", n: "a ranking (use protocol_health_scoreboard) or token contract risk (use token_risk)." },
41
+ protocol_health_track_protocol: { u: "a protocol you care about is not tracked yet and you want it scored from live GitHub and DefiLlama data.", n: "reading an existing score." },
42
+ protocol_health_refresh_all: { u: "you need every tracked protocol re-scored right now instead of waiting for the schedule.", n: "a single protocol (read it with protocol_health_score)." },
43
+ protocol_health_sync_universe: { u: "you want the tracked universe re-synced from DefiLlama (TVL above $1M), which also runs automatically on a schedule.", n: "routine reads." },
44
+ // --- Token Risk / Filings / Wallet (generated) ---
45
+ token_risk: { u: "you are about to buy or interact with a token contract and want honeypot, mint authority, holder and LP concentration and tax signals (EVM and Solana).", n: "wallet or address risk (use wallet_check) or protocol vetting (use protocol_health_score)." },
46
+ filings_freshness: { u: "you want proof the SEC feed is live: seconds since the last poll and ingest lag.", n: "the filings themselves (use filings_search)." },
47
+ filings_latest_preview: { u: "you want a free glimpse of the 10 most recent filings (form, company, ticker, time).", n: "full structured data (use filings_search)." },
48
+ filings_search: { u: "you want structured SEC filings (8-K, Form 4, 13D/13G, 10-K, 10-Q) filtered by ticker, form or time.", n: "classified event alerts (use filings_8k_events) or insider trades (use filings_insider_trades)." },
49
+ filings_8k_events: { u: "you want 8-K material events already classified (earnings, exec change, material agreement and similar).", n: "all form types (use filings_search)." },
50
+ filings_insider_trades: { u: "you want individual Form 4 insider transactions, one row per trade.", n: "a per-ticker rollup (use filings_insider_summary)." },
51
+ filings_insider_summary: { u: "you want a ticker's insider buying and selling rolled up over a window.", n: "the underlying trades (use filings_insider_trades)." },
52
+ filings_filing: { u: "you have an accession number and want that one filing with parsed items and the SEC link.", n: "searching (use filings_search)." },
53
+ wallet_sanctions_check: { u: "you need a free yes/no on whether an address is on the OFAC SDN sanctions list.", n: "broader counterparty risk (use wallet_check)." },
54
+ wallet_check: { u: "you are about to transact and want a pre-transaction check of a wallet or contract: OFAC, malicious-address flags and on-chain signals.", n: "token contract safety (use token_risk)." },
55
+ // --- Broker Sync (generated) ---
56
+ broker_sync_register_user: { u: "you are onboarding a user and need a SnapTrade brokerage connection-portal URL.", n: "reading data for an already connected user." },
57
+ broker_sync_connections: { u: "you want a user's connected brokerage accounts in a normalized shape.", n: "holdings (use broker_sync_positions) or cash (use broker_sync_balances)." },
58
+ broker_sync_positions: { u: "you want a user's real positions across all connected brokerages in one schema.", n: "cash and buying power (use broker_sync_balances)." },
59
+ broker_sync_balances: { u: "you want cash and buying power per connected account.", n: "holdings (use broker_sync_positions)." },
60
+ broker_sync_transactions: { u: "you want a user's transaction and activity history across brokerages.", n: "current holdings (use broker_sync_positions)." },
61
+ broker_sync_prop_firms: { u: "you want the registry of prop-firm platforms and which integration methods are live-verified.", n: "a user's own accounts (use broker_sync_connections)." },
62
+ // --- Reputation ---
63
+ reputation_status: { u: "you want one agent's tier, stake, reputation score and slash count before dealing with it.", n: "the event history (use reputation_statement) or a ranking (use reputation_leaderboard)." },
64
+ reputation_statement: { u: "you need an agent's full position and ledger of stakes, unstakes, withdrawals and slashes.", n: "a quick tier check (use reputation_status)." },
65
+ reputation_query: { u: "you want to filter or rank agents by tier, stake, reputation or slash history.", n: "one known wallet (use reputation_status)." },
66
+ reputation_tiers: { u: "you want the live tier thresholds and benefits (minimum stake, rate-limit multiplier, fee discount).", n: "a specific agent's tier (use reputation_status)." },
67
+ reputation_transparency: { u: "you want the treasury address, custody model, unbonding cooldown and totals, for example before staking.", n: "per-agent data." },
68
+ reputation_leaderboard: { u: "you want the public ranking of agents by active stake, with tier and slash count.", n: "filtered searches (use reputation_query)." },
69
+ reputation_history: { u: "you want how one agent's reputation and tier changed over time.", n: "current standing (use reputation_status)." },
70
+ reputation_erc8004: { u: "you want to cross-reference a wallet against the open ERC-8004 identity and reputation registries.", n: "Wicked's own staking data (use reputation_status)." },
71
+ reputation_get_nonce: { u: "you are about to register, stake, unstake or withdraw and need the one-time message to sign.", n: "read-only lookups, which need no signature." },
72
+ reputation_register: { u: "you want to register your wallet as an agent, with your own signature over the nonce message.", n: "staking (use reputation_stake after funding the treasury)." },
73
+ reputation_stake: { u: "you already sent USDC on-chain to the treasury and want that stake recorded, with its transaction hash and a signed nonce.", n: "moving funds, which this server never does." },
74
+ reputation_unstake: { u: "you want to start the unbonding period for part of your active stake, with a signed nonce.", n: "collecting funds (use reputation_withdraw once the cooldown has passed)." },
75
+ reputation_withdraw: { u: "your unbonding cooldown has elapsed and you want every matured stake paid out to your wallet.", n: "starting the unbonding (use reputation_unstake)." },
76
+ // --- Registry ---
77
+ registry_search_tools: { u: "you want to find x402/MCP tools filtered by category, protocol or minimum reliability score.", n: "a quick free check of a known tool (use registry_lookup)." },
78
+ registry_tool_detail: { u: "you want one tool's full score breakdown and recent real health checks.", n: "finding tools (use registry_search_tools or registry_lookup)." },
79
+ registry_lookup: { u: "you want a free lookup of a tool's id and headline score by name, URL fragment or exact endpoint before you call it.", n: "full detail (use registry_tool_detail)." },
80
+ registry_featured_tools: { u: "you want the top-scored active tools with no key or payment.", n: "filtered search (use registry_search_tools)." },
81
+ registry_tool_badge: { u: "you want one tool's current composite score as a free badge.", n: "the breakdown (use registry_tool_detail)." },
82
+ registry_report_tool: { u: "a tool misbehaved and you want to file a complaint for manual review.", n: "reading scores." },
83
+ registry_register_tool: { u: "you own a tool and want it listed and monitored, with your wallet's signature over the registration message.", n: "finding tools." },
84
+ registry_onboarding_message: { u: "you are starting self-serve onboarding and need the exact message to sign.", n: "when you already have a signature (use registry_create_api_key or registry_register_tool)." },
85
+ registry_create_api_key: { u: "you signed the onboarding message and want a free Registry API key (shown once).", n: "inspecting a key (use registry_key_info)." },
86
+ registry_key_info: { u: "you want a Registry API key's label, creation time and usage counts.", n: "creating or revoking a key." },
87
+ registry_revoke_key: { u: "a Registry API key leaked or is no longer needed and must be disabled permanently.", n: "inspecting a key (use registry_key_info)." },
88
+ // --- Identity ---
89
+ identity_get_nonce: { u: "you are about to register or take a challenge and need a fresh one-time message to sign.", n: "checking someone's status, which needs no signature." },
90
+ identity_register: { u: "you want a lightweight identity record for your wallet, with a signature over the nonce message.", n: "proving you are an agent (use identity_get_challenge next)." },
91
+ identity_get_challenge: { u: "your wallet is registered and you want the time-boxed agent-liveness challenge (about 12 seconds to answer).", n: "registering first (use identity_register)." },
92
+ identity_submit_response: { u: "you have the challenge answer and want it scored and, on a pass, a signed assertion token.", n: "getting the challenge (use identity_get_challenge)." },
93
+ identity_status: { u: "you want to know whether a wallet currently holds a valid, unexpired agent assertion.", n: "becoming verified (start with identity_get_nonce)." },
94
+ identity_jwks: { u: "you want the public keys to verify an assertion token locally.", n: "checking a wallet's status (use identity_status)." },
95
+ identity_sandbox_register: { u: "you have no wallet and want a sandbox identity to test the flow.", n: "production verification, which needs a real wallet signature." },
96
+ identity_get_test_key: { u: "you are testing the Identity API and want a free test key for the sandbox.", n: "production use." },
97
+ // --- Sanity ---
98
+ sanity_check: { u: "you must verify one claim before acting on it, either against source text you supply or against live web evidence.", n: "many claims at once (use sanity_check_batch)." },
99
+ sanity_check_batch: { u: "you have a multi-sentence output to verify against one shared context in a single call (up to 50 claims).", n: "a single claim (use sanity_check)." },
100
+ sanity_get_check: { u: "you want to re-read an earlier check by its check_id.", n: "running a new check." },
101
+ // --- Memory ---
102
+ memory_prepare: { u: "you are about to call any other memory tool and need the exact message to sign for that request.", n: "the call itself." },
103
+ memory_store: { u: "you want to save something for later, scoped to your wallet.", n: "changing an existing memory (use memory_update)." },
104
+ memory_search: { u: "you want to recall your memories by meaning rather than by id.", n: "fetching a known id (use memory_get)." },
105
+ memory_get: { u: "you have a memory id and want its content.", n: "searching by meaning (use memory_search)." },
106
+ memory_update: { u: "you want to change a memory while keeping its history (a new version supersedes the old).", n: "removing it (use memory_delete)." },
107
+ memory_history: { u: "you want a memory's full version chain and validity windows.", n: "its current content (use memory_get)." },
108
+ memory_delete: { u: "you must permanently erase a memory (right to be forgotten).", n: "correcting it (use memory_update, which keeps history)." },
109
+ memory_deletions: { u: "you want your audit log of deletions (ids, times, reasons, never content).", n: "recovering content, which is gone." },
110
+ // --- Paywall ---
111
+ paywall_demo_weather: { u: "you want to see a real x402 payment challenge from a paywalled route (price, network, payTo) without creating anything.", n: "setting up your own paywall (use paywall_signup then paywall_create_route)." },
112
+ paywall_signup: { u: "you want a paywall tenant account and its API key (shown once).", n: "managing routes of an existing tenant." },
113
+ paywall_get_tenant: { u: "you want your tenant record (name, slug, key hint).", n: "route management." },
114
+ paywall_list_routes: { u: "you want your paywalled routes and their prices.", n: "earnings (use paywall_list_settlements)." },
115
+ paywall_create_route: { u: "you want to put one HTTP endpoint behind x402 pay-per-call, paid in USDC straight to your wallet.", n: "many routes at once (use paywall_import_routes)." },
116
+ paywall_import_routes: { u: "you want to paywall many routes in one call (upsert on method and path).", n: "a single route (use paywall_create_route)." },
117
+ paywall_update_route: { u: "you want to change a route's price, payout wallet or upstream without a redeploy.", n: "creating or deleting routes." },
118
+ paywall_delete_route: { u: "you want a route removed from the paywall.", n: "changing its price (use paywall_update_route)." },
119
+ paywall_list_settlements: { u: "you want your real on-chain USDC settlements (earnings).", n: "route configuration (use paywall_list_routes)." },
120
+ };
121
+ const WRITES = new Set([
122
+ "protocol_health_track_protocol", "protocol_health_refresh_all", "protocol_health_sync_universe", "broker_sync_register_user",
123
+ "registry_register_tool", "registry_report_tool", "registry_create_api_key",
124
+ "identity_get_nonce", "identity_register", "identity_get_challenge", "identity_submit_response", "identity_sandbox_register", "identity_get_test_key",
125
+ "reputation_get_nonce", "reputation_register", "reputation_stake", "reputation_unstake", "reputation_withdraw",
126
+ "sanity_check", "sanity_check_batch", "memory_store", "memory_update",
127
+ "paywall_signup", "paywall_create_route", "paywall_import_routes", "paywall_update_route",
128
+ ]);
129
+ const DESTRUCTIVE = new Set(["memory_delete", "paywall_delete_route", "registry_revoke_key", "reputation_unstake", "reputation_withdraw"]);
130
+ const IDEMPOTENT_WRITES = new Set(["paywall_update_route", "paywall_delete_route", "registry_revoke_key"]);
131
+ const LOCAL_ONLY = new Set(["memory_prepare"]);
132
+ function humanize(name) {
133
+ return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
134
+ }
135
+ export function annotationsFor(name) {
136
+ const write = WRITES.has(name) || DESTRUCTIVE.has(name);
137
+ return {
138
+ title: humanize(name),
139
+ readOnlyHint: !write,
140
+ destructiveHint: DESTRUCTIVE.has(name),
141
+ idempotentHint: !write || IDEMPOTENT_WRITES.has(name),
142
+ openWorldHint: !LOCAL_ONLY.has(name),
143
+ };
144
+ }
145
+ function behavior(name, ctx) {
146
+ if (LOCAL_ONLY.has(name))
147
+ return "Behavior: pure local computation, no network call, nothing is stored.";
148
+ if (DESTRUCTIVE.has(name))
149
+ return "Behavior: DESTRUCTIVE and not reversible; confirm intent before calling.";
150
+ if (WRITES.has(name))
151
+ return "Behavior: writes state upstream (not read-only); needs the credentials or wallet signature noted above where applicable.";
152
+ if (ctx.priced === true) {
153
+ return "Behavior: read-only. Priced via x402 (USDC on Base) or free with WICKEDAPI_API_KEY; with neither, you get HTTP 402 with the decoded price in payment_required (a price quote, not a failure).";
154
+ }
155
+ if (ctx.priced === false)
156
+ return "Behavior: read-only and free; no key or payment needed.";
157
+ return "Behavior: read-only.";
158
+ }
159
+ /** Final description: original text, then usage guidance, then behavior. */
160
+ export function describeTool(name, base, ctx = {}) {
161
+ const u = USAGE[name];
162
+ const parts = [base.trim()];
163
+ if (u)
164
+ parts.push(`Use when ${u.u}${u.n ? ` Not for ${u.n}` : ""}`);
165
+ parts.push(behavior(name, ctx));
166
+ return parts.join("\n\n");
167
+ }
168
+ /** Wrap registerTool so every tool gets usage text and annotations. Call before registering. */
169
+ export function decorateRegistry(server, priceOf) {
170
+ const original = server.registerTool.bind(server);
171
+ server.registerTool = (name, config, handler) => original(name, { ...config, description: describeTool(name, config.description ?? "", { priced: priceOf(name) }), annotations: annotationsFor(name) }, handler);
172
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wickedlabs/wicked-mcp",
3
- "version": "0.7.1",
4
- "description": "MCP server for the whole Wicked suite — trading data, DeFi protocol health, token safety, SEC filings alerts, wallet and sanctions checks, brokerage sync, agent identity, reputation, tool registry, hallucination checks, persistent memory, and x402 paywalls. 105 tools, 6 resources, 3 prompts. No signup required: pay per call in USDC via x402, or use a partner API key.",
3
+ "version": "0.7.3",
4
+ "description": "MCP server for the whole Wicked suite — trading data, DeFi protocol health, token safety, SEC filings alerts, wallet and sanctions checks, brokerage sync, agent identity, reputation, tool registry, hallucination checks, persistent memory, and x402 paywalls. 100+ tools, 6 resources, 3 prompts. No signup required: pay per call in USDC via x402, or use a partner API key.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "wickedapi-mcp": "dist/index.js"