@0xprotovox/deficlaw 0.1.1 → 0.2.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 (39) hide show
  1. package/README.md +60 -39
  2. package/dist/analysis/formatOutput.d.ts +113 -2
  3. package/dist/analysis/formatOutput.js +135 -100
  4. package/dist/analysis/formatOutput.js.map +1 -1
  5. package/dist/analysis/riskScorer.d.ts +24 -2
  6. package/dist/analysis/riskScorer.js +146 -48
  7. package/dist/analysis/riskScorer.js.map +1 -1
  8. package/dist/analysis/summaryGenerator.d.ts +3 -2
  9. package/dist/analysis/summaryGenerator.js +125 -42
  10. package/dist/analysis/summaryGenerator.js.map +1 -1
  11. package/dist/cache/memoryCache.d.ts +12 -3
  12. package/dist/cache/memoryCache.js +43 -2
  13. package/dist/cache/memoryCache.js.map +1 -1
  14. package/dist/server.d.ts +3 -2
  15. package/dist/server.js +28 -16
  16. package/dist/server.js.map +1 -1
  17. package/dist/sources/dexscreener.d.ts +6 -17
  18. package/dist/sources/dexscreener.js +72 -26
  19. package/dist/sources/dexscreener.js.map +1 -1
  20. package/dist/sources/gmgn.d.ts +11 -4
  21. package/dist/sources/gmgn.js +179 -105
  22. package/dist/sources/gmgn.js.map +1 -1
  23. package/dist/sources/solanaRpc.d.ts +4 -4
  24. package/dist/sources/solanaRpc.js +60 -29
  25. package/dist/sources/solanaRpc.js.map +1 -1
  26. package/dist/tools/analyzeToken.d.ts +1 -2
  27. package/dist/tools/analyzeToken.js +114 -100
  28. package/dist/tools/analyzeToken.js.map +1 -1
  29. package/dist/tools/getPrice.d.ts +2 -12
  30. package/dist/tools/getPrice.js +23 -4
  31. package/dist/tools/getPrice.js.map +1 -1
  32. package/dist/tools/getTopTraders.d.ts +30 -24
  33. package/dist/tools/getTopTraders.js +36 -21
  34. package/dist/tools/getTopTraders.js.map +1 -1
  35. package/dist/tools/getTrending.d.ts +8 -1
  36. package/dist/tools/getTrending.js +12 -2
  37. package/dist/tools/getTrending.js.map +1 -1
  38. package/dist/types/index.d.ts +34 -0
  39. package/package.json +18 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # DefiClaw
1
+ # deficlaw
2
2
 
3
3
  **The first open-source DeFi MCP server for Claude Code.**
4
4
 
@@ -7,13 +7,13 @@ Analyze any Solana token in seconds. Holder intelligence, risk scoring, smart mo
7
7
  ```
8
8
  > "analyze token 3oQwNvAfZMuPWjVPC12ukY7RPA9JiGwLod6Pr4Lkpump"
9
9
 
10
- ═══ SUMMARY ═══
10
+ === SUMMARY ===
11
11
  POKE6900 is a 9mo old PumpSwap token with $25.9K liquidity and $34.5K market cap.
12
12
  53 diamond hands (53.0%), strong holder conviction.
13
13
  Only 18.0% of holders are in profit, most are underwater with avg loss of $844 per wallet.
14
14
  Buy pressure is strong at 3.3:1 ratio (1093 buys vs 395 sells), accumulation phase.
15
15
  Contract looks safe: mint and freeze authorities revoked.
16
- 🟢 Lower risk profile based on available data.
16
+ Lower risk profile based on available data.
17
17
  ```
18
18
 
19
19
  ## Why deficlaw?
@@ -27,18 +27,24 @@ Claude Code can write code, fix bugs, deploy apps. But ask it "what's the price
27
27
  - **Trending Tokens** with volume, liquidity, and boost data
28
28
  - **Top Traders** showing who made and lost money on any token
29
29
  - **Contract Security** checking mint/freeze authority on Solana
30
- - **1.5 second** full analysis (not 11 seconds like browser-based scrapers)
30
+ - **~1.5 second** full analysis (not 11 seconds like browser-based scrapers)
31
31
  - **Zero config** - no API keys, no wallets, no accounts needed
32
32
 
33
- ## Quick Start
33
+ ## Install
34
+
35
+ ### Option A: npm (recommended)
36
+
37
+ ```bash
38
+ npm install -g @0xprotovox/deficlaw
39
+ claude mcp add defi -- deficlaw
40
+ ```
41
+
42
+ ### Option B: From source
34
43
 
