@noelclaw/mcp 2.3.0 → 2.4.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/dist/index.js +10 -9
- package/dist/server.js +38 -28
- package/dist/tools/automation.js +38 -0
- package/dist/tools/coder.js +62 -0
- package/dist/tools/defi.js +127 -0
- package/dist/tools/humanizer.js +143 -2
- package/dist/tools/insight.js +197 -19
- package/dist/tools/market.js +182 -7
- package/dist/tools/memory.js +198 -170
- package/dist/tools/miroshark.js +15 -4
- package/dist/tools/os.js +223 -0
- package/dist/tools/scanner.js +183 -52
- package/dist/tools/swarm.js +327 -14
- package/dist/tools/vault.js +94 -153
- package/package.json +5 -2
- package/dist/tools/news.js +0 -6
- package/dist/tools/research.js +0 -8
- package/dist/tools/twitter.js +0 -67
package/dist/tools/os.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OS_TOOLS = void 0;
|
|
4
|
+
exports.handleOsTool = handleOsTool;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const convex_js_1 = require("../convex.js");
|
|
7
|
+
const market_js_1 = require("./market.js");
|
|
8
|
+
const memory_js_1 = require("./memory.js");
|
|
9
|
+
exports.OS_TOOLS = [
|
|
10
|
+
{
|
|
11
|
+
name: "noel_status",
|
|
12
|
+
description: "Full system dashboard for the Noelclaw AI OS — memory usage, swarm health, active automations, " +
|
|
13
|
+
"recent vault research, and execution scores. Like `htop` but for your AI operating system. " +
|
|
14
|
+
"Run this to get a complete picture of what's running and what your OS currently knows.",
|
|
15
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "noel_boot",
|
|
19
|
+
description: "Boot sequence for the Noelclaw AI OS — starts the swarm, loads live market prices, checks active automations, " +
|
|
20
|
+
"and returns a unified briefing. One command to wake up the entire operating system. " +
|
|
21
|
+
"Run this first to prime the system before any trading or research session.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
focus: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional: token or topic to focus today's session on (e.g. 'ETH', 'Base ecosystem')",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "noel_shutdown",
|
|
35
|
+
description: "Clean shutdown of the Noelclaw AI OS — stops the swarm, saves a session summary to vault, and returns a final briefing. " +
|
|
36
|
+
"Run at the end of a trading or research session to persist all findings before signing off.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
note: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Optional: a note to save with the session summary (e.g. 'Closed ETH position, watching BTC')",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: [],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const BootSchema = zod_1.z.object({ focus: zod_1.z.string().optional() });
|
|
50
|
+
const ShutdownSchema = zod_1.z.object({ note: zod_1.z.string().max(500).optional() });
|
|
51
|
+
async function handleOsTool(name, args) {
|
|
52
|
+
switch (name) {
|
|
53
|
+
case "noel_status": {
|
|
54
|
+
const [memRes, swarmRes, autoRes, vaultRes, scoresRes] = await Promise.allSettled([
|
|
55
|
+
(0, convex_js_1.callConvex)("/memory/profile", "GET"),
|
|
56
|
+
(0, convex_js_1.callConvex)("/swarm/status", "GET", undefined, "get_swarm_status"),
|
|
57
|
+
(0, convex_js_1.callConvex)("/automations/list", "GET", undefined, "list_automations"),
|
|
58
|
+
(0, convex_js_1.callConvex)("/vault/list?type=research&limit=5", "GET", undefined, "noel_status"),
|
|
59
|
+
(0, convex_js_1.callConvex)("/swarm/scores", "GET", undefined, "get_execution_scores"),
|
|
60
|
+
]);
|
|
61
|
+
const mem = memRes.status === "fulfilled" ? memRes.value : null;
|
|
62
|
+
const swarm = swarmRes.status === "fulfilled" ? swarmRes.value : null;
|
|
63
|
+
const autos = autoRes.status === "fulfilled" ? autoRes.value : null;
|
|
64
|
+
const vault = vaultRes.status === "fulfilled" ? vaultRes.value : null;
|
|
65
|
+
const scores = scoresRes.status === "fulfilled" ? scoresRes.value : null;
|
|
66
|
+
const automations = autos?.automations ?? [];
|
|
67
|
+
const activeAutos = automations.filter((a) => a.status === "active");
|
|
68
|
+
const vaultEntries = vault?.entries ?? [];
|
|
69
|
+
const allScores = (scores?.scores ?? []).sort((a, b) => b.lastScore - a.lastScore);
|
|
70
|
+
const topScore = allScores[0];
|
|
71
|
+
const swarmActive = swarm?.active ?? false;
|
|
72
|
+
const memTotal = mem?.total ?? 0;
|
|
73
|
+
const memStatus = mem?.status ?? "unknown";
|
|
74
|
+
const lines = [
|
|
75
|
+
`**Noelclaw AI OS — System Status**`,
|
|
76
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
77
|
+
``,
|
|
78
|
+
`🧠 **Memory** ${memStatus === "ok" ? "✅" : "⚠️"} ${memTotal} memories · Space: ${mem?.space ?? "—"}`,
|
|
79
|
+
`🤖 **Swarm** ${swarmActive ? "✅ Active" : "⏹️ Offline"} · ${swarm?.memory?.length ?? 0} shared memory entries`,
|
|
80
|
+
`⚡ **Automations** ${activeAutos.length} active of ${automations.length} total`,
|
|
81
|
+
allScores.length
|
|
82
|
+
? `📊 **Top Skill** ${topScore.skillName} — ${((topScore.lastScore ?? 0) * 100).toFixed(0)}% · ${topScore.successCount}W/${topScore.failCount}L`
|
|
83
|
+
: `📊 **Skills** No execution history yet`,
|
|
84
|
+
``,
|
|
85
|
+
];
|
|
86
|
+
if (activeAutos.length > 0) {
|
|
87
|
+
lines.push(`**Active Automations:**`);
|
|
88
|
+
for (const a of activeAutos.slice(0, 5)) {
|
|
89
|
+
const next = a.nextRunAt ? ` · next ${new Date(a.nextRunAt).toUTCString()}` : "";
|
|
90
|
+
lines.push(` • ${a.name} — ${a.triggerType}${next}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
94
|
+
if (vaultEntries.length > 0) {
|
|
95
|
+
lines.push(`**Recent Research:**`);
|
|
96
|
+
for (const e of vaultEntries) {
|
|
97
|
+
lines.push(` • [${e.agentId ?? "vault"}] ${e.title}`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
lines.push(swarmActive
|
|
102
|
+
? `💡 Run \`swarm_pulse\` for a live snapshot from all agents.`
|
|
103
|
+
: `💡 Run \`noel_boot\` to wake up the full system.`);
|
|
104
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
105
|
+
}
|
|
106
|
+
case "noel_boot": {
|
|
107
|
+
const parsed = BootSchema.safeParse(args ?? {});
|
|
108
|
+
if (!parsed.success)
|
|
109
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
110
|
+
const { focus } = parsed.data;
|
|
111
|
+
const [swarmRes, marketRes, autoRes, memRes, focusRes] = await Promise.allSettled([
|
|
112
|
+
(0, convex_js_1.callConvex)("/swarm/start", "POST", {}, "start_swarm"),
|
|
113
|
+
(0, market_js_1.fetchMarketSnapshot)(),
|
|
114
|
+
(0, convex_js_1.callConvex)("/automations/list", "GET", undefined, "list_automations"),
|
|
115
|
+
(0, convex_js_1.callConvex)("/memory/profile", "GET"),
|
|
116
|
+
focus ? (0, memory_js_1.searchSupermemory)(focus, 4) : Promise.resolve([]),
|
|
117
|
+
]);
|
|
118
|
+
const swarm = swarmRes.status === "fulfilled" ? swarmRes.value : null;
|
|
119
|
+
const market = marketRes.status === "fulfilled" ? marketRes.value : null;
|
|
120
|
+
const autos = autoRes.status === "fulfilled" ? autoRes.value : null;
|
|
121
|
+
const mem = memRes.status === "fulfilled" ? memRes.value : null;
|
|
122
|
+
const focusMem = focusRes.status === "fulfilled" ? focusRes.value : [];
|
|
123
|
+
const automations = autos?.automations ?? [];
|
|
124
|
+
const activeAutos = automations.filter((a) => a.status === "active");
|
|
125
|
+
const memTotal = mem?.total ?? 0;
|
|
126
|
+
const p = (n) => `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
127
|
+
const lines = [
|
|
128
|
+
`🚀 **Noelclaw AI OS — Boot Complete**`,
|
|
129
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
130
|
+
`${new Date().toUTCString()}`,
|
|
131
|
+
``,
|
|
132
|
+
`**Subsystems:**`,
|
|
133
|
+
` 🤖 Swarm ${swarm?.success ? `✅ Online · session ${swarm.sessionId ?? "active"}` : "⚠️ Could not start"}`,
|
|
134
|
+
` 🧠 Memory ✅ ${memTotal} memories ready`,
|
|
135
|
+
` ⚡ Automations ${activeAutos.length} active of ${automations.length}`,
|
|
136
|
+
``,
|
|
137
|
+
];
|
|
138
|
+
if (market) {
|
|
139
|
+
lines.push(`**Market Snapshot:**`);
|
|
140
|
+
lines.push(` BTC ${p(market.btc)} · ETH ${p(market.eth)} · SOL ${p(market.sol)}`);
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
if (activeAutos.length > 0) {
|
|
144
|
+
lines.push(`**Active Automations:**`);
|
|
145
|
+
for (const a of activeAutos.slice(0, 5)) {
|
|
146
|
+
const next = a.nextRunAt ? ` · runs ${new Date(a.nextRunAt).toUTCString()}` : "";
|
|
147
|
+
lines.push(` • ${a.name}${next}`);
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
if (focus && focusMem.length > 0) {
|
|
152
|
+
lines.push(`**Memory context for "${focus}" (${focusMem.length} items):**`);
|
|
153
|
+
for (const r of focusMem) {
|
|
154
|
+
const title = r.metadata?.title ?? r.content.slice(0, 70).replace(/\n/g, " ");
|
|
155
|
+
lines.push(` • ${title}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push("");
|
|
158
|
+
}
|
|
159
|
+
lines.push(`**Quick actions:**`);
|
|
160
|
+
lines.push(` • \`swarm_pulse\` — live readings from all agents`);
|
|
161
|
+
if (focus) {
|
|
162
|
+
lines.push(` • \`swarm_research topic: "${focus}"\` — deep research session`);
|
|
163
|
+
lines.push(` • \`memory_insight topic: "${focus}"\` — full intelligence report`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
lines.push(` • \`swarm_research topic: "BTC"\` — start morning research`);
|
|
167
|
+
lines.push(` • \`noel_status\` — full system dashboard`);
|
|
168
|
+
}
|
|
169
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
170
|
+
}
|
|
171
|
+
case "noel_shutdown": {
|
|
172
|
+
const parsed = ShutdownSchema.safeParse(args ?? {});
|
|
173
|
+
if (!parsed.success)
|
|
174
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
175
|
+
const { note } = parsed.data;
|
|
176
|
+
const [swarmRes, vaultRes] = await Promise.allSettled([
|
|
177
|
+
(0, convex_js_1.callConvex)("/swarm/stop", "POST", {}, "stop_swarm"),
|
|
178
|
+
(0, convex_js_1.callConvex)("/vault/list?type=research&limit=5", "GET", undefined, "noel_shutdown"),
|
|
179
|
+
]);
|
|
180
|
+
const swarm = swarmRes.status === "fulfilled" ? swarmRes.value : null;
|
|
181
|
+
const vault = vaultRes.status === "fulfilled" ? vaultRes.value : null;
|
|
182
|
+
const vaultEntries = vault?.entries ?? [];
|
|
183
|
+
const now = new Date();
|
|
184
|
+
const sessionKey = `session/shutdown-${now.toISOString().slice(0, 10)}-${now.getHours()}h`;
|
|
185
|
+
const summaryContent = [
|
|
186
|
+
`# Session Shutdown — ${now.toUTCString()}`,
|
|
187
|
+
note ? `\nNote: ${note}` : "",
|
|
188
|
+
`\n## Recent Research`,
|
|
189
|
+
...vaultEntries.map((e) => `• [${e.agentId ?? "vault"}] ${e.title}`),
|
|
190
|
+
].filter(Boolean).join("\n");
|
|
191
|
+
await (0, convex_js_1.callConvex)("/vault/save", "POST", {
|
|
192
|
+
type: "memory",
|
|
193
|
+
title: `Session: ${now.toLocaleDateString("en-US")}${note ? ` — ${note.slice(0, 50)}` : ""}`,
|
|
194
|
+
content: summaryContent,
|
|
195
|
+
key: sessionKey,
|
|
196
|
+
agentId: "os",
|
|
197
|
+
tags: ["session", "shutdown"],
|
|
198
|
+
commitMsg: "noel_shutdown session save",
|
|
199
|
+
}, "noel_shutdown").catch(() => { });
|
|
200
|
+
const lines = [
|
|
201
|
+
`⏹️ **Noelclaw AI OS — Shutdown**`,
|
|
202
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
203
|
+
`${now.toUTCString()}`,
|
|
204
|
+
``,
|
|
205
|
+
`🤖 Swarm ${swarm?.success ? "✅ Stopped" : "⚠️ Already offline"}`,
|
|
206
|
+
`💾 Session ✅ Saved to vault: \`${sessionKey}\``,
|
|
207
|
+
note ? `📝 Note ${note}` : "",
|
|
208
|
+
``,
|
|
209
|
+
];
|
|
210
|
+
if (vaultEntries.length > 0) {
|
|
211
|
+
lines.push(`**Session research (${vaultEntries.length} entries):**`);
|
|
212
|
+
for (const e of vaultEntries) {
|
|
213
|
+
lines.push(` • [${e.agentId ?? "vault"}] ${e.title}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push("");
|
|
216
|
+
}
|
|
217
|
+
lines.push(`Run \`noel_boot\` to start a new session.`);
|
|
218
|
+
return { content: [{ type: "text", text: lines.filter(l => l !== undefined && l !== "").join("\n") }] };
|
|
219
|
+
}
|
|
220
|
+
default:
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/tools/scanner.js
CHANGED
|
@@ -85,13 +85,116 @@ function scoreDipReversal(c, minLiquidity = DEFAULT_MIN_LIQ) {
|
|
|
85
85
|
buyPressure5m: bp,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
// ── Fetch
|
|
88
|
+
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
|
89
89
|
async function fetchJson(url) {
|
|
90
90
|
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
91
91
|
if (!res.ok)
|
|
92
92
|
throw new Error(`HTTP ${res.status} from ${new URL(url).hostname}`);
|
|
93
93
|
return res.json();
|
|
94
94
|
}
|
|
95
|
+
async function fetchBasePools(minLiquidity, limit) {
|
|
96
|
+
const [trendingRes, newPoolsRes] = await Promise.allSettled([
|
|
97
|
+
fetchJson("https://api.geckoterminal.com/api/v2/networks/base/trending_pools?page=1"),
|
|
98
|
+
fetchJson("https://api.geckoterminal.com/api/v2/networks/base/new_pools?page=1"),
|
|
99
|
+
]);
|
|
100
|
+
const rawPools = [
|
|
101
|
+
...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
|
|
102
|
+
...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
|
|
103
|
+
];
|
|
104
|
+
if (!rawPools.length)
|
|
105
|
+
throw new Error("GeckoTerminal returned no pools. Try again in a moment.");
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const deduped = rawPools.filter(p => {
|
|
108
|
+
if (!p?.attributes || !p.id)
|
|
109
|
+
return false;
|
|
110
|
+
if (seen.has(p.id))
|
|
111
|
+
return false;
|
|
112
|
+
seen.add(p.id);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
return deduped
|
|
116
|
+
.map((p) => {
|
|
117
|
+
const a = p.attributes ?? {};
|
|
118
|
+
const txns = a.transactions ?? {};
|
|
119
|
+
const pc = a.price_change_percentage ?? {};
|
|
120
|
+
const vol = a.volume_usd ?? {};
|
|
121
|
+
const liq = parseFloat(a.reserve_in_usd ?? "0");
|
|
122
|
+
const tokenRel = p.relationships?.base_token?.data?.id ?? "";
|
|
123
|
+
const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
|
|
124
|
+
if (!mint?.startsWith("0x"))
|
|
125
|
+
return null;
|
|
126
|
+
return {
|
|
127
|
+
mint,
|
|
128
|
+
symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
|
|
129
|
+
priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
|
|
130
|
+
priceChange5m: parseFloat(pc.m5 ?? "0"),
|
|
131
|
+
priceChange1h: parseFloat(pc.h1 ?? "0"),
|
|
132
|
+
priceChange6h: parseFloat(pc.h6 ?? "0"),
|
|
133
|
+
priceChange24h: parseFloat(pc.h24 ?? "0"),
|
|
134
|
+
volume1h: parseFloat(vol.h1 ?? "0"),
|
|
135
|
+
liquidity: liq,
|
|
136
|
+
buys5m: txns.m5?.buys ?? 0,
|
|
137
|
+
sells5m: txns.m5?.sells ?? 0,
|
|
138
|
+
buys1h: txns.h1?.buys ?? 0,
|
|
139
|
+
sells1h: txns.h1?.sells ?? 0,
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
.filter((c) => c !== null && c.liquidity >= minLiquidity)
|
|
143
|
+
.slice(0, limit);
|
|
144
|
+
}
|
|
145
|
+
function scoreMomentum(c, minLiquidity = DEFAULT_MIN_LIQ) {
|
|
146
|
+
const pc5m = c.priceChange5m;
|
|
147
|
+
const pc1h = c.priceChange1h;
|
|
148
|
+
const pc6h = c.priceChange6h;
|
|
149
|
+
const pc24h = c.priceChange24h;
|
|
150
|
+
const liq = c.liquidity;
|
|
151
|
+
const vol1h = c.volume1h;
|
|
152
|
+
const totalTxns5m = c.buys5m + c.sells5m;
|
|
153
|
+
const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
|
|
154
|
+
const totalTxns1h = c.buys1h + c.sells1h;
|
|
155
|
+
const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
|
|
156
|
+
const bp = buyRatio5m * 100;
|
|
157
|
+
// Hard gates — all must pass
|
|
158
|
+
const gateFailures = [];
|
|
159
|
+
if (pc1h < 3)
|
|
160
|
+
gateFailures.push(`1h momentum weak (${pc1h.toFixed(1)}% < 3%)`);
|
|
161
|
+
if (pc5m < 0.5)
|
|
162
|
+
gateFailures.push(`5m not accelerating (${pc5m.toFixed(1)}% < 0.5%)`);
|
|
163
|
+
if (totalTxns5m > 5 && buyRatio5m < 0.55)
|
|
164
|
+
gateFailures.push(`buy pressure low (${bp.toFixed(0)}% < 55%)`);
|
|
165
|
+
if (liq < minLiquidity)
|
|
166
|
+
gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
|
|
167
|
+
if (pc24h > 150)
|
|
168
|
+
gateFailures.push(`already parabolic (24h ${pc24h.toFixed(0)}%)`);
|
|
169
|
+
if (gateFailures.length > 0) {
|
|
170
|
+
return { score: 0, passed: false, pattern: null, gateFailures, buyPressure5m: bp };
|
|
171
|
+
}
|
|
172
|
+
// 1. Momentum strength (0–25 pts)
|
|
173
|
+
const momentumPts = pc1h >= 20 ? 25 : pc1h >= 10 ? 20 : pc1h >= 6 ? 15 : pc1h >= 3 ? 8 : 3;
|
|
174
|
+
// 2. 5m acceleration (0–20 pts)
|
|
175
|
+
const accelPts = pc5m >= 5 ? 20 : pc5m >= 3 ? 16 : pc5m >= 2 ? 12 : pc5m >= 1 ? 7 : 3;
|
|
176
|
+
// 3. Buy pressure (0–15 pts)
|
|
177
|
+
const bpPts = bp >= 70 ? 15 : bp >= 65 ? 12 : bp >= 60 ? 9 : bp >= 55 ? 5 : 2;
|
|
178
|
+
// 4. Volume & activity (0–15 pts)
|
|
179
|
+
const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
|
|
180
|
+
: vol1h >= 50000 && totalTxns1h >= 100 ? 12
|
|
181
|
+
: vol1h >= 20000 && totalTxns1h >= 40 ? 8
|
|
182
|
+
: vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
|
|
183
|
+
// 5. Trend continuation (0–15 pts)
|
|
184
|
+
let trendPts = 0;
|
|
185
|
+
if (pc6h > 5 && pc24h > 5)
|
|
186
|
+
trendPts = 15;
|
|
187
|
+
else if (pc6h > 0 && pc24h > 0)
|
|
188
|
+
trendPts = 10;
|
|
189
|
+
else if (pc6h > 0)
|
|
190
|
+
trendPts = 5;
|
|
191
|
+
// 6. Sentiment acceleration (0–10 pts)
|
|
192
|
+
const sentAccel = buyRatio5m - buyRatio1h;
|
|
193
|
+
const sentPts = sentAccel >= 0.10 ? 10 : sentAccel >= 0.05 ? 7 : sentAccel >= 0 ? 3 : 0;
|
|
194
|
+
const score = Math.max(0, Math.min(100, momentumPts + accelPts + bpPts + actPts + trendPts + sentPts));
|
|
195
|
+
const pattern = pc1h >= 15 ? "BREAKOUT" : pc1h >= 8 ? "MOMENTUM" : pc1h >= 3 ? "PUSH" : "WEAK-PUSH";
|
|
196
|
+
return { score, passed: true, pattern, gateFailures: [], buyPressure5m: bp };
|
|
197
|
+
}
|
|
95
198
|
// ── Schemas ───────────────────────────────────────────────────────────────────
|
|
96
199
|
const AddressSchema = zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address");
|
|
97
200
|
const ScoreTokenSchema = zod_1.z.object({ address: AddressSchema, minLiquidity: zod_1.z.number().positive().optional() });
|
|
@@ -101,6 +204,11 @@ const ScanDipsSchema = zod_1.z.object({
|
|
|
101
204
|
minLiquidity: zod_1.z.number().positive().optional(),
|
|
102
205
|
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
103
206
|
}).default({});
|
|
207
|
+
const ScanMomentumSchema = zod_1.z.object({
|
|
208
|
+
minScore: zod_1.z.number().min(0).max(100).optional(),
|
|
209
|
+
minLiquidity: zod_1.z.number().positive().optional(),
|
|
210
|
+
limit: zod_1.z.number().int().min(1).max(100).optional(),
|
|
211
|
+
}).default({});
|
|
104
212
|
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
105
213
|
exports.SCANNER_TOOLS = [
|
|
106
214
|
{
|
|
@@ -139,6 +247,22 @@ exports.SCANNER_TOOLS = [
|
|
|
139
247
|
required: [],
|
|
140
248
|
},
|
|
141
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: "scan_momentum",
|
|
252
|
+
description: "Scan Base pools for momentum breakout setups — tokens with strong 1h+ upward momentum that are still accelerating. " +
|
|
253
|
+
"The inverse of scan_dips: finds BREAKOUT / MOMENTUM / PUSH patterns instead of dip-reversals. " +
|
|
254
|
+
"Gates: 1h > +3%, 5m still rising, buy pressure > 55%, not already parabolic (24h < 150%). " +
|
|
255
|
+
"Returns scored and ranked candidates. No API keys required.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
minScore: { type: "number", description: "Min score to include (default 50)" },
|
|
260
|
+
minLiquidity: { type: "number", description: "Min pool liquidity in USD (default 50000)" },
|
|
261
|
+
limit: { type: "number", description: "Max pools to scan (default 40, max 100)" },
|
|
262
|
+
},
|
|
263
|
+
required: [],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
142
266
|
];
|
|
143
267
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
144
268
|
async function handleScannerTool(name, args) {
|
|
@@ -279,58 +403,13 @@ async function handleScannerTool(name, args) {
|
|
|
279
403
|
if (!parsed.success)
|
|
280
404
|
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
281
405
|
const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
|
|
289
|
-
...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
|
|
290
|
-
];
|
|
291
|
-
if (!rawPools.length) {
|
|
292
|
-
return { content: [{ type: "text", text: "GeckoTerminal returned no pools. Try again in a moment." }], isError: true };
|
|
406
|
+
let candidates;
|
|
407
|
+
try {
|
|
408
|
+
candidates = await fetchBasePools(minLiquidity, limit);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
293
412
|
}
|
|
294
|
-
// Deduplicate by pool id
|
|
295
|
-
const seen = new Set();
|
|
296
|
-
const pools = rawPools.filter(p => {
|
|
297
|
-
if (!p?.attributes || !p.id)
|
|
298
|
-
return false;
|
|
299
|
-
if (seen.has(p.id))
|
|
300
|
-
return false;
|
|
301
|
-
seen.add(p.id);
|
|
302
|
-
return true;
|
|
303
|
-
});
|
|
304
|
-
// Map GeckoTerminal pool → Candidate
|
|
305
|
-
const candidates = pools
|
|
306
|
-
.map((p) => {
|
|
307
|
-
const a = p.attributes ?? {};
|
|
308
|
-
const txns = a.transactions ?? {};
|
|
309
|
-
const pc = a.price_change_percentage ?? {};
|
|
310
|
-
const vol = a.volume_usd ?? {};
|
|
311
|
-
const liq = parseFloat(a.reserve_in_usd ?? "0");
|
|
312
|
-
const tokenRel = p.relationships?.base_token?.data?.id ?? "";
|
|
313
|
-
const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
|
|
314
|
-
if (!mint?.startsWith("0x"))
|
|
315
|
-
return null;
|
|
316
|
-
return {
|
|
317
|
-
mint,
|
|
318
|
-
symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
|
|
319
|
-
priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
|
|
320
|
-
priceChange5m: parseFloat(pc.m5 ?? "0"),
|
|
321
|
-
priceChange1h: parseFloat(pc.h1 ?? "0"),
|
|
322
|
-
priceChange6h: parseFloat(pc.h6 ?? "0"),
|
|
323
|
-
priceChange24h: parseFloat(pc.h24 ?? "0"),
|
|
324
|
-
volume1h: parseFloat(vol.h1 ?? "0"),
|
|
325
|
-
liquidity: liq,
|
|
326
|
-
buys5m: txns.m5?.buys ?? 0,
|
|
327
|
-
sells5m: txns.m5?.sells ?? 0,
|
|
328
|
-
buys1h: txns.h1?.buys ?? 0,
|
|
329
|
-
sells1h: txns.h1?.sells ?? 0,
|
|
330
|
-
};
|
|
331
|
-
})
|
|
332
|
-
.filter((c) => c !== null && c.liquidity >= minLiquidity)
|
|
333
|
-
.slice(0, limit);
|
|
334
413
|
const scored = candidates
|
|
335
414
|
.map(c => ({ ...c, ...scoreDipReversal(c, minLiquidity) }))
|
|
336
415
|
.filter(c => c.passed && c.score >= minScore)
|
|
@@ -371,5 +450,57 @@ async function handleScannerTool(name, args) {
|
|
|
371
450
|
lines.push(`Next steps: \`score_token\` for full breakdown · \`check_token\` for rug check before buying`);
|
|
372
451
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
373
452
|
}
|
|
453
|
+
// ── scan_momentum ──────────────────────────────────────────────────────────
|
|
454
|
+
if (name === "scan_momentum") {
|
|
455
|
+
const parsed = ScanMomentumSchema.safeParse(args ?? {});
|
|
456
|
+
if (!parsed.success)
|
|
457
|
+
return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
|
|
458
|
+
const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
|
|
459
|
+
let candidates;
|
|
460
|
+
try {
|
|
461
|
+
candidates = await fetchBasePools(minLiquidity, limit);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
465
|
+
}
|
|
466
|
+
const scored = candidates
|
|
467
|
+
.map(c => ({ ...c, ...scoreMomentum(c, minLiquidity) }))
|
|
468
|
+
.filter(c => c.passed && c.score >= minScore)
|
|
469
|
+
.sort((a, b) => b.score - a.score);
|
|
470
|
+
if (!scored.length) {
|
|
471
|
+
return {
|
|
472
|
+
content: [{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: [
|
|
475
|
+
`## Momentum Scan — No Breakouts Found`,
|
|
476
|
+
``,
|
|
477
|
+
`Scanned **${candidates.length} pools** on Base. None passed the momentum gates with score ≥ ${minScore}.`,
|
|
478
|
+
``,
|
|
479
|
+
`When nothing breaks out, the market may be in consolidation or distribution.`,
|
|
480
|
+
``,
|
|
481
|
+
`Try: \`scan_dips\` to look for reversal opportunities instead.`,
|
|
482
|
+
].join("\n"),
|
|
483
|
+
}],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const sign = (n) => n >= 0 ? `+${n.toFixed(1)}` : n.toFixed(1);
|
|
487
|
+
const lines = [
|
|
488
|
+
`## Momentum Scan — ${scored.length} Breakout${scored.length !== 1 ? "s" : ""} Found`,
|
|
489
|
+
`Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
|
|
490
|
+
``,
|
|
491
|
+
];
|
|
492
|
+
for (const c of scored.slice(0, 10)) {
|
|
493
|
+
const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
|
|
494
|
+
lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
|
|
495
|
+
lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
|
|
496
|
+
lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${(c.volume1h / 1000).toFixed(0)}k`);
|
|
497
|
+
lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
|
|
498
|
+
lines.push(`\`${c.mint}\``);
|
|
499
|
+
lines.push(``);
|
|
500
|
+
}
|
|
501
|
+
lines.push(`---`);
|
|
502
|
+
lines.push(`Next steps: \`score_token\` for dip-reversal score · \`check_token\` for rug check before buying`);
|
|
503
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
504
|
+
}
|
|
374
505
|
return null;
|
|
375
506
|
}
|