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 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.10",
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
- if (rc && !soft && young && !onCurve && liq > 0 && typeof rc.lpLockedPct === "number" && isFinite(rc.lpLockedPct)) {
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
- const launchPromise = fresh ? creatorLaunches(client, mint) : Promise.resolve(null);
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;