35
44
  ```bash
36
- # Clone and build
37
45
  git clone https://github.com/0xprotovox/deficlaw.git
38
46
  cd deficlaw
39
47
  npm install && npm run build
40
-
41
- # Add to Claude Code
42
48
  claude mcp add defi -- node /path/to/deficlaw/dist/index.js
43
49
  ```
44
50
 
@@ -81,12 +87,12 @@ Full token analysis with holder intelligence, risk scoring, and human-readable s
81
87
 
82
88
  ### `get_price`
83
89
 
84
- Quick price lookup. Sub-second response.
90
+ Quick price lookup. Sub-second response. Supports both token addresses and name/symbol search.
85
91
 
86
92
  ```
87
93
  > "price of So11111112222..."
88
94
 
89
- BONK $0.000024 (+5.2% 24h)
95
+ BONK -- $0.000024 (+5.2% 24h)
90
96
  Volume: $142M | Liquidity: $12M | MCap: $1.8B
91
97
  ```
92
98
 
@@ -97,9 +103,9 @@ Trending and boosted tokens on any chain.
97
103
  ```
98
104
  > "trending tokens on solana"
99
105
 
100
- 1. BONK $0.000024 (+12%) $142M volume
101
- 2. WIF $0.89 (-2.1%) $89M volume
102
- 3. JUP $0.94 (+3.5%) $45M volume
106
+ 1. BONK -- $0.000024 (+12%) -- $142M volume
107
+ 2. WIF -- $0.89 (-2.1%) -- $89M volume
108
+ 3. JUP -- $0.94 (+3.5%) -- $45M volume
103
109
  ...
104
110
  ```
105
111
 
@@ -110,13 +116,13 @@ Who made and lost money on a token. Winners, losers, PnL, tags.
110
116
  ```
111
117
  > "who profited on this token?"
112
118
 
113
- 🏆 Top Winners:
114
- 1. CR5N... +$680 (+124%) 🟢 still holding
115
- 2. EsRB... +$166 (+16%) 💎 diamond hands
119
+ Top Winners:
120
+ 1. CR5N... -- +$680 (+124%) still holding
121
+ 2. EsRB... -- +$166 (+16%) diamond hands
116
122
 
117
- 💀 Top Losers:
118
- 1. BgeeV... -$7,548 (-65%) 💎 diamond hands (!)
119
- 2. 5vqid... -$5,420 (-50%) gmgn user
123
+ Top Losers:
124
+ 1. BgeeV... -- -$7,548 (-65%) diamond hands (!)
125
+ 2. 5vqid... -- -$5,420 (-50%) gmgn user
120
126
  ```
121
127
 
122
128
  ## Data Sources
@@ -141,34 +147,36 @@ Holder analysis (GMGN) currently supports **Solana** tokens.
141
147
 
142
148
  ## Risk Scoring
143
149
 
144
- The risk scorer analyzes 6 dimensions to produce a 0-100 score:
150
+ The risk scorer analyzes 8 dimensions to produce a 0-100 score:
145
151
 
146
- | Dimension | Weight | What it checks |
147
- |-----------|--------|----------------|
148
- | Liquidity | 25% | Pool depth in USD |
149
- | Token Age | 10% | Time since creation |
150
- | Holder Concentration | 20% | Top 10 holder % |
151
- | Dev Wallet | 20% | Dev holdings and selling behavior |
152
- | Fresh Wallets | 15% | New wallet % (possible wash/bundle) |
153
- | Sniper Activity | 10% | Bot/sniper wallets in top holders |
152
+ | Dimension | Max Points | What it checks |
153
+ |-----------|-----------|----------------|
154
+ | Liquidity | 22 | Pool depth in USD |
155
+ | Token Age | 10 | Time since creation |
156
+ | Volume Anomalies | 8 | Wash trading signals, vol/mcap ratio |
157
+ | Holder Concentration | 18 | Top 10 holder % |
158
+ | Dev Wallet | 15 | Dev holdings and selling behavior |
159
+ | Fresh Wallets | 12 | New wallet % (possible wash/bundle) |
160
+ | Sniper Activity | 10 | Bot/sniper wallets in top holders |
161
+ | Contract Security | 5 | Mint/freeze authority status |
154
162
 
155
- **Levels:** 🟢 LOW (0-19) | 🟡 MEDIUM (20-44) | 🟠 HIGH (45-69) | 🔴 CRITICAL (70-100)
163
+ **Levels:** LOW (0-19) | MEDIUM (20-44) | HIGH (45-69) | CRITICAL (70-100)
156
164
 
