solana-risk-engine 0.4.4 → 0.4.5
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 +147 -145
- package/package.json +1 -1
- package/src/constants.js +179 -93
- package/src/markets.js +11 -9
- package/src/risk.js +302 -293
package/README.md
CHANGED
|
@@ -1,145 +1,147 @@
|
|
|
1
|
-
# solana-risk-engine
|
|
2
|
-
|
|
3
|
-
Explainable risk analysis for Solana tokens and wallets. **Zero dependencies**, runs in **Node 18+ and the browser**, works with free public RPCs out of the box.
|
|
4
|
-
|
|
5
|
-
It doesn't stop at a single score. Every verdict comes with the **signals** behind it, as stable codes plus the numbers involved. Apps can show them in any language (Spanish and English are included).
|
|
6
|
-
|
|
7
|
-
```js
|
|
8
|
-
import { createScanner, explainLight, lightLabel, levelLabel } from "solana-risk-engine";
|
|
9
|
-
|
|
10
|
-
const scanner = createScanner(); // free PublicNode + Vibe Station, or { rpcUrl }
|
|
11
|
-
const r = await scanner.analyzeToken("7xKX...gAsU");
|
|
12
|
-
|
|
13
|
-
console.log(r.lvl); // "HIGH"
|
|
14
|
-
console.log(r.type); // "CREATOR"
|
|
15
|
-
for (const l of r.lights) // the four traffic lights
|
|
16
|
-
console.log(l.s, lightLabel(l.k, "en"), "-", explainLight(l, "en"));
|
|
17
|
-
// bad Creator control - The creator can mint more tokens (mint authority active)
|
|
18
|
-
// bad Concentration - The largest individual wallet holds 50% and the top 10 hold 60% of the supply
|
|
19
|
-
// bad Market and exit - Very low liquidity ($5,000). Very new market (3 days)
|
|
20
|
-
// ok Dependency - Does not depend on an issuer or a bridge
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## What it detects
|
|
24
|
-
|
|
25
|
-
| Check | How |
|
|
26
|
-
|---|---|
|
|
27
|
-
| **Creator control** | Mint/freeze authority still active, and whether it sits in a person's wallet or a program (bridge, protocol, staking pool). A key off the ed25519 curve is always a program (PDA), even when the System Program owns its account. Risky Token-2022 extensions: permanent delegate, transfer hook, transfer fee, non-transferable, default-frozen accounts. |
|
|
28
|
-
| **Real concentration** | Largest accounts are traced to their owners. Pools and programs are separated from people, so `w1`/`w10` measure *people*, not AMM vaults. |
|
|
29
|
-
| **Inflated liquidity** | DexScreener values both sides of a pool at the pool's price. A pool with 4B of a token against 2 USDC shows "$4,000M liquidity". The engine recomputes it from the real side (USDC/USDT/SOL). It only flags extreme gaps (real side under 5%), because concentrated-liquidity pools are lopsided by nature. |
|
|
30
|
-
| **Unlocked LP** | % of LP locked/burned via RugCheck. It only counts for markets under 90 days old, so veteran tokens don't get false positives, and never while a token is on Pump.fun's bonding curve, where there is no LP to pull. |
|
|
31
|
-
| **Serial creators** | For markets under 7 days: how many other tokens the creator launched in the last 24 h (RugCheck). 2 or more is a warning, 5 or more a red flag. `recentLaunches(client, wallet)` counts the same on chain for the wallet that *paid* a launch, which catches farms that name a fresh creator each time. |
|
|
32
|
-
| **Impostors** | Tokens using the symbol or name of a famous token (USDC, JUP, BONK, WIF, BTC, ETH...) with a different address. |
|
|
33
|
-
| **
|
|
34
|
-
| **
|
|
35
|
-
| **
|
|
36
|
-
| **
|
|
37
|
-
| **
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
npx solana-risk <
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- [
|
|
130
|
-
- [
|
|
131
|
-
- [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
# solana-risk-engine
|
|
2
|
+
|
|
3
|
+
Explainable risk analysis for Solana tokens and wallets. **Zero dependencies**, runs in **Node 18+ and the browser**, works with free public RPCs out of the box.
|
|
4
|
+
|
|
5
|
+
It doesn't stop at a single score. Every verdict comes with the **signals** behind it, as stable codes plus the numbers involved. Apps can show them in any language (Spanish and English are included).
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createScanner, explainLight, lightLabel, levelLabel } from "solana-risk-engine";
|
|
9
|
+
|
|
10
|
+
const scanner = createScanner(); // free PublicNode + Vibe Station, or { rpcUrl }
|
|
11
|
+
const r = await scanner.analyzeToken("7xKX...gAsU");
|
|
12
|
+
|
|
13
|
+
console.log(r.lvl); // "HIGH"
|
|
14
|
+
console.log(r.type); // "CREATOR"
|
|
15
|
+
for (const l of r.lights) // the four traffic lights
|
|
16
|
+
console.log(l.s, lightLabel(l.k, "en"), "-", explainLight(l, "en"));
|
|
17
|
+
// bad Creator control - The creator can mint more tokens (mint authority active)
|
|
18
|
+
// bad Concentration - The largest individual wallet holds 50% and the top 10 hold 60% of the supply
|
|
19
|
+
// bad Market and exit - Very low liquidity ($5,000). Very new market (3 days)
|
|
20
|
+
// ok Dependency - Does not depend on an issuer or a bridge
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What it detects
|
|
24
|
+
|
|
25
|
+
| Check | How |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **Creator control** | Mint/freeze authority still active, and whether it sits in a person's wallet or a program (bridge, protocol, staking pool). A key off the ed25519 curve is always a program (PDA), even when the System Program owns its account. Risky Token-2022 extensions: permanent delegate, transfer hook, transfer fee, non-transferable, default-frozen accounts. |
|
|
28
|
+
| **Real concentration** | Largest accounts are traced to their owners. Pools and programs are separated from people, so `w1`/`w10` measure *people*, not AMM vaults. |
|
|
29
|
+
| **Inflated liquidity** | DexScreener values both sides of a pool at the pool's price. A pool with 4B of a token against 2 USDC shows "$4,000M liquidity". The engine recomputes it from the real side (USDC/USDT/SOL). It only flags extreme gaps (real side under 5%), because concentrated-liquidity pools are lopsided by nature. |
|
|
30
|
+
| **Unlocked LP** | % of LP locked/burned via RugCheck. It only counts for markets under 90 days old, so veteran tokens don't get false positives, and never while a token is on Pump.fun's bonding curve, where there is no LP to pull. |
|
|
31
|
+
| **Serial creators** | For markets under 7 days: how many other tokens the creator launched in the last 24 h (RugCheck). 2 or more is a warning, 5 or more a red flag. `recentLaunches(client, wallet)` counts the same on chain for the wallet that *paid* a launch, which catches farms that name a fresh creator each time. |
|
|
32
|
+
| **Impostors** | Tokens using the symbol or name of a famous token (USDC, JUP, BONK, WIF, BTC, ETH...) with a different address. |
|
|
33
|
+
| **Real brands** | About 320 real companies with no token on Solana (Spotify, Nike, Zara, Ferrari...). A token called just that is a red flag and **HIGH**; the brand as one word of a longer name is a warning. Everyday words that are also brands (Shell, Target, Puma) are left out, and markets at least 30 days old with $100k+ liquidity are not checked. |
|
|
34
|
+
| **Known issuers and official tokens** | Tokenized shares from xStocks (recognised by their control keys) and official tokens from exchanges and Socios.com (bnSOL, bbSOL, cbBTC, club fan tokens..., by mint address) are shown as dependency on the issuer. Their Solana pools are often tiny because they trade on the exchange: that is a warning, not a red flag. |
|
|
35
|
+
| **Token type** | `STABLE`, `TOKENIZED`, `BRIDGED`, `CREATOR`, `OPEN`. Each type is judged by the rules that fit it: a bridged token normally has a program as mint authority, a memecoin does not. |
|
|
36
|
+
| **Bots and exchanges** | Wallets that made 200 transactions in under 2 days are marked `active` and are not counted as whales. |
|
|
37
|
+
| **Funder clusters** | The fee payer of each holder's *first* transaction reveals who created it. Holders sharing a funder are grouped, since that is often one team splitting the supply. Exchange hot wallets are excluded as sources. |
|
|
38
|
+
| **Creator history** | Other tokens by the same creator and their risk, rugged flag, and insider networks with today's on-chain balances (RugCheck). |
|
|
39
|
+
| **Wallet PnL** | FIFO realized and unrealized PnL from the last ~300 swaps, valued with historical SOL prices (Helius + CoinGecko). |
|
|
40
|
+
| **Confluence** | Tokens held in common by a group of wallets, each one risk-assessed. Also the reverse: people who are top holders of several tokens. |
|
|
41
|
+
| **Change tracking** | Snapshots and diffs (new / bought more / sold part / sold all) per wallet and aggregated across many wallets. |
|
|
42
|
+
|
|
43
|
+
### How the level is decided
|
|
44
|
+
|
|
45
|
+
- Two red lights, or one red and one yellow, give **HIGH**.
|
|
46
|
+
- One red, or two yellows, give **MEDIUM**.
|
|
47
|
+
- Otherwise the level is **LOW**, with two exceptions that raise it to **MEDIUM**:
|
|
48
|
+
- **A check could not run.** Lights that could not be checked are marked `na`, and they never count as "fine". `dataOnly(r)` tells you when this is the only reason.
|
|
49
|
+
- **The market is under 7 days old.** It has no track record yet.
|
|
50
|
+
- An impostor of a famous token, or a token named just like a real company, is always **HIGH**.
|
|
51
|
+
|
|
52
|
+
A single yellow on an established token stays LOW, because those warnings are normal for its type (a staking pool's program-controlled mint, for example). This was measured on 62 real tokens: the 7-day rule moved 9 brand-new tokens from low to medium and no established one.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm install solana-risk-engine
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### `createScanner(options)`
|
|
63
|
+
|
|
64
|
+
| Option | |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `rpcUrl` | Your own RPC (Helius, Triton, QuickNode...). Without it, PublicNode is used, with Solana Vibe Station (rate-limited queue) for the methods PublicNode blocks. |
|
|
67
|
+
| `heliusApiKey` | For PnL. Taken from `rpcUrl` automatically when that is a Helius URL. It is never sent anywhere else. |
|
|
68
|
+
| `signal` | An `AbortSignal`. Aborting rejects pending work with `ScanError` code `CANCELLED`. |
|
|
69
|
+
| `fetch` | Custom fetch (tests, proxies). |
|
|
70
|
+
|
|
71
|
+
Methods (all accept `{ onProgress(step, fraction) }`):
|
|
72
|
+
|
|
73
|
+
| Method | Returns |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `analyzeToken(mint, { skipCreator })` | `{ lvl, sev, type, lights, reasons, impostor, pair, creator, ... }` |
|
|
76
|
+
| `analyzeWallet(wallet, { minUsd })` | `{ sol, balances, total, hidden, results: [token results] }` |
|
|
77
|
+
| `analyzeAddress(addr)` | `{ kind: "token" \| "wallet", ... }`, detected automatically |
|
|
78
|
+
| `analyzeHolders(mint)` | `{ rows, clusters, groups, hotGroups, creator, mode, ageDays, signals, ... }`. Young tokens (under 14 days) also trace busy wallets; a source over 100 tx/h counts as an exchange; a group over 10% (young) or 15% (mature) of the supply gives a `HOLD_GROUP` signal. With `{ base: true }` it also reads [SolFlag's wallet base](https://solflag.com/datos/base.json) (exchanges, launch farms, distributors and serial creators its collector has seen): known exchanges are recognised without measuring them, and rows and group sources get `known` / `knownSources`. Best with your own RPC. |
|
|
79
|
+
| `loadWalletBase([url])` | SolFlag's wallet base `{ wallets: { addr: { t, ph, n, roles } } }`, or `null` if it cannot be read. Kept 10 minutes. `knownType(base, addr)` gives a wallet's type. |
|
|
80
|
+
| `estimatePnl(wallets)` | `{ pnl: { [wallet]: { realized, unreal, total, ret, st, open } } }`. Needs Helius. |
|
|
81
|
+
| `takeSnapshots(wallets)` | Today's holdings. Store them yourself and pass them to `diffSnapshots(prev, next)`. |
|
|
82
|
+
| `confluence(wallets, { pct, withPnl, pnlFilter })` | Tokens held in common, with risk. |
|
|
83
|
+
| `commonHolders(mints, { min })` | Wallets that are top holders of several tokens. |
|
|
84
|
+
|
|
85
|
+
Every lower-level function is exported too (`buildLights`, `levelFrom`, `realLiq`, `clusterByFunder`, `chgDiff`, `pnlFromTrades`...). Most are pure functions that are easy to reuse and test.
|
|
86
|
+
|
|
87
|
+
### Results
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
{
|
|
91
|
+
lvl: "HIGH" | "MEDIUM" | "LOW" | "SPAM" | "NO_DATA" | "NO_MARKET",
|
|
92
|
+
type: "STABLE" | "TOKENIZED" | "BRIDGED" | "CREATOR" | "OPEN" | null,
|
|
93
|
+
lights: [ // null for tokens without a market
|
|
94
|
+
{ k: "control" | "concentration" | "market" | "dependency",
|
|
95
|
+
s: "ok" | "na" | "warn" | "bad",
|
|
96
|
+
items: [{ code: "MKT_LIQ_INFLATED", s: "bad", data: { reported: 4e9, real: 2 } }] }
|
|
97
|
+
],
|
|
98
|
+
reasons: [ ...every non-ok signal ],
|
|
99
|
+
sev: 6 // for sorting within a level
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Texts and languages
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { describe, explainLight, levelLabel, typeInfo, describeStep, describeError, addLanguage } from "solana-risk-engine";
|
|
107
|
+
|
|
108
|
+
describe({ code: "MKT_LIQ_VERY_LOW", data: { liq: 3000 } }, "es"); // "Liquidez muy baja (3.000 $)"
|
|
109
|
+
describeError(err, "en", { ownRpc: true }); // friendly message for any ScanError
|
|
110
|
+
addLanguage("pt", { signals: { ... }, levels: { ... } }); // same shape as src/i18n/en.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Errors
|
|
114
|
+
|
|
115
|
+
The engine throws `ScanError` with a stable `code`: `CANCELLED`, `HTTP`, `NETWORK`, `RPC`, `DEX_UNREACHABLE`, `HELIUS_REQUIRED`, `SOL_PRICE`, `INVALID_ADDRESS`, `NOT_A_TOKEN`, `IS_NFT`, `NO_HOLDERS`, `TOO_FEW_WALLETS`, `TOO_FEW_TOKENS`.
|
|
116
|
+
|
|
117
|
+
Optional extras (RugCheck) never throw. If they fail, the analysis goes on without them.
|
|
118
|
+
|
|
119
|
+
## CLI
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npx solana-risk <token-or-wallet> --lang en
|
|
123
|
+
npx solana-risk holders <mint> --rpc "https://mainnet.helius-rpc.com/?api-key=..."
|
|
124
|
+
npx solana-risk <mint> --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Data sources
|
|
128
|
+
|
|
129
|
+
- Solana RPC: [PublicNode](https://solana-rpc.publicnode.com), [Solana Vibe Station](https://solanavibestation.com), or yours
|
|
130
|
+
- [DexScreener](https://dexscreener.com): pairs, price, liquidity, volume
|
|
131
|
+
- [RugCheck](https://rugcheck.xyz): LP lock, creator, insider networks (optional)
|
|
132
|
+
- [Helius](https://helius.dev): parsed swap history for PnL (optional, your key)
|
|
133
|
+
- [CoinGecko](https://coingecko.com): SOL price history for PnL
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm test # node --test, no network needed: the network is mocked
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Disclaimer
|
|
142
|
+
|
|
143
|
+
This is a heuristic tool, not financial advice. A "low risk" result means the engine found none of the signals it looks for. It does not mean the token is safe.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Explainable risk analysis for Solana tokens and wallets: creator control, real holder concentration, inflated liquidity, impostor tokens, funder clusters and PnL. Zero dependencies, runs in Node and the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|