@finchagentic/mcp 4.0.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -0
  3. package/dist/_http-cache.js +96 -0
  4. package/dist/agent-loop.js +231 -0
  5. package/dist/annotations.js +113 -0
  6. package/dist/cli.js +1195 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +151 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +256 -0
  13. package/dist/llm.js +323 -0
  14. package/dist/local-memory.js +102 -0
  15. package/dist/local-vault.js +454 -0
  16. package/dist/output-schemas.js +551 -0
  17. package/dist/prompts.js +111 -0
  18. package/dist/public-url.js +107 -0
  19. package/dist/resources.js +116 -0
  20. package/dist/server.js +300 -0
  21. package/dist/signal-gate.js +57 -0
  22. package/dist/token-decimals.js +26 -0
  23. package/dist/token-gate.js +88 -0
  24. package/dist/tool-filter.js +44 -0
  25. package/dist/tools/_solidity-scan.js +313 -0
  26. package/dist/tools/agents.js +729 -0
  27. package/dist/tools/automation.js +314 -0
  28. package/dist/tools/base-mcp.js +478 -0
  29. package/dist/tools/base.js +269 -0
  30. package/dist/tools/chronicle.js +268 -0
  31. package/dist/tools/coder.js +94 -0
  32. package/dist/tools/deep-research.js +1416 -0
  33. package/dist/tools/defi.js +291 -0
  34. package/dist/tools/equity.js +364 -0
  35. package/dist/tools/events.js +182 -0
  36. package/dist/tools/framework.js +150 -0
  37. package/dist/tools/github.js +514 -0
  38. package/dist/tools/insider.js +264 -0
  39. package/dist/tools/insight.js +634 -0
  40. package/dist/tools/market.js +555 -0
  41. package/dist/tools/memory.js +1046 -0
  42. package/dist/tools/miroshark.js +343 -0
  43. package/dist/tools/monitor.js +319 -0
  44. package/dist/tools/os.js +226 -0
  45. package/dist/tools/packets.js +296 -0
  46. package/dist/tools/research-chain.js +226 -0
  47. package/dist/tools/research-compare.js +280 -0
  48. package/dist/tools/research.js +188 -0
  49. package/dist/tools/rh-bridge.js +148 -0
  50. package/dist/tools/rh-mcp.js +1411 -0
  51. package/dist/tools/rh-orders.js +471 -0
  52. package/dist/tools/scanner.js +534 -0
  53. package/dist/tools/vault.js +764 -0
  54. package/dist/tools/wallet.js +200 -0
  55. package/dist/types.js +2 -0
  56. package/dist/wallet.js +184 -0
  57. package/package.json +87 -0
