@finchagentic/mcp 4.7.1 → 4.7.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 (51) hide show
  1. package/README.md +74 -39
  2. package/dist/_zod-helpers.js +10 -0
  3. package/dist/cli-doctor.js +300 -0
  4. package/dist/cli-env.js +91 -0
  5. package/dist/cli-install.js +236 -0
  6. package/dist/cli-login.js +101 -0
  7. package/dist/cli-orders.js +175 -0
  8. package/dist/cli-setup.js +270 -0
  9. package/dist/cli-ui.js +168 -0
  10. package/dist/cli-vault.js +75 -0
  11. package/dist/cli.js +61 -1152
  12. package/dist/config.js +3 -2
  13. package/dist/enrichment-router.js +5 -18
  14. package/dist/finch-output.js +30 -29
  15. package/dist/finch-status.js +105 -40
  16. package/dist/index.js +0 -0
  17. package/dist/local-vault.js +5 -12
  18. package/dist/output-schemas.js +3 -12
  19. package/dist/project.js +4 -13
  20. package/dist/server.js +13 -35
  21. package/dist/tool-filter.js +4 -13
  22. package/dist/tools/_solidity-scan.js +5 -14
  23. package/dist/tools/agents.js +14 -26
  24. package/dist/tools/deep-research-constants.js +33 -0
  25. package/dist/tools/deep-research-firecrawl.js +88 -0
  26. package/dist/tools/deep-research-planning.js +249 -0
  27. package/dist/tools/deep-research-synthesis.js +343 -0
  28. package/dist/tools/deep-research-text.js +158 -0
  29. package/dist/tools/deep-research-tools.js +90 -0
  30. package/dist/tools/deep-research.js +47 -938
  31. package/dist/tools/defi.js +4 -3
  32. package/dist/tools/insider.js +4 -14
  33. package/dist/tools/insight.js +7 -6
  34. package/dist/tools/market.js +16 -15
  35. package/dist/tools/memory.js +57 -62
  36. package/dist/tools/monitor.js +7 -6
  37. package/dist/tools/os.js +7 -85
  38. package/dist/tools/research.js +7 -6
  39. package/dist/tools/rh-mcp-constants.js +65 -0
  40. package/dist/tools/rh-mcp-dex.js +107 -0
  41. package/dist/tools/rh-mcp-provider.js +127 -0
  42. package/dist/tools/rh-mcp-resolve.js +137 -0
  43. package/dist/tools/rh-mcp-risk.js +230 -0
  44. package/dist/tools/rh-mcp-safety.js +234 -0
  45. package/dist/tools/rh-mcp-swap.js +181 -0
  46. package/dist/tools/rh-mcp-tools.js +137 -0
  47. package/dist/tools/rh-mcp.js +75 -1191
  48. package/dist/tools/scanner.js +10 -9
  49. package/dist/tools/stake.js +7 -20
  50. package/dist/tools/vault.js +52 -59
  51. package/package.json +12 -9
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ // Robinhood Chain (chainId 4663) constants shared across the rh-mcp-* modules.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.RH_DEX_CHAIN = exports.DEXSCREENER = exports.RH_BLOCKSCOUT_V2 = exports.RH_STOCKS = exports.USDG_DECIMALS = exports.USDG_ADDRESS = exports.RH_UNIVERSAL_ROUTER = exports.RH_PERMIT2 = exports.NATIVE_ETH = exports.RH_EXPLORER = exports.RH_RPC = exports.RH_CHAIN_ID = void 0;
5
+ exports.rhRpcDisplay = rhRpcDisplay;
6
+ exports.RH_CHAIN_ID = 4663;
7
+ exports.RH_RPC = process.env.ROBINHOOD_RPC_URL ??
8
+ process.env.RH_RPC_URL ??
9
+ process.env.FINCH_RH_RPC_URL ??
10
+ "https://rpc.mainnet.chain.robinhood.com";
11
+ /** Host only, for display in tool output - never the full RH_RPC. An
12
+ * operator can point ROBINHOOD_RPC_URL/RH_RPC_URL at a keyed provider
13
+ * (same pattern app/convex just fixed for its own Alchemy-key leak risk -
14
+ * a `.../v2/<key>`-style URL echoed verbatim into a tool response lands the
15
+ * key in chat transcripts/client logs). Falls back to the raw value only
16
+ * if it isn't a parseable URL, which should never happen in practice. */
17
+ function rhRpcDisplay() {
18
+ try {
19
+ return new URL(exports.RH_RPC).host;
20
+ }
21
+ catch {
22
+ return "(configured)";
23
+ }
24
+ }
25
+ exports.RH_EXPLORER = "https://robinhoodchain.blockscout.com";
26
+ exports.NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
27
+ exports.RH_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
28
+ exports.RH_UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904";
29
+ /**
30
+ * Canonical Global Dollar (USDG) — RH chain's settlement stablecoin (same
31
+ * address as app/convex/_settlement.ts). Hardcoded rather than resolved via
32
+ * DexScreener ticker search: "USDG" is the exact symbol an imposter contract
33
+ * would spoof, and dexSearchByTicker ranks by pool liquidity, not authenticity
34
+ * — a fake pool with inflated liquidity could otherwise outrank the real token.
35
+ */
36
+ exports.USDG_ADDRESS = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
37
+ exports.USDG_DECIMALS = 6;
38
+ /** ClawHood / Finch catalog (22 tokenized stocks — official "Robinhood Token" contracts). */
39
+ exports.RH_STOCKS = [
40
+ { address: "0xaf3d76f1834a1d425780943c99ea8a608f8a93f9", symbol: "AAPL", name: "Apple" },
41
+ { address: "0x86923f96303d656e4aa86d9d42d1e57ad2023fdc", symbol: "AMD", name: "AMD" },
42
+ { address: "0x12f190a9f9d7d37a250758b26824b97ce941bf54", symbol: "AMZN", name: "Amazon" },
43
+ { address: "0x822cc93ffd030293e9842c30bbd678f530701867", symbol: "BE", name: "Bloom Energy" },
44
+ { address: "0x6330d8c3178a418788df01a47479c0ce7ccf450b", symbol: "COIN", name: "Coinbase" },
45
+ { address: "0x5f10a1c971b69e47e059e1dc91901b59b3fb49c3", symbol: "CRWV", name: "CoreWeave" },
46
+ { address: "0x1b0e319c6a659f002271b69db8a7df2f911c153e", symbol: "GME", name: "GameStop" },
47
+ { address: "0x2e0847e8910a9732eb3fb1bb4b70a580adad4fe3", symbol: "GOOGL", name: "Alphabet" },
48
+ { address: "0xc72b96e0e48ecd4dc75e1e45396e26300bc39681", symbol: "INTC", name: "Intel" },
49
+ { address: "0xc0d6457c16cc70d6790dd43521c899c87ce02f35", symbol: "META", name: "Meta" },
50
+ { address: "0xe93237c50d904957cf27e7b1133b510c669c2e74", symbol: "MSFT", name: "Microsoft" },
51
+ { address: "0xff080c8ce2e5feadaca0da81314ae59d232d4afd", symbol: "MU", name: "Micron" },
52
+ { address: "0xe0444ef8bf4ed74f74fd73686e2ddf4c1c5591e8", symbol: "NFLX", name: "Netflix" },
53
+ { address: "0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec", symbol: "NVDA", name: "NVIDIA" },
54
+ { address: "0xb0992820e760d836549ba69bc7598b4af75dee03", symbol: "ORCL", name: "Oracle" },
55
+ { address: "0x894e1ec2d74ffe5aef8dc8a9e84686accb964f2a", symbol: "PLTR", name: "Palantir" },
56
+ { address: "0xd5f3879160bc7c32ebb4dc785f8a4f505888de68", symbol: "QQQ", name: "Invesco QQQ" },
57
+ { address: "0xb90a19ff0af67f7779aff50a882a9cff42446400", symbol: "SNDK", name: "Sandisk" },
58
+ { address: "0x4a0e65a3eccec6dbe60ae065f2e7bb85fae35eea", symbol: "SPCX", name: "SPACEX" },
59
+ { address: "0x117cc2133c37b721f49de2a7a74833232b3b4c0c", symbol: "SPY", name: "SPDR S&P 500" },
60
+ { address: "0x322f0929c4625ed5bad873c95208d54e1c003b2d", symbol: "TSLA", name: "Tesla" },
61
+ { address: "0xd917b029c761d264c6a312bbbcda868658ef86a6", symbol: "USAR", name: "USA Rare Earth" },
62
+ ];
63
+ exports.RH_BLOCKSCOUT_V2 = "https://robinhoodchain.blockscout.com/api/v2";
64
+ exports.DEXSCREENER = "https://api.dexscreener.com";
65
+ exports.RH_DEX_CHAIN = "robinhood";
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ // DexScreener (Robinhood chain) — arbitrary token resolution + market data.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.dexFetch = dexFetch;
5
+ exports.dexTokenBest = dexTokenBest;
6
+ exports.quoteValueRatio = quoteValueRatio;
7
+ exports.badQuoteWarning = badQuoteWarning;
8
+ exports.dexSearchByTicker = dexSearchByTicker;
9
+ const dex_pair_js_1 = require("../dex-pair.js");
10
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
11
+ async function dexFetch(path) {
12
+ const res = await fetch(`${rh_mcp_constants_js_1.DEXSCREENER}${path}`, {
13
+ headers: { accept: "application/json" },
14
+ signal: AbortSignal.timeout(12000),
15
+ });
16
+ if (!res.ok)
17
+ throw new Error(`DexScreener HTTP ${res.status}`);
18
+ return res.json();
19
+ }
20
+ function rhOnly(pairs) {
21
+ return (pairs ?? []).filter((p) => p.chainId === rh_mcp_constants_js_1.RH_DEX_CHAIN);
22
+ }
23
+ /** Highest-liquidity RH pair that contains `address`. */
24
+ async function dexTokenBest(address) {
25
+ try {
26
+ const data = await dexFetch(`/latest/dex/tokens/${address}`);
27
+ // Only base-side pairs price our token; see pickTokenPair for why the
28
+ // deepest pair overall is the wrong answer. That price feeds TP/SL triggers.
29
+ return (0, dex_pair_js_1.pickTokenPair)(rhOnly(data?.pairs), address);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /**
36
+ * Sanity-check an on-chain quote against DexScreener spot value.
37
+ *
38
+ * The RH engine routes Uniswap V4 pools only. When a token's real depth lives
39
+ * elsewhere (e.g. a V2/V3 WETH pair) the V4 path can be empty and still return a
40
+ * quote — at a catastrophic rate. Returns the fraction of spot value received,
41
+ * or null when it can't be determined (no ETH-quoted pair → no false alarms).
42
+ */
43
+ async function quoteValueRatio(from, to, amountInHuman, amountOutHuman) {
44
+ const tokenSide = from.kind === "token" ? from : to;
45
+ const amtIn = Number(amountInHuman);
46
+ if (!isFinite(amtIn) || amtIn <= 0 || !isFinite(amountOutHuman) || amountOutHuman <= 0) {
47
+ return null;
48
+ }
49
+ try {
50
+ const data = await dexFetch(`/latest/dex/tokens/${tokenSide.address}`);
51
+ const pairs = rhOnly(data?.pairs);
52
+ if (!pairs.length)
53
+ return null;
54
+ // Derive ETH/USD from any ETH/WETH-quoted pair: priceUsd / priceNative.
55
+ const ethPair = pairs.find((p) => {
56
+ const q = p.quoteToken?.symbol?.toUpperCase();
57
+ return (q === "WETH" || q === "ETH") && Number(p.priceNative) > 0 && Number(p.priceUsd) > 0;
58
+ });
59
+ if (!ethPair)
60
+ return null;
61
+ const ethUsd = Number(ethPair.priceUsd) / Number(ethPair.priceNative);
62
+ // Only base-side pairs price OUR token — `priceUsd` always belongs to the
63
+ // base. Sorting across every pair could hand back a completely different
64
+ // token's price, and this value decides whether a swap is blocked.
65
+ const want = tokenSide.address.toLowerCase();
66
+ const ourPairs = pairs.filter((p) => p.baseToken?.address?.toLowerCase() === want);
67
+ const deepest = [...ourPairs].sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
68
+ const tokenUsd = Number(deepest?.priceUsd);
69
+ if (!isFinite(ethUsd) || ethUsd <= 0 || !isFinite(tokenUsd) || tokenUsd <= 0)
70
+ return null;
71
+ const inUsd = from.kind === "eth" ? amtIn * ethUsd : amtIn * tokenUsd;
72
+ const outUsd = to.kind === "eth" ? amountOutHuman * ethUsd : amountOutHuman * tokenUsd;
73
+ if (!isFinite(inUsd) || inUsd <= 0 || !isFinite(outUsd))
74
+ return null;
75
+ const expectedOut = to.kind === "eth" ? inUsd / ethUsd : inUsd / tokenUsd;
76
+ return { ratio: outUsd / inUsd, expectedOut };
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /** Human-readable warning when a quote is far below spot value, else null. */
83
+ function badQuoteWarning(check, toSymbol) {
84
+ if (!check || check.ratio >= 0.75)
85
+ return null;
86
+ const pct = (check.ratio * 100).toFixed(1);
87
+ const lost = ((1 - check.ratio) * 100).toFixed(1);
88
+ return (`🔴 **BAD PRICE — you would receive only ${pct}% of spot value (~${lost}% instant loss).**\n` +
89
+ `Expected ≈ **${check.expectedOut.toLocaleString(undefined, { maximumFractionDigits: 6 })} ${toSymbol}** at DexScreener spot.\n` +
90
+ `Cause: this engine routes **Uniswap V4 pools only**. This token's real depth is likely in a ` +
91
+ `V2/V3 pair the router cannot reach, so it is quoting a near-empty V4 path. **Do not swap.**`);
92
+ }
93
+ /** RH tokens whose base symbol matches `ticker`, deduped by address, ranked by liquidity. */
94
+ async function dexSearchByTicker(ticker) {
95
+ const data = await dexFetch(`/latest/dex/search/?q=${encodeURIComponent(ticker)}`);
96
+ const up = ticker.trim().toUpperCase();
97
+ const byToken = new Map();
98
+ for (const p of rhOnly(data?.pairs)) {
99
+ if (p.baseToken?.symbol?.toUpperCase() !== up)
100
+ continue;
101
+ const key = p.baseToken.address.toLowerCase();
102
+ const prev = byToken.get(key);
103
+ if (!prev || (p.liquidity?.usd ?? 0) > (prev.liquidity?.usd ?? 0))
104
+ byToken.set(key, p);
105
+ }
106
+ return [...byToken.values()].sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0));
107
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ // RH RPC access — provider, raw eth_call/eth_getBalance, and the wei/decimal
3
+ // helpers everything else in rh-mcp-* builds on.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.rhProviderAsync = rhProviderAsync;
6
+ exports.rhRpc = rhRpc;
7
+ exports.rhErc20Balance = rhErc20Balance;
8
+ exports.rhPriceUsd = rhPriceUsd;
9
+ exports.erc20Decimals = erc20Decimals;
10
+ exports.parseHumanToWei = parseHumanToWei;
11
+ exports.formatWei = formatWei;
12
+ const ethers_1 = require("ethers");
13
+ const convex_js_1 = require("../convex.js");
14
+ const config_js_1 = require("../config.js");
15
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
16
+ const rh_mcp_dex_js_1 = require("./rh-mcp-dex.js");
17
+ // Some networks can't reach the RH RPC at all (e.g. ISPs that DNS-block
18
+ // robinhood.com), which would break balances, approvals and broadcasting. Probe
19
+ // once, then transparently relay through Convex — which can reach it — instead.
20
+ const RH_RPC_RELAY = `${convex_js_1.CONVEX_SITE}/mcp/rh/rpc`;
21
+ let _rhRpcMode = null;
22
+ async function rhRpcMode() {
23
+ if (_rhRpcMode)
24
+ return _rhRpcMode;
25
+ try {
26
+ const res = await fetch(rh_mcp_constants_js_1.RH_RPC, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }),
30
+ signal: AbortSignal.timeout(6000),
31
+ });
32
+ if (res.ok) {
33
+ _rhRpcMode = "direct";
34
+ return _rhRpcMode;
35
+ }
36
+ }
37
+ catch {
38
+ /* blocked or unreachable → relay */
39
+ }
40
+ _rhRpcMode = "relay";
41
+ return _rhRpcMode;
42
+ }
43
+ /** Auth header for the Convex relay (session token or API key). */
44
+ function rhRelayHeaders() {
45
+ const token = (0, config_js_1.getSavedToken)();
46
+ return {
47
+ "Content-Type": "application/json",
48
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
49
+ };
50
+ }
51
+ /** Provider that works on both paths — relays via Convex when RPC is blocked. */
52
+ async function rhProviderAsync() {
53
+ if ((await rhRpcMode()) === "direct") {
54
+ return new ethers_1.ethers.JsonRpcProvider(rh_mcp_constants_js_1.RH_RPC, rh_mcp_constants_js_1.RH_CHAIN_ID);
55
+ }
56
+ const req = new ethers_1.ethers.FetchRequest(RH_RPC_RELAY);
57
+ for (const [k, v] of Object.entries(rhRelayHeaders()))
58
+ req.setHeader(k, v);
59
+ return new ethers_1.ethers.JsonRpcProvider(req, rh_mcp_constants_js_1.RH_CHAIN_ID);
60
+ }
61
+ async function rhRpc(method, params) {
62
+ const relay = (await rhRpcMode()) === "relay";
63
+ const url = relay ? RH_RPC_RELAY : rh_mcp_constants_js_1.RH_RPC;
64
+ const res = await fetch(url, {
65
+ method: "POST",
66
+ headers: relay ? rhRelayHeaders() : { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
68
+ signal: AbortSignal.timeout(20000),
69
+ });
70
+ if (!res.ok)
71
+ throw new Error(`RH RPC HTTP ${res.status}${relay ? " (via relay)" : ""}`);
72
+ const data = (await res.json());
73
+ if (data.error)
74
+ throw new Error(data.error.message ?? "RH RPC error");
75
+ return data.result;
76
+ }
77
+ function encodeBalanceOf(addr) {
78
+ return "0x70a08231" + addr.toLowerCase().replace("0x", "").padStart(64, "0");
79
+ }
80
+ /** Raw ERC-20 balanceOf(owner) on RH chain (wei). 0n on any failure. */
81
+ async function rhErc20Balance(token, owner) {
82
+ try {
83
+ const hex = await rhRpc("eth_call", [{ to: token, data: encodeBalanceOf(owner) }, "latest"]);
84
+ if (hex && hex !== "0x")
85
+ return BigInt(hex);
86
+ }
87
+ catch {
88
+ /* 0 */
89
+ }
90
+ return 0n;
91
+ }
92
+ /** Current USD price of a token on RH chain via DexScreener, or null. */
93
+ async function rhPriceUsd(address) {
94
+ const dex = await (0, rh_mcp_dex_js_1.dexTokenBest)(address);
95
+ const p = dex?.priceUsd != null ? Number(dex.priceUsd) : NaN;
96
+ return isFinite(p) ? p : null;
97
+ }
98
+ /** ERC-20 decimals() via RH RPC; falls back to 18. */
99
+ async function erc20Decimals(addr) {
100
+ try {
101
+ const hex = await rhRpc("eth_call", [{ to: addr, data: "0x313ce567" }, "latest"]);
102
+ if (hex && hex !== "0x") {
103
+ const d = Number(BigInt(hex));
104
+ if (d > 0 && d <= 36)
105
+ return d;
106
+ }
107
+ }
108
+ catch {
109
+ /* default 18 */
110
+ }
111
+ return 18;
112
+ }
113
+ function parseHumanToWei(amount, decimals = 18) {
114
+ const a = amount.trim();
115
+ if (!/^\d+(\.\d+)?$/.test(a))
116
+ throw new Error(`Invalid amount: ${amount}`);
117
+ return ethers_1.ethers.parseUnits(a, decimals).toString();
118
+ }
119
+ function formatWei(raw, decimals = 18, maxFrac = 6) {
120
+ try {
121
+ const n = Number(ethers_1.ethers.formatUnits(raw, decimals));
122
+ return n.toLocaleString(undefined, { maximumFractionDigits: maxFrac });
123
+ }
124
+ catch {
125
+ return raw;
126
+ }
127
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ // Resolve ETH / a catalog stock / a crypto ticker / a raw 0x address to a
3
+ // tradeable RH-chain token, plus the Blockscout-backed wallet balance render.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.isAddress = isAddress;
6
+ exports.isEth = isEth;
7
+ exports.resolveStock = resolveStock;
8
+ exports.resolveTokenSmart = resolveTokenSmart;
9
+ exports.fetchRhBalances = fetchRhBalances;
10
+ const ethers_1 = require("ethers");
11
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
12
+ const rh_mcp_dex_js_1 = require("./rh-mcp-dex.js");
13
+ const rh_mcp_provider_js_1 = require("./rh-mcp-provider.js");
14
+ function isAddress(s) {
15
+ return /^0x[a-fA-F0-9]{40}$/.test(s);
16
+ }
17
+ function isEth(sym) {
18
+ const s = sym.trim().toUpperCase();
19
+ return s === "ETH" || s === "WETH" || s === "NATIVE" || s === rh_mcp_constants_js_1.NATIVE_ETH.toUpperCase();
20
+ }
21
+ function resolveStock(input) {
22
+ const t = input.trim();
23
+ if (isEth(t))
24
+ return null;
25
+ if (isAddress(t)) {
26
+ const hit = rh_mcp_constants_js_1.RH_STOCKS.find((s) => s.address.toLowerCase() === t.toLowerCase());
27
+ return hit ?? { address: t, symbol: t.slice(0, 8), name: "Unknown token" };
28
+ }
29
+ const up = t.toUpperCase();
30
+ return rh_mcp_constants_js_1.RH_STOCKS.find((s) => s.symbol === up) ?? null;
31
+ }
32
+ /**
33
+ * Resolve ETH, a catalog stock, a crypto ticker, or a raw 0x address to a
34
+ * tradeable RH-chain token. Tickers resolve via DexScreener (highest liquidity
35
+ * wins) — callers should confirm the contract address with the user first.
36
+ */
37
+ async function resolveTokenSmart(input) {
38
+ if (isEth(input))
39
+ return { kind: "eth", address: rh_mcp_constants_js_1.NATIVE_ETH, symbol: "ETH", decimals: 18 };
40
+ const t = input.trim();
41
+ if (t.toUpperCase() === "USDG") {
42
+ return { kind: "token", address: rh_mcp_constants_js_1.USDG_ADDRESS, symbol: "USDG", name: "Global Dollar", decimals: rh_mcp_constants_js_1.USDG_DECIMALS };
43
+ }
44
+ // 1) catalog symbol fast-path (22 tokenized stocks, all 18-decimals)
45
+ const cat = rh_mcp_constants_js_1.RH_STOCKS.find((s) => s.symbol === t.toUpperCase());
46
+ if (cat)
47
+ return { kind: "token", address: cat.address, symbol: cat.symbol, name: cat.name, decimals: 18 };
48
+ // 2) raw contract address
49
+ if (isAddress(t)) {
50
+ const dex = await (0, rh_mcp_dex_js_1.dexTokenBest)(t);
51
+ const decimals = await (0, rh_mcp_provider_js_1.erc20Decimals)(t);
52
+ const known = resolveStock(t);
53
+ const symbol = dex?.baseToken?.address?.toLowerCase() === t.toLowerCase()
54
+ ? dex.baseToken.symbol
55
+ : known?.symbol ?? dex?.quoteToken?.symbol ?? `${t.slice(0, 6)}…`;
56
+ return { kind: "token", address: t, symbol, name: dex?.baseToken?.name ?? known?.name, decimals, dex };
57
+ }
58
+ // 3) crypto ticker via DexScreener
59
+ const cands = await (0, rh_mcp_dex_js_1.dexSearchByTicker)(t);
60
+ if (cands.length === 0) {
61
+ throw new Error(`Unknown RH asset "${input}". Not in the stock catalog and no Robinhood-chain token matched on DexScreener. Paste the 0x contract address instead.`);
62
+ }
63
+ const best = cands[0];
64
+ const decimals = await (0, rh_mcp_provider_js_1.erc20Decimals)(best.baseToken.address);
65
+ return {
66
+ kind: "token",
67
+ address: best.baseToken.address,
68
+ symbol: best.baseToken.symbol,
69
+ name: best.baseToken.name,
70
+ decimals,
71
+ dex: best,
72
+ };
73
+ }
74
+ async function fetchRhBalances(address) {
75
+ if (!isAddress(address))
76
+ return "Invalid address.";
77
+ let ethStr = "?";
78
+ try {
79
+ const hex = await (0, rh_mcp_provider_js_1.rhRpc)("eth_getBalance", [address, "latest"]);
80
+ ethStr = (Number(BigInt(hex)) / 1e18).toFixed(6);
81
+ }
82
+ catch (e) {
83
+ ethStr = `error: ${e?.message ?? "rpc"}`;
84
+ }
85
+ // Blockscout lists EVERY token held, not just the 18-stock catalog — the old
86
+ // per-catalog RPC loop was blind to arbitrary crypto we can now trade. It's
87
+ // also 1 request instead of 18, and stays reachable when the RPC is blocked.
88
+ const holdings = [];
89
+ try {
90
+ const res = await fetch(`${rh_mcp_constants_js_1.RH_BLOCKSCOUT_V2}/addresses/${address}/token-balances`, {
91
+ headers: { accept: "application/json" },
92
+ signal: AbortSignal.timeout(15000),
93
+ });
94
+ if (res.ok) {
95
+ const data = await res.json();
96
+ const items = Array.isArray(data) ? data : (data?.items ?? []);
97
+ for (const it of items) {
98
+ const tk = it?.token ?? {};
99
+ if (tk.type && String(tk.type).toUpperCase() !== "ERC-20")
100
+ continue;
101
+ const dec = Number(tk.decimals ?? 18);
102
+ const raw = String(it?.value ?? "0");
103
+ let bal = 0;
104
+ try {
105
+ bal = Number(ethers_1.ethers.formatUnits(raw, isFinite(dec) ? dec : 18));
106
+ }
107
+ catch {
108
+ continue;
109
+ }
110
+ if (bal > 1e-9) {
111
+ holdings.push({
112
+ symbol: tk.symbol ?? "?",
113
+ bal: bal.toLocaleString(undefined, { maximumFractionDigits: 6 }),
114
+ address: tk.address_hash ?? tk.address ?? "",
115
+ });
116
+ }
117
+ }
118
+ holdings.sort((a, b) => a.symbol.localeCompare(b.symbol));
119
+ }
120
+ }
121
+ catch {
122
+ /* fall through — ETH balance still reported */
123
+ }
124
+ const lines = [
125
+ `**Wallet**: \`${address}\``,
126
+ ``,
127
+ `**Balances on Robinhood Chain (${rh_mcp_constants_js_1.RH_CHAIN_ID}):**`,
128
+ `- ETH (gas): ${ethStr}`,
129
+ ];
130
+ if (holdings.length === 0)
131
+ lines.push(`- (no token balances)`);
132
+ else
133
+ for (const h of holdings)
134
+ lines.push(`- ${h.symbol}: ${h.bal} \`${h.address}\``);
135
+ lines.push(``, `_RPC: ${(0, rh_mcp_constants_js_1.rhRpcDisplay)()}_`, `_Explorer: ${rh_mcp_constants_js_1.RH_EXPLORER}/address/${address}_`);
136
+ return lines.join("\n");
137
+ }
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ // Market risk pre-screen (heuristic, DexScreener data only) — powers
3
+ // rh_analyze, rh_token_resolve and rh_mcp_list_stocks rendering.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.fmtUsd = fmtUsd;
6
+ exports.fmtPct = fmtPct;
7
+ exports.assessRisk = assessRisk;
8
+ exports.renderResolveList = renderResolveList;
9
+ exports.renderResolveOne = renderResolveOne;
10
+ exports.buildRhAnalysis = buildRhAnalysis;
11
+ exports.buildRhStocksList = buildRhStocksList;
12
+ exports.renderAnalysis = renderAnalysis;
13
+ const rh_mcp_constants_js_1 = require("./rh-mcp-constants.js");
14
+ function fmtUsd(n) {
15
+ const v = Number(n ?? 0);
16
+ if (!isFinite(v))
17
+ return "0";
18
+ if (v >= 1e9)
19
+ return (v / 1e9).toFixed(2) + "B";
20
+ if (v >= 1e6)
21
+ return (v / 1e6).toFixed(2) + "M";
22
+ if (v >= 1e3)
23
+ return (v / 1e3).toFixed(1) + "K";
24
+ return v.toFixed(2);
25
+ }
26
+ function fmtPct(n) {
27
+ if (n == null || !isFinite(n))
28
+ return "n/a";
29
+ return (n > 0 ? "+" : "") + Number(n).toFixed(1) + "%";
30
+ }
31
+ function assessRisk(dex) {
32
+ let score = 0;
33
+ const flags = [];
34
+ const liq = dex.liquidity?.usd ?? 0;
35
+ if (liq < 10000) {
36
+ score += 40;
37
+ flags.push(`🔴 Very low liquidity ($${fmtUsd(liq)}) — high slippage & rug risk`);
38
+ }
39
+ else if (liq < 25000) {
40
+ score += 28;
41
+ flags.push(`🟠 Low liquidity ($${fmtUsd(liq)})`);
42
+ }
43
+ else if (liq < 100000) {
44
+ score += 15;
45
+ flags.push(`🟡 Moderate liquidity ($${fmtUsd(liq)})`);
46
+ }
47
+ else {
48
+ flags.push(`🟢 Liquidity $${fmtUsd(liq)}`);
49
+ }
50
+ if (dex.pairCreatedAt) {
51
+ const ageH = (Date.now() - dex.pairCreatedAt) / 3600000;
52
+ if (ageH < 24) {
53
+ score += 25;
54
+ flags.push(`🔴 Very new pair (${ageH.toFixed(0)}h old)`);
55
+ }
56
+ else if (ageH < 24 * 7) {
57
+ score += 12;
58
+ flags.push(`🟠 New pair (${(ageH / 24).toFixed(1)}d old)`);
59
+ }
60
+ else {
61
+ flags.push(`🟢 Pair age ${(ageH / 24).toFixed(0)}d`);
62
+ }
63
+ }
64
+ else {
65
+ flags.push("⚪ Pair age unknown");
66
+ }
67
+ const vol = dex.volume?.h24 ?? 0;
68
+ const ratio = liq > 0 ? vol / liq : 0;
69
+ if (ratio > 20) {
70
+ score += 15;
71
+ flags.push(`🔴 24h volume ${ratio.toFixed(1)}× liquidity — possible wash/hype spike`);
72
+ }
73
+ else if (ratio > 5) {
74
+ score += 6;
75
+ flags.push(`🟠 High turnover (${ratio.toFixed(1)}× liq)`);
76
+ }
77
+ const buys = dex.txns?.h24?.buys ?? 0;
78
+ const sells = dex.txns?.h24?.sells ?? 0;
79
+ if (buys + sells < 10) {
80
+ score += 8;
81
+ flags.push(`🟠 Thin activity (${buys + sells} txns/24h)`);
82
+ }
83
+ else if (sells > buys * 2) {
84
+ score += 10;
85
+ flags.push(`🟠 Sell-heavy (${sells} sells vs ${buys} buys/24h) — distribution`);
86
+ }
87
+ const ch = dex.priceChange?.h24;
88
+ if (ch != null && Math.abs(ch) > 50) {
89
+ score += 8;
90
+ flags.push(`🟠 Volatile: ${fmtPct(ch)} 24h`);
91
+ }
92
+ const hasSocials = ((dex.info?.socials?.length ?? 0) + (dex.info?.websites?.length ?? 0)) > 0;
93
+ if (!hasSocials) {
94
+ score += 6;
95
+ flags.push("🟡 No website/socials listed on DexScreener");
96
+ }
97
+ // ── Implausibility checks ────────────────────────────────────────────────
98
+ // A launchpad whitelist ages badly — new platforms appear constantly. These
99
+ // look instead at whether the numbers are internally coherent, which a
100
+ // bundled or honeypot token cannot fake without giving up the illusion.
101
+ // Exit ratio: notional value vs the pool that would have to absorb a sale.
102
+ // A $35M "market cap" sitting on $17K of liquidity is not a market cap —
103
+ // it is a quote nobody can realise.
104
+ const fdv = dex.fdv ?? dex.marketCap ?? 0;
105
+ if (fdv > 0 && liq > 0) {
106
+ const exitRatio = fdv / liq;
107
+ if (exitRatio > 500) {
108
+ score += 22;
109
+ flags.push(`🔴 FDV is ${exitRatio.toFixed(0)}× liquidity ($${fmtUsd(fdv)} vs $${fmtUsd(liq)}) — ` +
110
+ `the quoted valuation cannot be exited; treat the price as notional`);
111
+ }
112
+ else if (exitRatio > 100) {
113
+ score += 12;
114
+ flags.push(`🟠 FDV ${exitRatio.toFixed(0)}× liquidity — thin float behind the headline value`);
115
+ }
116
+ }
117
+ // Wash pattern: heavy volume relative to the pool, with buys and sells almost
118
+ // perfectly matched. Organic trading is lopsided; round-tripping is not.
119
+ if (buys > 40 && sells > 40 && liq > 0) {
120
+ const symmetry = Math.abs(buys - sells) / (buys + sells); // 0 = perfectly matched
121
+ if (symmetry < 0.06 && ratio > 3) {
122
+ score += 15;
123
+ flags.push(`🔴 Buy/sell almost perfectly matched (${buys}/${sells}) on ${ratio.toFixed(1)}× turnover — ` +
124
+ `consistent with wash trading or a bundled wallet set`);
125
+ }
126
+ }
127
+ // Implausibly uniform trade size: many transactions, negligible average value.
128
+ const txCount = buys + sells;
129
+ if (txCount > 100 && vol > 0) {
130
+ const avgTrade = vol / txCount;
131
+ if (avgTrade < 25) {
132
+ score += 10;
133
+ flags.push(`🟠 Average trade only $${avgTrade.toFixed(2)} across ${txCount} txns — ` +
134
+ `bot-like churn rather than real participation`);
135
+ }
136
+ }
137
+ // Buys with no exits at all is the clearest honeypot tell available here.
138
+ if (buys > 10 && sells === 0) {
139
+ score += 30;
140
+ flags.push(`🔴 ${buys} buys and ZERO sells in 24h — nobody has exited; possible honeypot`);
141
+ }
142
+ score = Math.min(100, score);
143
+ const tier = score >= 70 ? "🔴 EXTREME" : score >= 45 ? "🟠 HIGH" : score >= 25 ? "🟡 MEDIUM" : "🟢 LOWER";
144
+ return { score, tier, flags };
145
+ }
146
+ function renderResolveList(query, cands) {
147
+ const rows = cands.slice(0, 8).map((p, i) => `| ${i === 0 ? "⭐" : " "} | \`${p.baseToken.address}\` | $${fmtUsd(p.liquidity?.usd ?? 0)} | $${fmtUsd(p.volume?.h24 ?? 0)} | ${p.priceUsd ? "$" + p.priceUsd : "?"} |`);
148
+ return [
149
+ `## 🔎 RH resolve — "${query}" (${cands.length} match${cands.length === 1 ? "" : "es"})`,
150
+ "",
151
+ "| | Contract | Liquidity | Vol 24h | Price |",
152
+ "|--|----------|-----------|---------|-------|",
153
+ ...rows,
154
+ "",
155
+ `⭐ = highest liquidity. ⚠️ **Tickers can be spoofed** — several tokens may share "${query}". Trade using the exact contract address, never the ticker alone.`,
156
+ "",
157
+ `Next: \`rh_analyze\` the contract, then \`rh_mcp_estimate\` / \`rh_mcp_swap\` with the CA.`,
158
+ ].join("\n");
159
+ }
160
+ function renderResolveOne(address, dex) {
161
+ const isBase = dex.baseToken.address.toLowerCase() === address.toLowerCase();
162
+ const tok = isBase ? dex.baseToken : dex.quoteToken;
163
+ return [
164
+ `## 🔎 RH token — ${tok.symbol}${tok.name ? ` (${tok.name})` : ""}`,
165
+ `\`${tok.address}\` · chain 4663`,
166
+ "",
167
+ `**Price**: ${dex.priceUsd ? "$" + dex.priceUsd : "?"} · **24h**: ${fmtPct(dex.priceChange?.h24)}`,
168
+ `**Liquidity**: $${fmtUsd(dex.liquidity?.usd ?? 0)} · **Vol 24h**: $${fmtUsd(dex.volume?.h24 ?? 0)}`,
169
+ `**Pair**: ${dex.dexId ?? "uniswap"} · ${rh_mcp_constants_js_1.RH_EXPLORER}/address/${dex.pairAddress}`,
170
+ "",
171
+ `Next: \`rh_analyze\` for a risk read, then \`rh_mcp_estimate\` / \`rh_mcp_swap\`.`,
172
+ ].join("\n");
173
+ }
174
+ // structuredContent payload for rh_analyze - mirrors buildRhSafetyStructured:
175
+ // `tier` is the assessRisk label with its leading emoji stripped
176
+ // ("EXTREME"/"HIGH"/"MEDIUM"/"LOWER"), single source, no thresholds duplicated.
177
+ function buildRhAnalysis(r, dex, assessment) {
178
+ return {
179
+ address: r.address,
180
+ symbol: r.symbol ?? null,
181
+ name: r.name ?? null,
182
+ priceUsd: dex.priceUsd ? Number(dex.priceUsd) : null,
183
+ change1hPct: dex.priceChange?.h1 ?? null,
184
+ change24hPct: dex.priceChange?.h24 ?? null,
185
+ liquidityUsd: dex.liquidity?.usd ?? null,
186
+ volume24hUsd: dex.volume?.h24 ?? null,
187
+ fdvUsd: dex.fdv ?? null,
188
+ marketCapUsd: dex.marketCap ?? null,
189
+ buys24h: dex.txns?.h24?.buys ?? null,
190
+ sells24h: dex.txns?.h24?.sells ?? null,
191
+ pairAgeDays: dex.pairCreatedAt ? Math.floor((Date.now() - dex.pairCreatedAt) / 86400000) : null,
192
+ riskScore: assessment.score,
193
+ tier: assessment.tier.replace(/^\S+\s+/, ""),
194
+ flags: assessment.flags,
195
+ };
196
+ }
197
+ // structuredContent payload for rh_mcp_list_stocks - a machine-readable
198
+ // mirror of the human table (already filtered by the caller's query).
199
+ function buildRhStocksList(rows) {
200
+ return {
201
+ count: rows.length,
202
+ stocks: rows.map((s) => ({ symbol: s.symbol, name: s.name, address: s.address })),
203
+ };
204
+ }
205
+ function renderAnalysis(r, dex, assessment) {
206
+ const { score, tier, flags } = assessment;
207
+ const ageLine = dex.pairCreatedAt
208
+ ? `**Age**: ${((Date.now() - dex.pairCreatedAt) / 86400000).toFixed(1)}d`
209
+ : "**Age**: unknown";
210
+ return [
211
+ `## 🔬 RH Token Analysis — ${r.symbol}${r.name ? ` (${r.name})` : ""}`,
212
+ `\`${r.address}\` · chain 4663`,
213
+ "",
214
+ `**Price**: ${dex.priceUsd ? "$" + dex.priceUsd : "?"} · **24h**: ${fmtPct(dex.priceChange?.h24)} · **1h**: ${fmtPct(dex.priceChange?.h1)}`,
215
+ `**Liquidity**: $${fmtUsd(dex.liquidity?.usd ?? 0)} · **Vol 24h**: $${fmtUsd(dex.volume?.h24 ?? 0)}`,
216
+ `**FDV**: $${fmtUsd(dex.fdv ?? 0)} · **MCap**: $${fmtUsd(dex.marketCap ?? 0)}`,
217
+ `**Txns 24h**: ${dex.txns?.h24?.buys ?? 0} buys / ${dex.txns?.h24?.sells ?? 0} sells · ${ageLine}`,
218
+ "",
219
+ `### Risk: ${tier} — ${score}/100`,
220
+ ...flags.map((f) => `- ${f}`),
221
+ "",
222
+ `**Market data only (DexScreener).** For a full verdict also run:`,
223
+ `- \`audit_contract\` — honeypot / mint authority / owner privileges`,
224
+ `- \`deep_research\` or \`web_scrape\` — X/Twitter sentiment & narrative`,
225
+ "",
226
+ `**Pair chart**: ${rh_mcp_constants_js_1.RH_EXPLORER}/address/${dex.pairAddress}`,
227
+ "",
228
+ `_Heuristic pre-screen, not financial advice. Thin RH-chain liquidity = elevated rug risk. Verify the contract address before trading._`,
229
+ ].join("\n");
230
+ }