coinopai-mcp 2.0.2 → 2.2.0

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
@@ -8,6 +8,7 @@
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
9
9
  [![payments](https://img.shields.io/badge/payments-x402%20USDC-0052FF)](https://x402.org)
10
10
  [![network](https://img.shields.io/badge/network-Base-0052FF)](https://base.org)
11
+ [![mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/forgemeshlabs/coinopai-mcp)
11
12
 
12
13
  **Auditable market context with calibrated forecast ranges and outcome verification.**
13
14
 
@@ -167,6 +168,9 @@ Fresh audits can return `pending_window` until the evaluation window matures.
167
168
  | `get_crypto_signals` | Model context for BTC, ETH, SOL, XRP, ADA | $0.05 | ✓ |
168
169
  | `get_crypto_signal_history` | Up to 168h of context history for analysis | $0.05 | ✓ |
169
170
  | `get_crypto_forecast` | Conformally-calibrated 80% price range (~0.80 empirical coverage) for BTC, ETH, SOL, XRP, ADA | $0.05 | ✓ |
171
+ | `get_perp_funding` | **Futures.** Live perp funding, mark/index, open interest, crowding (Kraken Futures + Hyperliquid) | $0.02 | — |
172
+ | `check_futures_risk` | **Futures.** Will this side / leverage / entry survive the calibrated range? Liquidation distance + verdict | $0.05 | — |
173
+ | `get_futures_decision` | **Futures.** LONG / SHORT / FLAT with stop and target on the calibrated range, leverage cap, liquidation, sizing, funding cost | $0.15 | — |
170
174
  | `review_signal_anomaly` | Score signal features for unusual conditions; returns review labels, drivers, and component scores | $0.07 | — |
171
175
  | `get_crypto_risk` | Market risk state and cooldown context | $0.02 | — |
172
176
  | `search_agent_automations` | Search 819 agent automation prompts | $0.01 | — |
@@ -306,6 +310,27 @@ Route to an available symbol or wait 15 minutes for the next cycle.
306
310
 
307
311
  ---
308
312
 
313
+ ## Sponsored cards (Lulu Ads)
314
+
315
+ Free-tier tool responses may carry a single disclosed sponsored card from [Lulu Ads](https://getlulu.dev). It is a plain, labelled data field on the JSON result — never text the model could read as an instruction:
316
+
317
+ ```json
318
+ {
319
+ "results": [...],
320
+ "sponsored": { "label": "Sponsored", "text": "...", "url": "https://..." }
321
+ }
322
+ ```
323
+
324
+ - **Eligible tools:** `search_agent_automations`, `list_automation_categories`, `get_agent_automation`, `check_trade_preflight` — and only when the response did **not** settle an x402 payment (no `_payment` receipt). A paid response never carries a card.
325
+ - **Current state:** one tool is free — `list_tools` (a plain fetch of `/menu`, no wallet, no payment) — so that is the only tool where a card can render today. The other four allowlisted tools are still paid, so no card attaches to them; the switch is server-side: when the operator makes one of those endpoints free (a plain `200` with no `402`), cards start appearing there with no client change.
326
+ - **Paid tools never carry it:** `get_crypto_signals`, `get_crypto_risk`, `get_crypto_signal_history`, `get_crypto_decision`, `audit_trade_decision`, `get_crypto_forecast`, `review_signal_anomaly`, `get_futures_decision`, `get_perp_funding`, `check_futures_risk` never touch the ads SDK.
327
+ - **Strip it:** `delete result.sponsored`.
328
+ - **Where the card comes from:** ForgeMesh's own publisher card is attached server-side to `GET https://x402.coinopai.com/menu`, so `list_tools` simply passes it through — this package ships **no** ad credentials and makes **no** calls to the ads network on its own.
329
+ - **Operator env vars (forks only):** `LULU_ADS_PUBLISHER_ID` and `LULU_ADS_API_KEY` let a fork attach its own publisher card client-side (both required; if either is missing the package makes zero calls to the ads network and responses are unchanged). `LULU_ADS_ENABLED=false` is a kill switch.
330
+ - **Fail-open:** any SDK error or timeout (hard budget 2s) returns the original response unchanged. The only external host contacted is `ads.getlulu.dev`.
331
+
332
+ ---
333
+
309
334
  ## Affiliate Attribution (via Pyrimid)
310
335
 
311
336
  High-value tools accept an optional `affiliate_id` parameter. When provided, payment routes through the [Pyrimid](https://pyrimid.xyz) affiliate network — the affiliate earns a commission split from within the listed price. **No extra cost to the caller.**
package/ads.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ // Lulu Ads sponsored cards — disclosed data field, free-tier tools only.
4
+ //
5
+ // Contract:
6
+ // - Card attaches ONLY to tools in FREE_TIER_TOOLS, and only when the
7
+ // response carries no x402 `_payment` settlement receipt. A response
8
+ // that settled a payment never carries a card, even for an allowlisted
9
+ // tool. Every other tool (Kronos decision loop, anomaly review) is
10
+ // excluded by name and never touches the SDK.
11
+ // - The card is a plain data field `sponsored: {label, text, url}` on the
12
+ // tool's JSON result — never injected into prose the model could read
13
+ // as an instruction. Strip it with `delete result.sponsored`.
14
+ // - This package ships NO ad credentials. ForgeMesh's own card is attached
15
+ // server-side to GET /menu and passes through `list_tools` untouched
16
+ // (see "never clobber an upstream field" below). The env path here
17
+ // (LULU_ADS_PUBLISHER_ID, LULU_ADS_API_KEY) exists for forks that want
18
+ // their own publisher card client-side. Either missing → zero network
19
+ // calls, response unchanged. LULU_ADS_ENABLED=false is a kill switch.
20
+ // - Fail-open with a hard time budget: SDK error, timeout, or import
21
+ // failure → original response unchanged. Never throws.
22
+ //
23
+ // The SDK is deliberately NOT wired via `enableLuluAds`/`withLuluAds`
24
+ // from "lulu-ads/mcp": those wrap every registered tool, which would put
25
+ // cards on paid tools.
26
+
27
+ // Tools whose responses may carry a card. Everything else is paid-only.
28
+ //
29
+ // NOTE: `list_tools` is the one genuinely free tool (plain fetch of /menu,
30
+ // no callPaid, no `_payment`), so it is the only place a card renders today.
31
+ // The other four still go through callPaid and settle x402, so `_payment`
32
+ // is set and no card attaches; the switch for them is server-side — make the
33
+ // endpoint free (200 without a 402) and cards appear with no change here.
34
+ const FREE_TIER_TOOLS = new Set([
35
+ "list_tools",
36
+ "search_agent_automations",
37
+ "list_automation_categories",
38
+ "get_agent_automation",
39
+ "check_trade_preflight",
40
+ ]);
41
+
42
+ // Ad category forwarded to ads-server per tool (allowlisted context key).
43
+ const TOOL_CATEGORY = {
44
+ list_tools: "agents.automation",
45
+ search_agent_automations: "agents.automation",
46
+ list_automation_categories: "agents.automation",
47
+ get_agent_automation: "agents.automation",
48
+ check_trade_preflight: "crypto.tools",
49
+ };
50
+
51
+ // Hard ceiling on the whole slot call (SDK's own default is 1500ms warm /
52
+ // 3000ms cold). We pass SDK_TIMEOUT_MS explicitly so the SDK never picks the
53
+ // 3000ms cold budget, and race it against HARD_BUDGET_MS as a belt-and-braces
54
+ // guard in case the SDK's AbortSignal misbehaves.
55
+ const SDK_TIMEOUT_MS = 1500;
56
+ const HARD_BUDGET_MS = 2000;
57
+
58
+ let clientPromise = null; // lazily-built LuluAds instance (or null when inert)
59
+
60
+ // Test seam: `setAdsClientForTests(fake)` swaps the SDK client; `null` resets.
61
+ let injectedClient;
62
+ function setAdsClientForTests(client) {
63
+ injectedClient = client;
64
+ clientPromise = null;
65
+ }
66
+
67
+ function adsEnabled(env = process.env) {
68
+ if (String(env.LULU_ADS_ENABLED || "").toLowerCase() === "false") return false;
69
+ return Boolean(env.LULU_ADS_PUBLISHER_ID && env.LULU_ADS_API_KEY);
70
+ }
71
+
72
+ // lulu-ads is ESM-only ("type": "module", `import` export only), so a
73
+ // CommonJS caller has to dynamic-import it. Done once, lazily, and only
74
+ // after the env guard passes — so no creds means the module is never even
75
+ // loaded, let alone warmed up (warmUp() would otherwise POST /telemetry/init
76
+ // with an empty key).
77
+ function getClient() {
78
+ if (injectedClient !== undefined) return Promise.resolve(injectedClient);
79
+ if (!clientPromise) {
80
+ clientPromise = import("lulu-ads")
81
+ .then(({ LuluAds }) => {
82
+ const client = new LuluAds({
83
+ publisherId: process.env.LULU_ADS_PUBLISHER_ID,
84
+ apiKey: process.env.LULU_ADS_API_KEY,
85
+ });
86
+ void client.warmUp(); // fire-and-forget, never throws
87
+ return client;
88
+ })
89
+ .catch(() => null);
90
+ }
91
+ return clientPromise;
92
+ }
93
+
94
+ function isFreeTierResponse(toolName, data) {
95
+ if (!FREE_TIER_TOOLS.has(toolName)) return false;
96
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
97
+ if (data._payment) return false; // x402 settled → paid response, no card
98
+ if ("sponsored" in data) return false; // never clobber an upstream field
99
+ return true;
100
+ }
101
+
102
+ // Returns `data` with a `sponsored` field appended when a card is available,
103
+ // otherwise the exact same object. Never throws, never exceeds HARD_BUDGET_MS.
104
+ async function attachSponsored(toolName, data) {
105
+ try {
106
+ if (!adsEnabled()) return data;
107
+ if (!isFreeTierResponse(toolName, data)) return data;
108
+ const client = await getClient();
109
+ if (!client) return data;
110
+
111
+ const slot = client.sponsoredSlot({
112
+ context: { tool: toolName, category: TOOL_CATEGORY[toolName] },
113
+ timeoutMs: SDK_TIMEOUT_MS,
114
+ });
115
+ let timer;
116
+ const budget = new Promise((resolve) => { timer = setTimeout(() => resolve(null), HARD_BUDGET_MS); });
117
+ const sponsored = await Promise.race([slot, budget]).catch(() => null);
118
+ clearTimeout(timer);
119
+
120
+ if (!sponsored || !sponsored.text || !sponsored.url) return data;
121
+ return { ...data, sponsored: { label: "Sponsored", text: String(sponsored.text), url: String(sponsored.url) } };
122
+ } catch (_) {
123
+ return data;
124
+ }
125
+ }
126
+
127
+ module.exports = { attachSponsored, adsEnabled, isFreeTierResponse, FREE_TIER_TOOLS, TOOL_CATEGORY, setAdsClientForTests };
package/index.js CHANGED
@@ -10,6 +10,7 @@ const { toClientEvmSigner } = require("@x402/evm");
10
10
  const { privateKeyToAccount } = require("viem/accounts");
11
11
  const { createPublicClient, createWalletClient, http, parseAbi } = require("viem");
12
12
  const { base } = require("viem/chains");
13
+ const { attachSponsored, FREE_TIER_TOOLS } = require("./ads.js");
13
14
 
14
15
  const BASE_URL = "https://x402.coinopai.com";
15
16
 
@@ -184,6 +185,63 @@ const TOOLS = [
184
185
  required: ["decision_id"]
185
186
  }
186
187
  },
188
+ {
189
+ name: "get_futures_decision",
190
+ annotations: {
191
+ readOnlyHint: false,
192
+ destructiveHint: false,
193
+ idempotentHint: false,
194
+ openWorldHint: true,
195
+ },
196
+ description: "Kronos Futures: perpetual-futures decision package. Direction LONG / SHORT / FLAT from the Kronos signal, stop-loss and take-profit on the calibrated 80% range, leverage cap keeping liquidation outside 2x the stop, liquidation price, risk-based position size for your equity, and live perp funding cost. Market intelligence only; no exchange execution. Costs $0.15 USDC.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ symbol: { type: "string", description: "Symbol: BTC, ETH, SOL, XRP, ADA (default: BTC)" },
201
+ equity: { type: "number", description: "Account equity in USD for position sizing (optional)" },
202
+ max_loss_pct: { type: "number", description: "Max loss per position as a fraction of equity (default 0.01)" },
203
+ max_leverage: { type: "number", description: "Your leverage ceiling, 1-5 (default 5)" }
204
+ }
205
+ }
206
+ },
207
+ {
208
+ name: "get_perp_funding",
209
+ annotations: {
210
+ readOnlyHint: true,
211
+ destructiveHint: false,
212
+ idempotentHint: false,
213
+ openWorldHint: true,
214
+ },
215
+ description: "Kronos Futures: live perpetual funding rates (1h, 8h, annualized), mark/index price, open interest, and crowding label (LONG_CROWDED / SHORT_CROWDED / BALANCED) for BTC, ETH, SOL, XRP, ADA from Kraken Futures and Hyperliquid. Costs $0.02 USDC.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ symbol: { type: "string", description: "Optional single symbol; omit for all five" }
220
+ }
221
+ }
222
+ },
223
+ {
224
+ name: "check_futures_risk",
225
+ annotations: {
226
+ readOnlyHint: true,
227
+ destructiveHint: false,
228
+ idempotentHint: false,
229
+ openWorldHint: true,
230
+ },
231
+ description: "Kronos Futures: will a leveraged perp position survive the calibrated range? Returns liquidation price and distance, adverse range bound, verdict (SURVIVES_RANGE / THIN_BUFFER / LIQUIDATION_INSIDE_RANGE), max surviving leverage, P&L at both range bounds, and funding cost. Costs $0.05 USDC.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ symbol: { type: "string", description: "Symbol: BTC, ETH, SOL, XRP, ADA (default: BTC)" },
236
+ side: { type: "string", description: "LONG or SHORT" },
237
+ leverage: { type: "number", description: "Leverage to test (1-125)" },
238
+ entry: { type: "number", description: "Entry price (defaults to current perp mark)" },
239
+ notional: { type: "number", description: "Position notional in USD for funding cost (optional)" },
240
+ horizon_hours: { type: "number", description: "Hours held for funding cost (optional)" }
241
+ },
242
+ required: ["side", "leverage"]
243
+ }
244
+ },
187
245
  {
188
246
  name: "get_crypto_forecast",
189
247
  annotations: {
@@ -201,6 +259,17 @@ const TOOLS = [
201
259
  }
202
260
  }
203
261
  },
