@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.
- package/README.md +60 -39
- package/dist/analysis/formatOutput.d.ts +113 -2
- package/dist/analysis/formatOutput.js +135 -100
- package/dist/analysis/formatOutput.js.map +1 -1
- package/dist/analysis/riskScorer.d.ts +24 -2
- package/dist/analysis/riskScorer.js +146 -48
- package/dist/analysis/riskScorer.js.map +1 -1
- package/dist/analysis/summaryGenerator.d.ts +3 -2
- package/dist/analysis/summaryGenerator.js +125 -42
- package/dist/analysis/summaryGenerator.js.map +1 -1
- package/dist/cache/memoryCache.d.ts +12 -3
- package/dist/cache/memoryCache.js +43 -2
- package/dist/cache/memoryCache.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +28 -16
- package/dist/server.js.map +1 -1
- package/dist/sources/dexscreener.d.ts +6 -17
- package/dist/sources/dexscreener.js +72 -26
- package/dist/sources/dexscreener.js.map +1 -1
- package/dist/sources/gmgn.d.ts +11 -4
- package/dist/sources/gmgn.js +179 -105
- package/dist/sources/gmgn.js.map +1 -1
- package/dist/sources/solanaRpc.d.ts +4 -4
- package/dist/sources/solanaRpc.js +60 -29
- package/dist/sources/solanaRpc.js.map +1 -1
- package/dist/tools/analyzeToken.d.ts +1 -2
- package/dist/tools/analyzeToken.js +114 -100
- package/dist/tools/analyzeToken.js.map +1 -1
- package/dist/tools/getPrice.d.ts +2 -12
- package/dist/tools/getPrice.js +23 -4
- package/dist/tools/getPrice.js.map +1 -1
- package/dist/tools/getTopTraders.d.ts +30 -24
- package/dist/tools/getTopTraders.js +36 -21
- package/dist/tools/getTopTraders.js.map +1 -1
- package/dist/tools/getTrending.d.ts +8 -1
- package/dist/tools/getTrending.js +12 -2
- package/dist/tools/getTrending.js.map +1 -1
- package/dist/types/index.d.ts +34 -0
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
##
|
|
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
|
|
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
|
|
101
|
-
2. WIF
|
|
102
|
-
3. JUP
|
|
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
|
-
|
|
114
|
-
1. CR5N...
|
|
115
|
-
2. EsRB...
|
|
119
|
+
Top Winners:
|
|
120
|
+
1. CR5N... -- +$680 (+124%) still holding
|
|
121
|
+
2. EsRB... -- +$166 (+16%) diamond hands
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
1. BgeeV...
|
|
119
|
-
2. 5vqid...
|
|
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
|
|
150
|
+
The risk scorer analyzes 8 dimensions to produce a 0-100 score:
|
|
145
151
|
|
|
146
|
-
| Dimension |
|
|
147
|
-
|
|
148
|
-
| Liquidity |
|
|
149
|
-
| Token Age | 10
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
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:**
|
|
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
|
|
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": "
|
|
191
|
-
"args": ["/
|
|
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 (
|
|
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
|
|
2
|
+
* Format analysis output as clean, readable plain text.
|
|
3
|
+
* Both human-readable summary and detailed numbers.
|
|
3
4
|
*/
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
55
|
+
lines.push('=== SUMMARY ===');
|
|
37
56
|
lines.push(data.summary);
|
|
38
57
|
lines.push('');
|
|
39
58
|
}
|
|
40
59
|
// ═══════ TOKEN INFO ═══════
|
|
41
|
-
lines.push('
|
|
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
|
-
|
|
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('
|
|
70
|
+
lines.push('=== PRICE ===');
|
|
51
71
|
lines.push(`Price: ${fmt(data.price.usd)}`);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
lines.push(`
|
|
55
|
-
|
|
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('
|
|
59
|
-
lines.push(`Market Cap:
|
|
60
|
-
lines.push(`FDV:
|
|
61
|
-
lines.push(`Liquidity:
|
|
62
|
-
lines.push(`Volume 24h:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
lines.push(`Volume
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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('
|
|
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:
|
|
76
|
-
lines.push(`Freeze Authority: ${freezeOk ? '
|
|
77
|
-
lines.push(`Total Supply:
|
|
78
|
-
lines.push(`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('
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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('
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
133
|
-
lines.push(` Losing:
|
|
134
|
-
lines.push(` Total PnL:
|
|
135
|
-
lines.push(` Total Cost:
|
|
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:
|
|
159
|
+
lines.push(` Buy TX: ${h.pressure.totalBuyTx}`);
|
|
141
160
|
lines.push(` Sell TX: ${h.pressure.totalSellTx}`);
|
|
142
|
-
|
|
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:
|
|
148
|
-
lines.push(` Holding:
|
|
149
|
-
lines.push(` PnL:
|
|
150
|
-
lines.push(` 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('
|
|
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('
|
|
183
|
-
data.kols.
|
|
184
|
-
const
|
|
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' ? '
|
|
187
|
-
const tags = k.tags
|
|
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('
|
|
195
|
-
if (data.socials.websites
|
|
196
|
-
lines.push(`Website:
|
|
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:
|
|
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('
|
|
205
|
-
lines.push(`Sources:
|
|
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
|
}
|