coinopai-mcp 2.0.2 → 2.1.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 +22 -0
- package/ads.js +127 -0
- package/index.js +24 -2
- package/package.json +3 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](https://x402.org)
|
|
10
10
|
[](https://base.org)
|
|
11
|
+
[](https://mcpservers.org/servers/forgemeshlabs/coinopai-mcp)
|
|
11
12
|
|
|
12
13
|
**Auditable market context with calibrated forecast ranges and outcome verification.**
|
|
13
14
|
|
|
@@ -306,6 +307,27 @@ Route to an available symbol or wait 15 minutes for the next cycle.
|
|
|
306
307
|
|
|
307
308
|
---
|
|
308
309
|
|
|
310
|
+
## Sponsored cards (Lulu Ads)
|
|
311
|
+
|
|
312
|
+
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:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"results": [...],
|
|
317
|
+
"sponsored": { "label": "Sponsored", "text": "...", "url": "https://..." }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- **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.
|
|
322
|
+
- **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.
|
|
323
|
+
- **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` never touch the ads SDK.
|
|
324
|
+
- **Strip it:** `delete result.sponsored`.
|
|
325
|
+
- **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.
|
|
326
|
+
- **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.
|
|
327
|
+
- **Fail-open:** any SDK error or timeout (hard budget 2s) returns the original response unchanged. The only external host contacted is `ads.getlulu.dev`.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
309
331
|
## Affiliate Attribution (via Pyrimid)
|
|
310
332
|
|
|
311
333
|
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
|
|
|
@@ -201,6 +202,17 @@ const TOOLS = [
|
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
},
|
|
205
|
+
{
|
|
206
|
+
name: "list_tools",
|
|
207
|
+
annotations: {
|
|
208
|
+
readOnlyHint: true,
|
|
209
|
+
destructiveHint: false,
|
|
210
|
+
idempotentHint: true,
|
|
211
|
+
openWorldHint: true,
|
|
212
|
+
},
|
|
213
|
+
description: "Free. Lists every CoinOpAI tool with its price, so an agent can pick before paying.",
|
|
214
|
+
inputSchema: { type: "object", properties: {} }
|
|
215
|
+
},
|
|
204
216
|
{
|
|
205
217
|
name: "review_signal_anomaly",
|
|
206
218
|
annotations: {
|
|
@@ -358,7 +370,7 @@ async function main() {
|
|
|
358
370
|
}
|
|
359
371
|
|
|
360
372
|
const server = new Server(
|
|
361
|
-
{ name: "coinopai-mcp", version: "1.
|
|
373
|
+
{ name: "coinopai-mcp", version: "2.1.0" },
|
|
362
374
|
{ capabilities: { tools: {} } }
|
|
363
375
|
);
|
|
364
376
|
|
|
@@ -369,9 +381,16 @@ async function main() {
|
|
|
369
381
|
try {
|
|
370
382
|
// Affiliate ID: tool arg takes precedence, then env fallback, then none
|
|
371
383
|
const affiliateId = args.affiliate_id || process.env.PYRIMID_AFFILIATE_ID || null;
|
|
372
|
-
|
|
384
|
+
// list_tools is free: plain fetch, no wallet, never touches callPaid
|
|
385
|
+
const paymentContext = name === "list_tools" ? null : getPaymentContext();
|
|
373
386
|
let data;
|
|
374
387
|
switch (name) {
|
|
388
|
+
case "list_tools": {
|
|
389
|
+
const r = await fetch(`${BASE_URL}/menu`);
|
|
390
|
+
if (!r.ok) throw new Error(`/menu returned ${r.status}`);
|
|
391
|
+
data = await r.json();
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
375
394
|
// Low-value utility endpoints — no affiliate routing
|
|
376
395
|
case "search_agent_automations":
|
|
377
396
|
data = await callPaid(paymentContext, `/api/search?q=${encodeURIComponent(args.query || "")}&limit=${args.limit || 20}`, null);
|
|
@@ -417,6 +436,9 @@ async function main() {
|
|
|
417
436
|
default:
|
|
418
437
|
throw new Error("Unknown tool: " + name);
|
|
419
438
|
}
|
|
439
|
+
// Sponsored card (Lulu Ads) — free-tier tools only, skipped when the
|
|
440
|
+
// response settled an x402 payment. Paid tools never reach this call.
|
|
441
|
+
if (FREE_TIER_TOOLS.has(name)) data = await attachSponsored(name, data);
|
|
420
442
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
421
443
|
} catch (e) {
|
|
422
444
|
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
|
|
3
|
+
"version": "2.1.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
|
|
9
|
+
"version": "2.1.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "coinopai-mcp",
|
|
14
|
-
"version": "2.0
|
|
14
|
+
"version": "2.1.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|