262
+ {
263
+ name: "list_tools",
264
+ annotations: {
265
+ readOnlyHint: true,
266
+ destructiveHint: false,
267
+ idempotentHint: true,
268
+ openWorldHint: true,
269
+ },
270
+ description: "Free. Lists every CoinOpAI tool with its price, so an agent can pick before paying.",
271
+ inputSchema: { type: "object", properties: {} }
272
+ },
204
273
  {
205
274
  name: "review_signal_anomaly",
206
275
  annotations: {
@@ -292,6 +361,13 @@ async function callPyrimid(account, path, affiliateId, baseUrl, products) {
292
361
  return paidRes.json();
293
362
  }
294
363
 
364
+ function queryString(params) {
365
+ const parts = Object.entries(params)
366
+ .filter(([, v]) => v !== undefined && v !== null && v !== "")
367
+ .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`);
368
+ return parts.length ? `?${parts.join("&")}` : "";
369
+ }
370
+
295
371
  async function callPaid(ctx, path, affiliateId, opts = {}) {
296
372
  const { httpClient, account } = ctx;
297
373
  const baseUrl = opts.baseUrl || BASE_URL;
@@ -358,7 +434,7 @@ async function main() {
358
434
  }
359
435
 
360
436
  const server = new Server(
361
- { name: "coinopai-mcp", version: "1.2.10" },
437
+ { name: "coinopai-mcp", version: "2.1.0" },
362
438
  { capabilities: { tools: {} } }
363
439
  );
364
440
 
@@ -369,9 +445,16 @@ async function main() {
369
445
  try {
370
446
  // Affiliate ID: tool arg takes precedence, then env fallback, then none
371
447
  const affiliateId = args.affiliate_id || process.env.PYRIMID_AFFILIATE_ID || null;
372
- const paymentContext = getPaymentContext();
448
+ // list_tools is free: plain fetch, no wallet, never touches callPaid
449
+ const paymentContext = name === "list_tools" ? null : getPaymentContext();
373
450
  let data;
374
451
  switch (name) {
452
+ case "list_tools": {
453
+ const r = await fetch(`${BASE_URL}/menu`);
454
+ if (!r.ok) throw new Error(`/menu returned ${r.status}`);
455
+ data = await r.json();
456
+ break;
457
+ }
375
458
  // Low-value utility endpoints — no affiliate routing
376
459
  case "search_agent_automations":
377
460
  data = await callPaid(paymentContext, `/api/search?q=${encodeURIComponent(args.query || "")}&limit=${args.limit || 20}`, null);
@@ -404,6 +487,15 @@ async function main() {
404
487
  case "get_crypto_forecast":
405
488
  data = await callPaid(paymentContext, `/api/kronos/forecast?symbol=${encodeURIComponent(args.symbol || "BTC")}`, affiliateId);
406
489
  break;
490
+ case "get_futures_decision":
491
+ data = await callPaid(paymentContext, `/api/kronos/futures/decision${queryString({ symbol: args.symbol || "BTC", equity: args.equity, max_loss_pct: args.max_loss_pct, max_leverage: args.max_leverage })}`, null);
492
+ break;
493
+ case "get_perp_funding":
494
+ data = await callPaid(paymentContext, `/api/kronos/futures/funding${queryString({ symbol: args.symbol })}`, null);
495
+ break;
496
+ case "check_futures_risk":
497
+ data = await callPaid(paymentContext, `/api/kronos/futures/risk${queryString({ symbol: args.symbol || "BTC", side: args.side, leverage: args.leverage, entry: args.entry, notional: args.notional, horizon_hours: args.horizon_hours })}`, null);
498
+ break;
407
499
  case "review_signal_anomaly":
408
500
  data = await callPaid(paymentContext, "/api/anomaly", null, {
409
501
  method: "POST",
@@ -417,6 +509,9 @@ async function main() {
417
509
  default:
418
510
  throw new Error("Unknown tool: " + name);
419
511
  }
512
+ // Sponsored card (Lulu Ads) — free-tier tools only, skipped when the
513
+ // response settled an x402 payment. Paid tools never reach this call.
514
+ if (FREE_TIER_TOOLS.has(name)) data = await attachSponsored(name, data);
420
515
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
421
516
  } catch (e) {
422
517
  return { content: [{ type: "text", text: "Error: " + e.message }], isError: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coinopai-mcp",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "mcpName": "io.github.forgemeshlabs/coinopai-mcp",
5
5
  "description": "Auditable market intelligence MCP with x402 payments, calibrated forecast ranges, anomaly review, risk context, and outcome audits",
6
6
  "main": "index.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "index.js",
12
+ "ads.js",
12
13
  "README.md",
13
14
  "server.json"
14
15
  ],
@@ -50,6 +51,7 @@
50
51
  "@modelcontextprotocol/sdk": "^1.30.0",
51
52
  "@x402/core": "2.11.0",
52
53
  "@x402/evm": "2.11.0",
54
+ "lulu-ads": "^0.9.19",
53
55
  "viem": "^2.0.0"
54
56
  }
55
57
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/forgemeshlabs/coinopai-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.0.2",
9
+ "version": "2.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "coinopai-mcp",
14
- "version": "2.0.2",
14
+ "version": "2.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },