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