@@ -0,0 +1,534 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCANNER_TOOLS = void 0;
4
+ exports.buildScoreStructured = buildScoreStructured;
5
+ exports.buildScanResults = buildScanResults;
6
+ exports.assessTokenSecurity = assessTokenSecurity;
7
+ exports.handleScannerTool = handleScannerTool;
8
+ const zod_1 = require("zod");
9
+ const _http_cache_js_1 = require("../_http-cache.js");
10
+ // ── Constants ─────────────────────────────────────────────────────────────────
11
+ const DEFAULT_MIN_LIQ = 50000;
12
+ const DEFAULT_MIN_SCORE = 50;
13
+ // ── Dip-reversal scorer ───────────────────────────────────────────────────────
14
+ // Ported 1:1 from circuit-agent/lib/scoring.js (battle-tested, pure math).
15
+ function scoreDipReversal(c, minLiquidity = DEFAULT_MIN_LIQ) {
16
+ const pc5m = c.priceChange5m;
17
+ const pc1h = c.priceChange1h;
18
+ const pc6h = c.priceChange6h;
19
+ const pc24h = c.priceChange24h;
20
+ const liq = c.liquidity;
21
+ const vol1h = c.volume1h;
22
+ const totalTxns5m = c.buys5m + c.sells5m;
23
+ const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
24
+ const totalTxns1h = c.buys1h + c.sells1h;
25
+ const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
26
+ // ── Hard gates - all must pass ────────────────────────────────────────────
27
+ const gateFailures = [];
28
+ if (pc1h >= 0)
29
+ gateFailures.push(`1h not negative (${pc1h.toFixed(1)}%)`);
30
+ if (pc5m < 0.5)
31
+ gateFailures.push(`5m bounce weak (${pc5m.toFixed(1)}% < 0.5%)`);
32
+ if (totalTxns5m > 5 && buyRatio5m <= 0.50)
33
+ gateFailures.push(`buy ratio low (${(buyRatio5m * 100).toFixed(0)}%)`);
34
+ if (liq < minLiquidity)
35
+ gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
36
+ if (pc6h <= -20 && pc24h <= -20)
37
+ gateFailures.push(`dead cat (6h ${pc6h.toFixed(0)}% / 24h ${pc24h.toFixed(0)}%)`);
38
+ if (gateFailures.length > 0) {
39
+ return { score: 0, passed: false, pattern: null, breakdown: {}, gateFailures, buyPressure5m: buyRatio5m * 100 };
40
+ }
41
+ // ── 1. Drop depth (0–25 pts) - deeper dip = more bounce room ─────────────
42
+ const dropPts = pc1h <= -10 ? 25 : pc1h <= -5 ? 20 : pc1h <= -3 ? 15 : 5;
43
+ // ── 2. Bounce confirmation (0–20 pts) ─────────────────────────────────────
44
+ const bouncePts = pc5m >= 5 ? 20 : pc5m >= 3 ? 17 : pc5m >= 2 ? 14 : pc5m >= 1 ? 10 : 5;
45
+ // ── 3. Sentiment shift (0–15 pts) - buyers returning after selloff ────────
46
+ const sentimentShift = buyRatio5m - buyRatio1h;
47
+ const sentPts = sentimentShift >= 0.10 ? 15
48
+ : sentimentShift >= 0.05 ? 10
49
+ : sentimentShift >= 0.02 ? 7
50
+ : sentimentShift > 0 ? 3 : 0;
51
+ // ── 4. Buy pressure (0–10 pts) ────────────────────────────────────────────
52
+ const bp = buyRatio5m * 100;
53
+ const bpPts = bp >= 65 ? 10 : bp >= 58 ? 8 : bp >= 53 ? 5 : 2;
54
+ // ── 5. Volume & activity (0–15 pts) - validates bounce is real ───────────
55
+ const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
56
+ : vol1h >= 50000 && totalTxns1h >= 100 ? 12
57
+ : vol1h >= 20000 && totalTxns1h >= 40 ? 8
58
+ : vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
59
+ // ── 6. Trend alignment (−10 to +15 pts) - dip in uptrend vs dead cat ─────
60
+ let trendPts;
61
+ if (pc6h > 0 && pc24h > 0)
62
+ trendPts = 15;
63
+ else if (pc24h > 0)
64
+ trendPts = 10;
65
+ else if (pc6h > 0)
66
+ trendPts = 5;
67
+ else {
68
+ const avg = (pc6h + pc24h) / 2;
69
+ trendPts = avg <= -15 ? -10 : avg <= -8 ? -7 : avg <= -4 ? -5 : -2;
70
+ }
71
+ const score = Math.max(0, Math.min(100, dropPts + bouncePts + sentPts + bpPts + actPts + trendPts));
72
+ const pattern = pc1h < -10 ? "DEEP-REVERSAL"
73
+ : pc1h < -5 ? "REVERSAL"
74
+ : pc1h < -3 ? "DIP-BUY"
75
+ : "SHALLOW-DIP";
76
+ return {
77
+ score,
78
+ passed: true,
79
+ pattern,
80
+ breakdown: {
81
+ dropDepth: { value: +pc1h.toFixed(1), points: dropPts },
82
+ bounce: { value: +pc5m.toFixed(1), points: bouncePts },
83
+ sentimentShift: { value: +sentimentShift.toFixed(2), points: sentPts },
84
+ buyPressure: { value: +bp.toFixed(0), points: bpPts },
85
+ activity: { vol1h, txns1h: totalTxns1h, points: actPts },
86
+ trendAlignment: { pc6h: +pc6h.toFixed(1), pc24h: +pc24h.toFixed(1), points: trendPts },
87
+ },
88
+ gateFailures: [],
89
+ buyPressure5m: bp,
90
+ };
91
+ }
92
+ // ── Structured output builders (MCP outputSchema payloads) ──────────────────
93
+ // Pure functions so both the human-readable text and the machine-readable
94
+ // `structuredContent` derive from one source, and so they can be unit-tested
95
+ // without a network round-trip.
96
+ function buildScoreStructured(address, c, result) {
97
+ return {
98
+ address,
99
+ symbol: c.symbol,
100
+ priceUsd: c.priceUsd,
101
+ liquidityUsd: c.liquidity,
102
+ score: result.score,
103
+ pattern: result.pattern,
104
+ passed: result.passed,
105
+ gateFailures: result.gateFailures,
106
+ breakdown: result.breakdown,
107
+ };
108
+ }
109
+ function buildScanResults(mode, scanned, scored) {
110
+ // Map the full result set (the text view truncates to 10 for readability;
111
+ // structured output carries everything so count === results.length, matching
112
+ // every other list builder).
113
+ return {
114
+ mode,
115
+ scanned,
116
+ count: scored.length,
117
+ results: scored.map((c) => ({
118
+ symbol: c.symbol,
119
+ address: c.mint,
120
+ score: c.score,
121
+ pattern: c.pattern ?? null,
122
+ priceUsd: c.priceUsd ?? null,
123
+ liquidityUsd: c.liquidity ?? null,
124
+ priceChange5m: c.priceChange5m ?? null,
125
+ priceChange1h: c.priceChange1h ?? null,
126
+ priceChange6h: c.priceChange6h ?? null,
127
+ priceChange24h: c.priceChange24h ?? null,
128
+ buyPressure5m: c.buyPressure5m ?? null,
129
+ volume1h: c.volume1h ?? null,
130
+ })),
131
+ };
132
+ }
133
+ // GoPlusLabs token_security payload → normalized verdict. Extracted verbatim
134
+ // from the check_token handler so the text report and structuredContent can't
135
+ // drift apart.
136
+ function assessTokenSecurity(info) {
137
+ const isHoneypot = info.is_honeypot === "1";
138
+ const isMintable = info.is_mintable === "1";
139
+ const isFreezeAuth = info.transfer_pausable === "1";
140
+ const isOpenSource = info.is_open_source === "1";
141
+ const lpLockedPct = (info.lp_holders ?? [])
142
+ .filter(h => h.is_locked)
143
+ .reduce((s, h) => s + parseFloat(h.percent ?? "0"), 0) * 100;
144
+ const buyTax = parseFloat(info.buy_tax ?? "0");
145
+ const sellTax = parseFloat(info.sell_tax ?? "0");
146
+ let rugScore = 0;
147
+ if (isHoneypot)
148
+ rugScore += 50; // cannot sell → auto-danger
149
+ if (isMintable)
150
+ rugScore += 30; // unlimited supply risk
151
+ if (isFreezeAuth)
152
+ rugScore += 30; // transfers can be blocked
153
+ if (lpLockedPct < 50)
154
+ rugScore += 20; // LP can be pulled
155
+ if (sellTax > 10)
156
+ rugScore += 15; // high sell tax
157
+ rugScore = Math.min(100, rugScore);
158
+ const verdict = (isHoneypot || rugScore >= 60) ? "DANGER"
159
+ : rugScore >= 30 ? "CAUTION"
160
+ : "SAFE";
161
+ const holderCount = info.holder_count != null ? Number(info.holder_count) : null;
162
+ return { verdict, rugScore, isHoneypot, isMintable, isFreezeAuth, isOpenSource, lpLockedPct, buyTax, sellTax, holderCount };
163
+ }
164
+ // ── Fetch helpers ─────────────────────────────────────────────────────────────
165
+ // All external scanner calls flow through cachedFetch - adds 45s LRU caching
166
+ // + 429 backoff. GeckoTerminal and DexScreener both throttle aggressively;
167
+ // before this, parallel scan_market / score_token calls were tripping limits.
168
+ async function fetchJson(url) {
169
+ const res = await (0, _http_cache_js_1.cachedFetch)(url, { headers: { Accept: "application/json" } });
170
+ if (!res.ok)
171
+ throw new Error(`HTTP ${res.status} from ${new URL(url).hostname}`);
172
+ return JSON.parse(res.text);
173
+ }
174
+ async function fetchBasePools(minLiquidity, limit) {
175
+ const [trendingRes, newPoolsRes] = await Promise.allSettled([
176
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/trending_pools?page=1"),
177
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/new_pools?page=1"),
178
+ ]);
179
+ const rawPools = [
180
+ ...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
181
+ ...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
182
+ ];
183
+ if (!rawPools.length)
184
+ throw new Error("GeckoTerminal returned no pools. Try again in a moment.");
185
+ const seen = new Set();
186
+ const deduped = rawPools.filter(p => {
187
+ if (!p?.attributes || !p.id)
188
+ return false;
189
+ if (seen.has(p.id))
190
+ return false;
191
+ seen.add(p.id);
192
+ return true;
193
+ });
194
+ return deduped
195
+ .map((p) => {
196
+ const a = p.attributes ?? {};
197
+ const txns = a.transactions ?? {};
198
+ const pc = a.price_change_percentage ?? {};
199
+ const vol = a.volume_usd ?? {};
200
+ const liq = parseFloat(a.reserve_in_usd ?? "0");
201
+ const tokenRel = p.relationships?.base_token?.data?.id ?? "";
202
+ const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
203
+ if (!mint?.startsWith("0x"))
204
+ return null;
205
+ return {
206
+ mint,
207
+ symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
208
+ priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
209
+ priceChange5m: parseFloat(pc.m5 ?? "0"),
210
+ priceChange1h: parseFloat(pc.h1 ?? "0"),
211
+ priceChange6h: parseFloat(pc.h6 ?? "0"),
212
+ priceChange24h: parseFloat(pc.h24 ?? "0"),
213
+ volume1h: parseFloat(vol.h1 ?? "0"),
214
+ liquidity: liq,
215
+ buys5m: txns.m5?.buys ?? 0,
216
+ sells5m: txns.m5?.sells ?? 0,
217
+ buys1h: txns.h1?.buys ?? 0,
218
+ sells1h: txns.h1?.sells ?? 0,
219
+ };
220
+ })
221
+ .filter((c) => c !== null && c.liquidity >= minLiquidity)
222
+ .slice(0, limit);
223
+ }
224
+ function scoreMomentum(c, minLiquidity = DEFAULT_MIN_LIQ) {
225
+ const pc5m = c.priceChange5m;
226
+ const pc1h = c.priceChange1h;
227
+ const pc6h = c.priceChange6h;
228
+ const pc24h = c.priceChange24h;
229
+ const liq = c.liquidity;
230
+ const vol1h = c.volume1h;
231
+ const totalTxns5m = c.buys5m + c.sells5m;
232
+ const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
233
+ const totalTxns1h = c.buys1h + c.sells1h;
234
+ const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
235
+ const bp = buyRatio5m * 100;
236
+ // Hard gates - all must pass
237
+ const gateFailures = [];
238
+ if (pc1h < 3)
239
+ gateFailures.push(`1h momentum weak (${pc1h.toFixed(1)}% < 3%)`);
240
+ if (pc5m < 0.5)
241
+ gateFailures.push(`5m not accelerating (${pc5m.toFixed(1)}% < 0.5%)`);
242
+ if (totalTxns5m > 5 && buyRatio5m < 0.55)
243
+ gateFailures.push(`buy pressure low (${bp.toFixed(0)}% < 55%)`);
244
+ if (liq < minLiquidity)
245
+ gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
246
+ if (pc24h > 150)
247
+ gateFailures.push(`already parabolic (24h ${pc24h.toFixed(0)}%)`);
248
+ if (gateFailures.length > 0) {
249
+ return { score: 0, passed: false, pattern: null, gateFailures, buyPressure5m: bp };
250
+ }
251
+ // 1. Momentum strength (0–25 pts)
252
+ const momentumPts = pc1h >= 20 ? 25 : pc1h >= 10 ? 20 : pc1h >= 6 ? 15 : pc1h >= 3 ? 8 : 3;
253
+ // 2. 5m acceleration (0–20 pts)
254
+ const accelPts = pc5m >= 5 ? 20 : pc5m >= 3 ? 16 : pc5m >= 2 ? 12 : pc5m >= 1 ? 7 : 3;
255
+ // 3. Buy pressure (0–15 pts)
256
+ const bpPts = bp >= 70 ? 15 : bp >= 65 ? 12 : bp >= 60 ? 9 : bp >= 55 ? 5 : 2;
257
+ // 4. Volume & activity (0–15 pts)
258
+ const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
259
+ : vol1h >= 50000 && totalTxns1h >= 100 ? 12
260
+ : vol1h >= 20000 && totalTxns1h >= 40 ? 8
261
+ : vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
262
+ // 5. Trend continuation (0–15 pts)
263
+ let trendPts = 0;
264
+ if (pc6h > 5 && pc24h > 5)
265
+ trendPts = 15;
266
+ else if (pc6h > 0 && pc24h > 0)
267
+ trendPts = 10;
268
+ else if (pc6h > 0)
269
+ trendPts = 5;
270
+ // 6. Sentiment acceleration (0–10 pts)
271
+ const sentAccel = buyRatio5m - buyRatio1h;
272
+ const sentPts = sentAccel >= 0.10 ? 10 : sentAccel >= 0.05 ? 7 : sentAccel >= 0 ? 3 : 0;
273
+ const score = Math.max(0, Math.min(100, momentumPts + accelPts + bpPts + actPts + trendPts + sentPts));
274
+ const pattern = pc1h >= 15 ? "BREAKOUT" : pc1h >= 8 ? "MOMENTUM" : pc1h >= 3 ? "PUSH" : "WEAK-PUSH";
275
+ return { score, passed: true, pattern, gateFailures: [], buyPressure5m: bp };
276
+ }
277
+ // ── Schemas ───────────────────────────────────────────────────────────────────
278
+ const AddressSchema = zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address");
279
+ const ScoreTokenSchema = zod_1.z.object({ address: AddressSchema, minLiquidity: zod_1.z.number().positive().optional() });
280
+ const CheckTokenSchema = zod_1.z.object({ address: AddressSchema });
281
+ // scan_market validates both modes (dips/momentum) with the same shape.
282
+ const ScanDipsSchema = zod_1.z.object({
283
+ minScore: zod_1.z.number().min(0).max(100).optional(),
284
+ minLiquidity: zod_1.z.number().positive().optional(),
285
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
286
+ }).default({});
287
+ // ── Tool definitions ──────────────────────────────────────────────────────────
288
+ exports.SCANNER_TOOLS = [
289
+ {
290
+ name: "score_token",
291
+ description: "Run the 6-component dip-reversal score on any Base token. Returns a 0–100 score, pattern label (DEEP-REVERSAL / REVERSAL / DIP-BUY / SHALLOW-DIP), and full component breakdown. Data from DexScreener - no API key required.",
292
+ inputSchema: {
293
+ type: "object",
294
+ properties: {
295
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
296
+ minLiquidity: { type: "number", description: "Minimum liquidity in USD (default 50000)" },
297
+ },
298
+ required: ["address"],
299
+ },
300
+ },
301
+ {
302
+ name: "check_token",
303
+ description: "Security audit a Base token: honeypot, rug risk score, mint authority, freeze authority, LP lock %, buy/sell tax, holder count. Powered by GoPlusLabs (free). Always run this before buying an unknown token.",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
308
+ },
309
+ required: ["address"],
310
+ },
311
+ },
312
+ {
313
+ name: "scan_market",
314
+ description: "Scan all trending + new Base pools for trading opportunities. Two modes: " +
315
+ "'dips' finds dip-reversal setups (6-component scorer: momentum, buy pressure, depth, volume surge, trend context, volatility). " +
316
+ "'momentum' finds breakout setups - tokens with strong 1h+ upward momentum still accelerating (gates: 1h > +3%, 5m rising, buy pressure > 55%). " +
317
+ "No API keys required.",
318
+ inputSchema: {
319
+ type: "object",
320
+ properties: {
321
+ mode: { type: "string", enum: ["dips", "momentum"], description: "Scan mode: 'dips' for reversal setups (default), 'momentum' for breakouts" },
322
+ minScore: { type: "number", description: "Min score to include in results (default 50)" },
323
+ minLiquidity: { type: "number", description: "Min pool liquidity in USD (default 50000)" },
324
+ limit: { type: "number", description: "Max pools to scan (default 40, max 100)" },
325
+ },
326
+ required: [],
327
+ },
328
+ },
329
+ ];
330
+ // ── Handlers ──────────────────────────────────────────────────────────────────
331
+ async function handleScannerTool(name, args) {
332
+ // ── score_token ────────────────────────────────────────────────────────────
333
+ if (name === "score_token") {
334
+ const parsed = ScoreTokenSchema.safeParse(args);
335
+ if (!parsed.success)
336
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
337
+ const { address, minLiquidity = DEFAULT_MIN_LIQ } = parsed.data;
338
+ const data = await fetchJson(`https://api.dexscreener.com/latest/dex/tokens/${address}`);
339
+ const pair = (data.pairs ?? [])
340
+ .filter((p) => p.chainId === "base" && p.baseToken?.address?.toLowerCase() === address.toLowerCase())
341
+ .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
342
+ if (!pair) {
343
+ return { content: [{ type: "text", text: `No Base trading pair found where \`${address}\` is the base token (it may only appear as a quote currency, e.g. a stablecoin). Check the address or try a different token.` }], isError: true };
344
+ }
345
+ const c = {
346
+ mint: address,
347
+ symbol: pair.baseToken?.symbol ?? address.slice(0, 8),
348
+ priceUsd: parseFloat(pair.priceUsd ?? "0"),
349
+ priceChange5m: pair.priceChange?.m5 ?? 0,
350
+ priceChange1h: pair.priceChange?.h1 ?? 0,
351
+ priceChange6h: pair.priceChange?.h6 ?? 0,
352
+ priceChange24h: pair.priceChange?.h24 ?? 0,
353
+ volume1h: pair.volume?.h1 ?? 0,
354
+ liquidity: pair.liquidity?.usd ?? 0,
355
+ buys5m: pair.txns?.m5?.buys ?? 0,
356
+ sells5m: pair.txns?.m5?.sells ?? 0,
357
+ buys1h: pair.txns?.h1?.buys ?? 0,
358
+ sells1h: pair.txns?.h1?.sells ?? 0,
359
+ };
360
+ const result = scoreDipReversal(c, minLiquidity);
361
+ const bd = result.breakdown;
362
+ const bar = "█".repeat(Math.round(result.score / 5)).padEnd(20, "░");
363
+ const lines = [
364
+ `## Dip-Reversal Score: ${c.symbol}`,
365
+ `\`${address}\``,
366
+ ``,
367
+ `**Score: ${result.score}/100** \`${bar}\``,
368
+ `**Pattern:** ${result.pattern ?? "-"}`,
369
+ `**Price:** $${c.priceUsd.toFixed(8).replace(/0+$/, "").replace(/\.$/, "")}`,
370
+ `**Liquidity:** $${(c.liquidity / 1000).toFixed(0)}k`,
371
+ ``,
372
+ ];
373
+ if (!result.passed) {
374
+ lines.push(`**Gates failed - not a valid dip-reversal setup:**`);
375
+ result.gateFailures.forEach(f => lines.push(`• ${f}`));
376
+ }
377
+ else {
378
+ lines.push(`**Breakdown:**`);
379
+ lines.push(`| Component | Signal | Points |`);
380
+ lines.push(`|-----------|--------|--------|`);
381
+ lines.push(`| Drop depth | ${bd.dropDepth?.value}% (1h) | ${bd.dropDepth?.points}/25 |`);
382
+ lines.push(`| Bounce | ${bd.bounce?.value}% (5m) | ${bd.bounce?.points}/20 |`);
383
+ lines.push(`| Sentiment shift | ${bd.sentimentShift?.value} ratio shift | ${bd.sentimentShift?.points}/15 |`);
384
+ lines.push(`| Buy pressure | ${bd.buyPressure?.value}% buyers (5m) | ${bd.buyPressure?.points}/10 |`);
385
+ lines.push(`| Activity | $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k vol / ${bd.activity?.txns1h} txns | ${bd.activity?.points}/15 |`);
386
+ lines.push(`| Trend | 6h ${bd.trendAlignment?.pc6h}% / 24h ${bd.trendAlignment?.pc24h}% | ${(bd.trendAlignment?.points ?? 0) > 0 ? "+" : ""}${bd.trendAlignment?.points}/15 |`);
387
+ lines.push(``);
388
+ if (result.score >= 65)
389
+ lines.push(`**Strong setup.** Score ≥65 - high probability dip-reversal.`);
390
+ else if (result.score >= 50)
391
+ lines.push(`**Marginal.** Score 50–64 - look for additional confirmation before entry.`);
392
+ else
393
+ lines.push(`**Weak.** Score <50 - skip this setup.`);
394
+ lines.push(``, `Run \`check_token address="${address}"\` for a rug/security check before buying.`);
395
+ }
396
+ return {
397
+ content: [{ type: "text", text: lines.join("\n") }],
398
+ structuredContent: buildScoreStructured(address, c, result),
399
+ };
400
+ }
401
+ // ── check_token ────────────────────────────────────────────────────────────
402
+ if (name === "check_token") {
403
+ const parsed = CheckTokenSchema.safeParse(args);
404
+ if (!parsed.success)
405
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
406
+ const { address } = parsed.data;
407
+ // GoPlusLabs - chain 8453 = Base mainnet
408
+ const data = await fetchJson(`https://api.gopluslabs.io/api/v1/token_security/8453?contract_addresses=${address}`);
409
+ const info = data.result?.[address.toLowerCase()] ?? data.result?.[address] ?? {};
410
+ const sec = assessTokenSecurity(info);
411
+ const { verdict, rugScore, isHoneypot, isMintable, isFreezeAuth, isOpenSource, lpLockedPct, buyTax, sellTax } = sec;
412
+ const icon = { DANGER: "🔴", CAUTION: "🟡", SAFE: "🟢" }[verdict];
413
+ const lines = [
414
+ `## Token Security Check`,
415
+ `\`${address}\``,
416
+ ``,
417
+ `**Verdict: ${icon} ${verdict}** (rug score: ${rugScore}/100)`,
418
+ ``,
419
+ `| Check | Status |`,
420
+ `|-------|--------|`,
421
+ `| Honeypot | ${isHoneypot ? "🔴 YES - cannot sell" : "🟢 No"} |`,
422
+ `| Mint authority | ${isMintable ? "🔴 Yes - supply can inflate" : "🟢 No"} |`,
423
+ `| Transfer freeze | ${isFreezeAuth ? "🔴 Yes - transfers can be paused" : "🟢 No"} |`,
424
+ `| Open source | ${isOpenSource ? "🟢 Yes" : "🔴 No - unverified contract"} |`,
425
+ `| LP locked | ${lpLockedPct >= 80 ? "🟢" : lpLockedPct >= 50 ? "🟡" : "🔴"} ${lpLockedPct.toFixed(1)}% |`,
426
+ `| Buy tax | ${buyTax > 5 ? "🟡" : "🟢"} ${buyTax}% |`,
427
+ `| Sell tax | ${sellTax > 10 ? "🔴" : sellTax > 5 ? "🟡" : "🟢"} ${sellTax}% |`,
428
+ `| Holder count | ${info.holder_count ?? "unknown"} |`,
429
+ ``,
430
+ ];
431
+ if (verdict === "DANGER") {
432
+ lines.push(`**Do not buy.** This token has critical red flags. High risk of total loss.`);
433
+ }
434
+ else if (verdict === "CAUTION") {
435
+ lines.push(`**Trade carefully.** Elevated risk - verify team, community, and LP lock before buying. Keep position size small.`);
436
+ }
437
+ else {
438
+ lines.push(`**Passes basic security checks.** No critical red flags found. Always DYOR - security checks are not a guarantee.`);
439
+ }
440
+ return {
441
+ content: [{ type: "text", text: lines.join("\n") }],
442
+ structuredContent: { address, ...sec },
443
+ };
444
+ }
445
+ // ── scan_market ────────────────────────────────────────────────────────────
446
+ if (name === "scan_market") {
447
+ const parsed = ScanDipsSchema.safeParse(args ?? {});
448
+ if (!parsed.success)
449
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
450
+ const input = args;
451
+ const mode = input?.mode === "momentum" ? "momentum" : "dips";
452
+ const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
453
+ let candidates;
454
+ try {
455
+ candidates = await fetchBasePools(minLiquidity, limit);
456
+ }
457
+ catch (err) {
458
+ return { content: [{ type: "text", text: err.message }], isError: true };
459
+ }
460
+ const sign = (n) => n >= 0 ? `+${n.toFixed(1)}` : n.toFixed(1);
461
+ if (mode === "momentum") {
462
+ const scored = candidates
463
+ .map(c => ({ ...c, ...scoreMomentum(c, minLiquidity) }))
464
+ .filter(c => c.passed && c.score >= minScore)
465
+ .sort((a, b) => b.score - a.score);
466
+ if (!scored.length) {
467
+ return {
468
+ content: [{
469
+ type: "text",
470
+ text: [
471
+ `## Momentum Scan - No Breakouts Found`,
472
+ `Scanned **${candidates.length} pools** on Base. None passed the momentum gates with score ≥ ${minScore}.`,
473
+ `When nothing breaks out, the market may be in consolidation. Try \`scan_market mode=dips\` instead.`,
474
+ ].join("\n"),
475
+ }],
476
+ structuredContent: buildScanResults("momentum", candidates.length, []),
477
+ };
478
+ }
479
+ const lines = [
480
+ `## Momentum Scan - ${scored.length} Breakout${scored.length !== 1 ? "s" : ""} Found`,
481
+ `Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
482
+ ``,
483
+ ];
484
+ for (const c of scored.slice(0, 10)) {
485
+ const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
486
+ lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
487
+ lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
488
+ lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${(c.volume1h / 1000).toFixed(0)}k`);
489
+ lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
490
+ lines.push(`\`${c.mint}\``);
491
+ lines.push(``);
492
+ }
493
+ lines.push(`---`);
494
+ lines.push(`Next steps: \`score_token\` · \`check_token\` for rug check`);
495
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildScanResults("momentum", candidates.length, scored) };
496
+ }
497
+ const scored = candidates
498
+ .map(c => ({ ...c, ...scoreDipReversal(c, minLiquidity) }))
499
+ .filter(c => c.passed && c.score >= minScore)
500
+ .sort((a, b) => b.score - a.score);
501
+ if (!scored.length) {
502
+ return {
503
+ content: [{
504
+ type: "text",
505
+ text: [
506
+ `## Dip Scan - No Setups Found`,
507
+ `Scanned **${candidates.length} pools** on Base. None passed the dip-reversal gates with score ≥ ${minScore}.`,
508
+ `This is a signal in itself - try \`scan_market mode=momentum\` or check back in 5–10 minutes.`,
509
+ ].join("\n"),
510
+ }],
511
+ structuredContent: buildScanResults("dips", candidates.length, []),
512
+ };
513
+ }
514
+ const lines = [
515
+ `## Dip Scan - ${scored.length} Setup${scored.length !== 1 ? "s" : ""} Found`,
516
+ `Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
517
+ ``,
518
+ ];
519
+ for (const c of scored.slice(0, 10)) {
520
+ const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
521
+ const bd = c.breakdown;
522
+ lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
523
+ lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
524
+ lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k · **Txns:** ${bd.activity?.txns1h ?? 0}`);
525
+ lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
526
+ lines.push(`\`${c.mint}\``);
527
+ lines.push(``);
528
+ }
529
+ lines.push(`---`);
530
+ lines.push(`Next steps: \`score_token\` for full breakdown · \`check_token\` for rug check`);
531
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildScanResults("dips", candidates.length, scored) };
532
+ }
533
+ return null;
534
+ }