157
165
  ## Architecture
158
166
 
159
167
  ```
160
- Claude Code ←→ MCP stdio ←→ deficlaw server
161
-
162
- ┌────────────┼────────────┐
163
-
168
+ Claude Code <-> MCP stdio <-> deficlaw server
169
+ |
170
+ +------------+------------+
171
+ | | |
164
172
  DexScreener GMGN API Solana RPC
165
173
  (prices) (holders) (security)
166
174
  ```
167
175
 
168
176
  - **MCP SDK** for Claude Code integration
169
177
  - **curl-based GMGN fetcher** bypasses Cloudflare (Playwright fallback if needed)
170
- - **In-memory TTL cache** prevents rate limiting
171
- - **Rate limiter** for DexScreener (150 req/min)
178
+ - **In-memory TTL cache** with automatic cleanup prevents rate limiting
179
+ - **Rate limiter** with retry logic for DexScreener (150 req/min)
172
180
  - **TypeScript** with full type safety
173
181
 
174
182
  ## Configuration
@@ -187,8 +195,8 @@ Add to your project's `.mcp.json`:
187
195
  {
188
196
  "mcpServers": {
189
197
  "defi": {
190
- "command": "node",
191
- "args": ["/path/to/deficlaw/dist/index.js"],
198
+ "command": "npx",
199
+ "args": ["-y", "@0xprotovox/deficlaw"],
192
200
  "env": {
193
201
  "SOLANA_RPC_URL": "https://your-rpc.com"
194
202
  }
@@ -197,19 +205,32 @@ Add to your project's `.mcp.json`:
197
205
  }
198
206
  ```
199
207
 
208
+ Or if installed from source:
209
+
210
+ ```json
211
+ {
212
+ "mcpServers": {
213
+ "defi": {
214
+ "command": "node",
215
+ "args": ["/path/to/deficlaw/dist/index.js"]
216
+ }
217
+ }
218
+ }
219
+ ```
220
+
200
221
  ## Roadmap
201
222
 
202
223
  - [x] Token analysis with holder intelligence
203
- - [x] Risk scoring (6 dimensions)
224
+ - [x] Risk scoring (8 dimensions)
204
225
  - [x] Contract security checks
205
226
  - [x] KOL detection with Twitter handles
206
227
  - [x] Human-readable AI summary
207
228
  - [x] Top traders (winners/losers)
229
+ - [x] npm package (`npm install -g @0xprotovox/deficlaw`)
208
230
  - [ ] Token search by name/symbol
209
231
  - [ ] Slippage estimation (Jupiter quotes)
210
232
  - [ ] Token comparison (side by side)
211
233
  - [ ] Multi-chain holder analysis
212
- - [ ] npm package (`npm install -g deficlaw`)
213
234
  - [ ] Price alerts via MCP resources
214
235
 
215
236
  ## Built With
@@ -1,4 +1,115 @@
1
1
  /**
2
- * Format analysis output clean, readable, both summary + detailed numbers
2
+ * Format analysis output as clean, readable plain text.
3
+ * Both human-readable summary and detailed numbers.
3
4
  */
4
- export declare function formatAnalysis(data: any): string;
5
+ interface AnalysisData {
6
+ summary?: string;
7
+ token: {
8
+ name: string;
9
+ symbol: string;
10
+ chain: string;
11
+ address: string;
12
+ age?: string;
13
+ createdAt?: string;
14
+ };
15
+ price: {
16
+ usd: number;
17
+ change5m?: number;
18
+ change1h: number;
19
+ change6h?: number;
20
+ change24h: number;
21
+ };
22
+ market: {
23
+ marketCap: number;
24
+ fdv: number;
25
+ liquidity: number;
26
+ volume24h: number;
27
+ volume6h?: number;
28
+ volume1h: number;
29
+ volume5m?: number;
30
+ volumeLiquidityRatio?: number;
31
+ dex: string;
32
+ pairAddress: string;
33
+ };
34
+ security?: {
35
+ mintAuthority: string;
36
+ freezeAuthority: string;
37
+ supply: number;
38
+ decimals: number;
39
+ };
40
+ risk: {
41
+ score: number;
42
+ level: string;
43
+ flags?: {
44
+ severity: string;
45
+ message: string;
46
+ }[];
47
+ };
48
+ holders?: {
49
+ total: number;
50
+ concentration: {
51
+ top5Pct: number;
52
+ top10Pct: number;
53
+ top20Pct: number;
54
+ };
55
+ categories: Record<string, number>;
56
+ sentiment: {
57
+ profitableHolders: number;
58
+ losingHolders: number;
59
+ profitRatio: number;
60
+ totalPnlUsd: number;
61
+ totalCostUsd: number;
62
+ avgPnlPerHolder: number;
63
+ };
64
+ pressure: {
65
+ totalBuyTx: number;
66
+ totalSellTx: number;
67
+ buySellRatio: number;
68
+ };
69
+ devWallet?: {
70
+ address: string;
71
+ holdingPercent: number;
72
+ pnl: number;
73
+ status: string;
74
+ } | null;
75
+ topHolders: {
76
+ address: string;
77
+ tags: string[];
78
+ supplyPercent: number;
79
+ valueUsd: number;
80
+ pnl: number;
81
+ profitMultiple?: number;
82
+ buyTx?: number;
83
+ sellTx?: number;
84
+ isDeployer?: boolean;
85
+ isFreshWallet?: boolean;
86
+ twitterHandle?: string | null;
87
+ }[];
88
+ };
89
+ lpDetection?: {
90
+ lpAddress: string;
91
+ lpPercent: number;
92
+ realTopHolder?: {
93
+ address: string;
94
+ percent: number;
95
+ } | null;
96
+ };
97
+ kols?: {
98
+ address?: string;
99
+ twitterHandle?: string | null;
100
+ pnl: number;
101
+ status: string;
102
+ tags: string[];
103
+ }[];
104
+ socials?: {
105
+ websites?: string[];
106
+ twitter?: string | null;
107
+ telegram?: string | null;
108
+ };
109
+ meta: {
110
+ sources: string[];
111
+ fetchTimeMs: number;
112
+ };
113
+ }
114
+ export declare function formatAnalysis(data: AnalysisData): string;
115
+ export {};
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Format analysis output clean, readable, both summary + detailed numbers
2
+ * Format analysis output as clean, readable plain text.
3
+ * Both human-readable summary and detailed numbers.
3
4
  */
5
+ /** Format a number as a dollar amount with appropriate precision */
4
6
  function fmt(n) {
5
7
  if (n === 0)
6
8
  return '$0';
@@ -12,89 +14,111 @@ function fmt(n) {
12
14
  return `$${n.toFixed(2)}`;
13
15
  if (Math.abs(n) >= 0.001)
14
16
  return `$${n.toFixed(4)}`;
15
- return `$${n.toFixed(8)}`;
17
+ if (Math.abs(n) >= 0.0000001)
18
+ return `$${n.toFixed(8)}`;
19
+ return `$${n.toExponential(2)}`;
16
20
  }
21
+ /** Format a decimal as a percentage string */
17
22
  function pct(n, digits = 1) {
18
23
  return `${(n * 100).toFixed(digits)}%`;
19
24
  }
25
+ /** Format a price change with directional arrow */
20
26
  function arrow(n) {
21
27
  if (n > 0)
22
- return `+${n.toFixed(2)}% 📈`;
28
+ return `+${n.toFixed(2)}%`;
23
29
  if (n < 0)
24
- return `${n.toFixed(2)}% 📉`;
25
- return '0%';
30
+ return `${n.toFixed(2)}%`;
31
+ return '0%';
26
32
  }
33
+ /** Shorten a blockchain address for display */
27
34
  function shortenAddr(addr) {
28
35
  if (addr.length <= 12)
29
36
  return addr;
30
37
  return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
31
38
  }
39
+ /** Pad or truncate a string to a fixed width */
40
+ function pad(str, width) {
41
+ if (str.length >= width)
42
+ return str.slice(0, width);
43
+ return str + ' '.repeat(width - str.length);
44
+ }
45
+ /** Right-align a string within a fixed width */
46
+ function rpad(str, width) {
47
+ if (str.length >= width)
48
+ return str.slice(0, width);
49
+ return ' '.repeat(width - str.length) + str;
50
+ }
32
51
  export function formatAnalysis(data) {
33
52
  const lines = [];
34
53
  // ═══════ SUMMARY ═══════
35
54
  if (data.summary) {
36
- lines.push('═══ SUMMARY ═══');
55
+ lines.push('=== SUMMARY ===');
37
56
  lines.push(data.summary);
38
57
  lines.push('');
39
58
  }
40
59
  // ═══════ TOKEN INFO ═══════
41
- lines.push('═══ TOKEN ═══');
60
+ lines.push('=== TOKEN ===');
42
61
  lines.push(`Name: ${data.token.name} (${data.token.symbol})`);
43
62
  lines.push(`Chain: ${data.token.chain}`);
44
63
  lines.push(`Address: ${data.token.address}`);
45
- lines.push(`Age: ${data.token.age}`);
64
+ if (data.token.age)
65
+ lines.push(`Age: ${data.token.age}`);
46
66
  if (data.token.createdAt)
47
67
  lines.push(`Created: ${data.token.createdAt}`);
48
68
  lines.push('');
49
69
  // ═══════ PRICE ═══════
50
- lines.push('═══ PRICE ═══');
70
+ lines.push('=== PRICE ===');
51
71
  lines.push(`Price: ${fmt(data.price.usd)}`);
52
- lines.push(`5min: ${arrow(data.price.change5m)}`);
53
- lines.push(`1h: ${arrow(data.price.change1h)}`);
54
- lines.push(`6h: ${arrow(data.price.change6h)}`);
55
- lines.push(`24h: ${arrow(data.price.change24h)}`);
72
+ if (data.price.change5m !== undefined)
73
+ lines.push(`5min: ${arrow(data.price.change5m)}`);
74
+ lines.push(`1h: ${arrow(data.price.change1h)}`);
75
+ if (data.price.change6h !== undefined)
76
+ lines.push(`6h: ${arrow(data.price.change6h)}`);
77
+ lines.push(`24h: ${arrow(data.price.change24h)}`);
56
78
  lines.push('');
57
79
  // ═══════ MARKET ═══════
58
- lines.push('═══ MARKET ═══');
59
- lines.push(`Market Cap: ${fmt(data.market.marketCap)}`);
60
- lines.push(`FDV: ${fmt(data.market.fdv)}`);
61
- lines.push(`Liquidity: ${fmt(data.market.liquidity)}`);
62
- lines.push(`Volume 24h: ${fmt(data.market.volume24h)}`);
63
- lines.push(`Volume 6h: ${fmt(data.market.volume6h)}`);
64
- lines.push(`Volume 1h: ${fmt(data.market.volume1h)}`);
65
- lines.push(`Volume 5m: ${fmt(data.market.volume5m)}`);
66
- lines.push(`Vol/Liq Ratio: ${data.market.volumeLiquidityRatio}x`);
67
- lines.push(`DEX: ${data.market.dex}`);
68
- lines.push(`Pair: ${data.market.pairAddress}`);
80
+ lines.push('=== MARKET ===');
81
+ lines.push(`Market Cap: ${fmt(data.market.marketCap)}`);
82
+ lines.push(`FDV: ${fmt(data.market.fdv)}`);
83
+ lines.push(`Liquidity: ${fmt(data.market.liquidity)}`);
84
+ lines.push(`Volume 24h: ${fmt(data.market.volume24h)}`);
85
+ if (data.market.volume6h !== undefined)
86
+ lines.push(`Volume 6h: ${fmt(data.market.volume6h)}`);
87
+ lines.push(`Volume 1h: ${fmt(data.market.volume1h)}`);
88
+ if (data.market.volume5m !== undefined)
89
+ lines.push(`Volume 5m: ${fmt(data.market.volume5m)}`);
90
+ if (data.market.volumeLiquidityRatio !== undefined) {
91
+ lines.push(`Vol/Liq: ${data.market.volumeLiquidityRatio}x`);
92
+ }
93
+ lines.push(`DEX: ${data.market.dex}`);
94
+ lines.push(`Pair: ${data.market.pairAddress}`);
69
95
  lines.push('');
70
96
  // ═══════ SECURITY ═══════
71
97
  if (data.security) {
72
- lines.push('═══ SECURITY ═══');
98
+ lines.push('=== SECURITY ===');
73
99
  const mintOk = data.security.mintAuthority === 'revoked';
74
100
  const freezeOk = data.security.freezeAuthority === 'revoked';
75
- lines.push(`Mint Authority: ${mintOk ? 'Revoked (safe)' : '⚠️ ACTIVE (can print tokens!)'}`);
76
- lines.push(`Freeze Authority: ${freezeOk ? 'Revoked (safe)' : '⚠️ ACTIVE (can freeze wallets!)'}`);
77
- lines.push(`Total Supply: ${Math.round(data.security.supply).toLocaleString()}`);
78
- lines.push(`Decimals: ${data.security.decimals}`);
101
+ lines.push(`Mint Authority: ${mintOk ? 'Revoked (safe)' : 'ACTIVE (can print tokens!)'}`);
102
+ lines.push(`Freeze Authority: ${freezeOk ? 'Revoked (safe)' : 'ACTIVE (can freeze wallets!)'}`);
103
+ lines.push(`Total Supply: ${Math.round(data.security.supply).toLocaleString()}`);
104
+ lines.push(`Decimals: ${data.security.decimals}`);
79
105
  lines.push('');
80
106
  }
81
107
  // ═══════ RISK ═══════
82
- lines.push('═══ RISK ═══');
83
- const riskMap = { LOW: '🟢', MEDIUM: '🟡', HIGH: '🟠', CRITICAL: '🔴' };
84
- const riskEmoji = riskMap[data.risk.level] || '⚪';
85
- lines.push(`Score: ${data.risk.score}/100 ${riskEmoji} ${data.risk.level}`);
86
- if (data.risk.flags?.length > 0) {
87
- data.risk.flags.forEach((f) => {
88
- const sevMap = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
89
- const fEmoji = sevMap[f.severity] || '⚪';
90
- lines.push(` ${fEmoji} ${f.message}`);
91
- });
108
+ lines.push('=== RISK ===');
109
+ const riskIndicator = { LOW: '[LOW]', MEDIUM: '[MEDIUM]', HIGH: '[HIGH]', CRITICAL: '[CRITICAL]' };
110
+ lines.push(`Score: ${data.risk.score}/100 ${riskIndicator[data.risk.level] ?? data.risk.level}`);
111
+ if (data.risk.flags && data.risk.flags.length > 0) {
112
+ for (const f of data.risk.flags) {
113
+ const sevIndicator = { critical: '[!]', high: '[!]', medium: '[~]', low: '[.]' };
114
+ lines.push(` ${sevIndicator[f.severity] ?? ' '} ${f.message}`);
115
+ }
92
116
  }
93
117
  lines.push('');
94
118
  // ═══════ HOLDERS ═══════
95
119
  if (data.holders) {
96
120
  const h = data.holders;
97
- lines.push('═══ HOLDERS ═══');
121
+ lines.push('=== HOLDERS ===');
98
122
  lines.push(`Total Analyzed: ${h.total}`);
99
123
  lines.push('');
100
124
  // Concentration
@@ -105,71 +129,78 @@ export function formatAnalysis(data) {
105
129
  lines.push('');
106
130
  // Categories
107
131
  lines.push('Categories:');
108
- const cats = h.categories;
109
- if (cats.diamondHands > 0)
110
- lines.push(` 💎 Diamond Hands: ${cats.diamondHands}`);
111
- if (cats.snipers > 0)
112
- lines.push(` 🎯 Snipers: ${cats.snipers}`);
113
- if (cats.freshWallets > 0)
114
- lines.push(` 🆕 Fresh Wallets: ${cats.freshWallets}`);
115
- if (cats.kols > 0)
116
- lines.push(` 👑 KOLs: ${cats.kols}`);
117
- if (cats.smartMoney > 0)
118
- lines.push(` 🧠 Smart Money: ${cats.smartMoney}`);
119
- if (cats.insiders > 0)
120
- lines.push(` 🔑 Insiders: ${cats.insiders}`);
121
- if (cats.devWallets > 0)
122
- lines.push(` 🛠️ Dev Wallets: ${cats.devWallets}`);
123
- if (cats.photonUsers > 0)
124
- lines.push(` 📸 Photon: ${cats.photonUsers}`);
125
- if (cats.gmgnUsers > 0)
126
- lines.push(` 📲 GMGN: ${cats.gmgnUsers}`);
127
- if (cats.transferIn > 0)
128
- lines.push(` ↗️ Transfer In: ${cats.transferIn}`);
132
+ const catEntries = [
133
+ ['diamondHands', h.categories.diamondHands ?? 0, 'Diamond Hands'],
134
+ ['snipers', h.categories.snipers ?? 0, 'Snipers'],
135
+ ['freshWallets', h.categories.freshWallets ?? 0, 'Fresh Wallets'],
136
+ ['kols', h.categories.kols ?? 0, 'KOLs'],
137
+ ['smartMoney', h.categories.smartMoney ?? 0, 'Smart Money'],
138
+ ['insiders', h.categories.insiders ?? 0, 'Insiders'],
139
+ ['devWallets', h.categories.devWallets ?? 0, 'Dev Wallets'],
140
+ ['photonUsers', h.categories.photonUsers ?? 0, 'Photon'],
141
+ ['gmgnUsers', h.categories.gmgnUsers ?? 0, 'GMGN'],
142
+ ['transferIn', h.categories.transferIn ?? 0, 'Transfer In'],
143
+ ];
144
+ for (const [, count, label] of catEntries) {
145
+ if (count > 0)
146
+ lines.push(` ${label}: ${count}`);
147
+ }
129
148
  lines.push('');
130
149
  // Sentiment
131
150
  lines.push('Sentiment:');
132
- lines.push(` Profitable: ${h.sentiment.profitableHolders} (${pct(h.sentiment.profitRatio)})`);
133
- lines.push(` Losing: ${h.sentiment.losingHolders}`);
134
- lines.push(` Total PnL: ${fmt(h.sentiment.totalPnlUsd)}`);
135
- lines.push(` Total Cost: ${fmt(h.sentiment.totalCostUsd)}`);
151
+ lines.push(` Profitable: ${h.sentiment.profitableHolders} (${pct(h.sentiment.profitRatio)})`);
152
+ lines.push(` Losing: ${h.sentiment.losingHolders}`);
153
+ lines.push(` Total PnL: ${fmt(h.sentiment.totalPnlUsd)}`);
154
+ lines.push(` Total Cost: ${fmt(h.sentiment.totalCostUsd)}`);
136
155
  lines.push(` Avg PnL/holder: ${fmt(h.sentiment.avgPnlPerHolder)}`);
137
156
  lines.push('');
138
157
  // Buy/Sell Pressure
139
158
  lines.push('Buy/Sell Pressure:');
140
- lines.push(` Buy TX: ${h.pressure.totalBuyTx}`);
159
+ lines.push(` Buy TX: ${h.pressure.totalBuyTx}`);
141
160
  lines.push(` Sell TX: ${h.pressure.totalSellTx}`);
142
- lines.push(` Ratio: ${h.pressure.buySellRatio.toFixed(2)}:1 ${h.pressure.buySellRatio > 1.5 ? '🟢' : h.pressure.buySellRatio < 0.7 ? '🔴' : '🟡'}`);
161
+ const ratioStr = h.pressure.buySellRatio === Infinity
162
+ ? 'all buys'
163
+ : `${h.pressure.buySellRatio.toFixed(2)}:1`;
164
+ const ratioIndicator = h.pressure.buySellRatio > 1.5 ? ' (bullish)'
165
+ : h.pressure.buySellRatio < 0.7 ? ' (bearish)'
166
+ : ' (neutral)';
167
+ lines.push(` Ratio: ${ratioStr}${ratioIndicator}`);
143
168
  lines.push('');
144
169
  // Dev Wallet
145
170
  if (h.devWallet) {
146
171
  lines.push('Dev Wallet:');
147
- lines.push(` Address: ${shortenAddr(h.devWallet.address)}`);
148
- lines.push(` Holding: ${pct(h.devWallet.holdingPercent)}`);
149
- lines.push(` PnL: ${fmt(h.devWallet.pnl)}`);
150
- lines.push(` Status: ${h.devWallet.status}`);
172
+ lines.push(` Address: ${shortenAddr(h.devWallet.address)}`);
173
+ lines.push(` Holding: ${pct(h.devWallet.holdingPercent)}`);
174
+ lines.push(` PnL: ${fmt(h.devWallet.pnl)}`);
175
+ lines.push(` Status: ${h.devWallet.status}`);
176
+ lines.push('');
177
+ }
178
+ // Top 20 Holders Table (fixed-width columns for alignment)
179
+ if (h.topHolders.length > 0) {
180
+ lines.push('Top Holders:');
181
+ lines.push(` ${rpad('#', 3)} ${pad('Address', 13)} ${rpad('Supply', 7)} ${rpad('Value', 10)} ${rpad('PnL', 11)} Tags`);
182
+ lines.push(` ${'-'.repeat(3)} ${'-'.repeat(13)} ${'-'.repeat(7)} ${'-'.repeat(10)} ${'-'.repeat(11)} ${'----'}`);
183
+ for (let i = 0; i < Math.min(h.topHolders.length, 20); i++) {
184
+ const holder = h.topHolders[i];
185
+ const num = rpad(String(i + 1), 3);
186
+ const addr = pad(shortenAddr(holder.address), 13);
187
+ const supply = rpad(pct(holder.supplyPercent), 7);
188
+ const value = rpad(fmt(holder.valueUsd), 10);
189
+ const pnlSign = holder.pnl >= 0 ? '+' : '';
190
+ const pnlStr = rpad(pnlSign + fmt(holder.pnl), 11);
191
+ const tags = holder.tags
192
+ .filter((t) => !/^TOP\d+$/i.test(t))
193
+ .join(', ') || '-';
194
+ const twitter = holder.twitterHandle ? ` @${holder.twitterHandle}` : '';
195
+ lines.push(` ${num} ${addr} ${supply} ${value} ${pnlStr} ${tags}${twitter}`);
196
+ }
151
197
  lines.push('');
152
198
  }
153
- // Top 20 Holders Table
154
- lines.push('Top 20 Holders:');
155
- lines.push(' # | Address | Supply | Value | PnL | Tags');
156
- lines.push(' ---|-------------|---------|-----------|------------|------');
157
- h.topHolders.slice(0, 20).forEach((holder, i) => {
158
- const num = String(i + 1).padStart(2);
159
- const addr = shortenAddr(holder.address).padEnd(12);
160
- const supply = pct(holder.supplyPercent).padStart(6);
161
- const value = fmt(holder.valueUsd).padStart(9);
162
- const pnl = (holder.pnl >= 0 ? '+' : '') + fmt(holder.pnl);
163
- const tags = holder.tags.filter((t) => !/^TOP\d+$/.test(t)).join(', ') || '-';
164
- const twitter = holder.twitterHandle ? ` @${holder.twitterHandle}` : '';
165
- lines.push(` ${num} | ${addr} | ${supply} | ${value} | ${pnl.padStart(10)} | ${tags}${twitter}`);
166
- });
167
- lines.push('');
168
199
  }
169
200
  // ═══════ LP DETECTION ═══════
170
201
  if (data.lpDetection) {
171
- lines.push('═══ LP POOL ═══');
172
- lines.push(`Top holder is LP pool: ✅`);
202
+ lines.push('=== LP POOL ===');
203
+ lines.push(`Top holder is LP pool`);
173
204
  lines.push(`LP Address: ${shortenAddr(data.lpDetection.lpAddress)}`);
174
205
  lines.push(`LP holds: ${pct(data.lpDetection.lpPercent)}`);
175
206
  if (data.lpDetection.realTopHolder) {
@@ -179,30 +210,34 @@ export function formatAnalysis(data) {
179
210
  }
180
211
  // ═══════ KOLs ═══════
181
212
  if (data.kols && data.kols.length > 0) {
182
- lines.push('═══ KOLs ═══');
183
- data.kols.forEach((k, i) => {
184
- const handle = k.twitterHandle ? `@${k.twitterHandle}` : shortenAddr(k.address);
213
+ lines.push('=== KOLs ===');
214
+ for (let i = 0; i < data.kols.length; i++) {
215
+ const k = data.kols[i];
216
+ const handle = k.twitterHandle ? `@${k.twitterHandle}` : shortenAddr(k.address ?? 'unknown');
185
217
  const pnlStr = k.pnl !== 0 ? ` | PnL: ${fmt(k.pnl)}` : '';
186
- const status = k.status === 'selling' ? '🔴 selling' : '🟢 holding';
187
- const tags = k.tags.filter((t) => !/^TOP\d+$/.test(t) && t !== 'kol').join(', ');
218
+ const status = k.status === 'selling' ? 'selling' : 'holding';
219
+ const tags = k.tags
220
+ .filter((t) => !/^TOP\d+$/i.test(t) && t !== 'kol')
221
+ .join(', ');
188
222
  lines.push(` ${i + 1}. ${handle} | ${status}${pnlStr}${tags ? ` | ${tags}` : ''}`);
189
- });
223
+ }
190
224
  lines.push('');
191
225
  }
192
226
  // ═══════ SOCIALS ═══════
193
227
  if (data.socials) {
194
- lines.push('═══ SOCIALS ═══');
195
- if (data.socials.websites?.length > 0)
196
- lines.push(`Website: ${data.socials.websites.join(', ')}`);
228
+ lines.push('=== SOCIALS ===');
229
+ if (data.socials.websites && data.socials.websites.length > 0) {
230
+ lines.push(`Website: ${data.socials.websites.join(', ')}`);
231
+ }
197
232
  if (data.socials.twitter)
198
- lines.push(`Twitter: ${data.socials.twitter}`);
233
+ lines.push(`Twitter: ${data.socials.twitter}`);
199
234
  if (data.socials.telegram)
200
235
  lines.push(`Telegram: ${data.socials.telegram}`);
201
236
  lines.push('');
202
237
  }
203
238
  // ═══════ META ═══════
204
- lines.push('═══ META ═══');
205
- lines.push(`Sources: ${data.meta.sources.join(' + ')}`);
239
+ lines.push('=== META ===');
240
+ lines.push(`Sources: ${data.meta.sources.join(' + ')}`);
206
241
  lines.push(`Fetch time: ${(data.meta.fetchTimeMs / 1000).toFixed(1)}s`);
207
242
  return lines.join('\n');
208
243
  }