solana-risk-engine 0.4.10 → 0.4.12
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 +1 -1
- package/package.json +1 -1
- package/src/constants.js +3 -0
- package/src/index.js +1 -1
- package/src/launch.js +24 -0
- package/src/risk.js +10 -3
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ for (const l of r.lights) // the four traffic lights
|
|
|
28
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
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
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. |
|
|
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. When RugCheck has no report yet (it often answers 400 for tokens a few minutes old) the analysis falls back to `chainLaunches(client, mint)`: that count for the first signer of the token's creation transaction. |
|
|
32
32
|
| **Impostors** | Tokens using the symbol or name of a famous token (USDC, JUP, BONK, WIF, BTC, ETH...) with a different address. |
|
|
33
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
34
|
| **Known issuers and official tokens** | Stablecoins from Paxos (PYUSD, USDG), Circle (USDC, EURC) and Tether (USDT), tokenized shares from xStocks (all recognised by their control keys, taken from mints the issuer lists on its own pages) 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 large holders (treasury, custodians, exchanges) are shown but do not count as concentration, a 0% transfer fee is not a warning, and a tiny Solana pool is a warning, not a red flag. A short **trusted** list (Circle, Paxos, Tether, xStocks; `trustedIssuer()`) goes further: every light green, holders not checked. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-risk-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
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",
|
package/src/constants.js
CHANGED
|
@@ -19,6 +19,9 @@ export const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
|
19
19
|
export const PUMP_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
|
20
20
|
// dexId of a token still on Pump.fun's bonding curve: DexScreener says "pumpfun", GeckoTerminal "pump-fun"
|
|
21
21
|
export const PUMP_CURVE_DEXES = ["pumpfun", "pump-fun"];
|
|
22
|
+
// DexScreener labels of concentrated-liquidity pools (Meteora DLMM, Raydium CLMM, Orca Whirlpool): liquidity
|
|
23
|
+
// lives in positions, there is no LP token to lock or burn, so RugCheck's "locked %" means nothing there
|
|
24
|
+
export const CL_POOL_LABELS = ["DLMM", "CLMM", "wp"];
|
|
22
25
|
|
|
23
26
|
export const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
24
27
|
export const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
|
package/src/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf
|
|
|
49
49
|
export { getMarkets, realLiq, impostorOf, INFLATED_RATIO, geckoLiquidity, geckoPair, brandOf, copiesOf, fakeLiquidity, FAKE_LIQ_RATIO } from "./markets.js";
|
|
50
50
|
export { rugcheckSummary, rugcheckReport, rugcheckGraph, netBalances, creatorLaunches, CREATOR_DEAD_MCAP } from "./rugcheck.js";
|
|
51
51
|
export { score, issuerOf, trustedIssuer, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
|
|
52
|
-
export { recentLaunches } from "./launch.js";
|
|
52
|
+
export { recentLaunches, chainLaunches } from "./launch.js";
|
|
53
53
|
export { analyzeToken, analyzeWallet, analyzeAddress, sortResults } from "./wallet.js";
|
|
54
54
|
export { holderBase, markBusy, onlyReceives, PASSIVE_SAMPLE, findFunder, findFunders, clusterByFunder, holderRows, creatorInfo, analyzeHolders,
|
|
55
55
|
perHour, sourceRates, groupList, EXCHANGE_PER_HOUR, YOUNG_DAYS, HOLD_GROUP_YOUNG, HOLD_GROUP_MATURE } from "./holders.js";
|
package/src/launch.js
CHANGED
|
@@ -42,3 +42,27 @@ export async function recentLaunches(client, wallet, opts) {
|
|
|
42
42
|
});
|
|
43
43
|
return { n: mints.length, mints: mints, scanned: day.length, more: sigs.length >= limit && day.length === sigs.length };
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The creator's recent launches read on chain, for when RugCheck does not know the token yet (it answers
|
|
48
|
+
* 400 "unable to generate report" for most tokens a few minutes old, 2026-11-02) or does not answer.
|
|
49
|
+
* The launcher is the first signer of the token's creation transaction: the wallet that paid, as in
|
|
50
|
+
* recentLaunches (farms name a fresh creator each time but pay from one wallet). Only new tokens: past
|
|
51
|
+
* 1000 transactions the creation is too far back to find cheaply.
|
|
52
|
+
* Same shape as creatorLaunches, without the history of older launches (that needs market caps):
|
|
53
|
+
* @returns {creator, day, total, old: 0, dead: 0, best: null, chain: true, more} or null
|
|
54
|
+
*/
|
|
55
|
+
export async function chainLaunches(client, mint, opts) {
|
|
56
|
+
opts = opts || {};
|
|
57
|
+
const sigs = (await client.rpc("getSignaturesForAddress", [mint, { limit: 1000 }])) || [];
|
|
58
|
+
if (!sigs.length || sigs.length >= 1000) return null;
|
|
59
|
+
const p = { encoding: "jsonParsed", maxSupportedTransactionVersion: 1, commitment: "confirmed" };
|
|
60
|
+
const tx = await client.rpc("getTransaction", [sigs[sigs.length - 1].signature, p]);
|
|
61
|
+
const keys = tx && tx.transaction && tx.transaction.message && tx.transaction.message.accountKeys;
|
|
62
|
+
const first = keys && keys.filter(function (k) { return k.signer; })[0];
|
|
63
|
+
const wallet = first && (first.pubkey || first);
|
|
64
|
+
if (!wallet || wallet === mint) return null;
|
|
65
|
+
const r = await recentLaunches(client, wallet, { now: opts.now, limit: opts.limit });
|
|
66
|
+
const day = r.mints.filter(function (m) { return m !== mint; }).length;
|
|
67
|
+
return { creator: wallet, day: day, total: day, old: 0, dead: 0, best: null, chain: true, more: r.more };
|
|
68
|
+
}
|
package/src/risk.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { EXT_POINTS, RANKS, PUMP_CURVE_DEXES, ISSUERS, OFFICIAL_TOKENS, FAMOUS } from "./constants.js";
|
|
1
|
+
import { EXT_POINTS, RANKS, PUMP_CURVE_DEXES, CL_POOL_LABELS, ISSUERS, OFFICIAL_TOKENS, FAMOUS } from "./constants.js";
|
|
2
2
|
import { isCancel } from "./errors.js";
|
|
3
3
|
import { short, sleep } from "./util.js";
|
|
4
4
|
import { getMint, getAuthKinds, getConc } from "./tokens.js";
|
|
5
5
|
import { impostorOf, brandOf, copiesOf, fakeLiquidity } from "./markets.js";
|
|
6
6
|
import { rugcheckSummary, creatorLaunches } from "./rugcheck.js";
|
|
7
|
+
import { chainLaunches } from "./launch.js";
|
|
7
8
|
|
|
8
9
|
// Every finding is a "signal": {code, s, data}. `s` is ok | na | warn | bad, `data` holds the numbers
|
|
9
10
|
// behind it. Turning signals into sentences is i18n's job (describe / explainLight), never the engine's.
|
|
@@ -183,7 +184,10 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
|
|
|
183
184
|
// On Pump.fun's bonding curve there is no LP at all: the curve program holds the liquidity and nobody
|
|
184
185
|
// can pull it (RugCheck reports 0% locked, which flagged 49 of 75 curve tokens in a 100-token sample)
|
|
185
186
|
const onCurve = PUMP_CURVE_DEXES.indexOf(pair.dexId) >= 0;
|
|
186
|
-
|
|
187
|
+
// Same in concentrated-liquidity pools: no LP token, RugCheck reports ~0% locked (STONK on Meteora DLMM,
|
|
188
|
+
// $2.4M liquidity in 27 pairs, showed a red flag, 1/11)
|
|
189
|
+
const clPool = (pair.labels || []).some(function (l) { return CL_POOL_LABELS.indexOf(l) >= 0; });
|
|
190
|
+
if (rc && !soft && young && !onCurve && !clPool && liq > 0 && typeof rc.lpLockedPct === "number" && isFinite(rc.lpLockedPct)) {
|
|
187
191
|
if (rc.lpLockedPct < 20) it.push(sig("MKT_LP_UNLOCKED", "bad", { pct: rc.lpLockedPct }));
|
|
188
192
|
else if (rc.lpLockedPct < 60) it.push(sig("MKT_LP_PARTLY_LOCKED", "warn", { pct: rc.lpLockedPct }));
|
|
189
193
|
}
|
|
@@ -261,7 +265,10 @@ export async function assess(client, mint, pair, opts) {
|
|
|
261
265
|
const rcPromise = pair ? rugcheckSummary(client, mint) : Promise.resolve(null); // in parallel with the RPC calls below
|
|
262
266
|
// Serial launching only matters while a token is new; RugCheck's full report is small (~10 KB) then
|
|
263
267
|
const fresh = pair && !opts.skipCreator && (!pair.pairCreatedAt || Date.now() - pair.pairCreatedAt < 7 * DAY);
|
|
264
|
-
|
|
268
|
+
// when RugCheck does not know the creator (new tokens often get a 400) the launches are counted on chain
|
|
269
|
+
const launchPromise = fresh ? creatorLaunches(client, mint).then(function (r) {
|
|
270
|
+
return r || chainLaunches(client, mint).catch(function (e) { if (isCancel(e)) throw e; return null; });
|
|
271
|
+
}) : Promise.resolve(null);
|
|
265
272
|
// Copies of the same name and ticker: one search per token, so only when asked (a single token, not a wallet)
|
|
266
273
|
const copiesPromise = pair && opts.copies && !OFFICIAL_TOKENS[mint] ? copiesOf(client, mint, pair).catch(function (e) { if (isCancel(e)) throw e; return null; }) : Promise.resolve(null);
|
|
267
274
|
let info = null, conc = null;
|