solana-risk-engine 0.4.9 → 0.4.11

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
@@ -31,7 +31,7 @@ for (const l of r.lights) // the four traffic lights
31
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
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
- | **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. |
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. |
35
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
36
  | **Bots and exchanges** | Wallets that made 200 transactions in under 2 days are marked `active` and are not counted as whales. |
37
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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-risk-engine",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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";
@@ -157,7 +160,9 @@ export const OFFICIAL_TOKENS = {
157
160
  // (anyone can hand the mint authority to someone else's wallet) is not enough. Their control is the point of
158
161
  // the product: it is shown as dependency on the issuer, not as a creator who can rug
159
162
  export const ISSUERS = [
160
- { name: "xStocks (Backed)", keys: [
163
+ // trusted: a short list SolFlag vouches for (1/11/2026: Circle, Paxos, Tether, xStocks): every light green,
164
+ // holders not checked. Other verified issuers and official tokens keep the dependency in yellow
165
+ { name: "xStocks (Backed)", trusted: true, keys: [
161
166
  "7pt9tkctJPK7PPNQJ77GKg8ZffSF6QxoMiCFYHxrtaCj", // mint
162
167
  "JDq14BWvqCRFNu1krb12bcRpbGtJZ1FLEakMw6FdxJNs", // freeze and pause (program)
163
168
  "5aMNNLQJwAEeoemTEMkv5NVjqKwvvefRYCQ5Z67HFvEq", // permanent delegate, transfer hook, confidential transfers
@@ -165,14 +170,14 @@ export const ISSUERS = [
165
170
  ] },
166
171
  // Each mint below is listed by its issuer (URL); the keys are the ones those mints carry on-chain (1/11/2026).
167
172
  // An issuer is only added with an official page for its mints: FDUSD was left out for lack of one.
168
- { name: "Paxos", keys: [
173
+ { name: "Paxos", trusted: true, keys: [
169
174
  // PYUSD 2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo https://docs.paxos.com/guides/stablecoin/pyusd/mainnet
170
175
  // USDG 2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH https://docs.paxos.com/guides/stablecoin/usdg/mainnet
171
176
  "AxJkHRY3puoofyj7Pn512M1SGBgWroXMkEdNPQypkzbu", // PYUSD mint
172
177
  "2e99QgqSDtiJSi5omGRVeZJuqNFRZdpy2YgG3mBd1AB4", // USDG mint
173
178
  "2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk" // freeze, permanent delegate, hook, fee (both)
174
179
  ] },
175
- { name: "Circle", keys: [
180
+ { name: "Circle", trusted: true, keys: [
176
181
  // USDC EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, EURC HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr
177
182
  // https://developers.circle.com/stablecoins/usdc-contract-addresses , .../eurc-contract-addresses
178
183
  "BJE5MMbqXjVwjAF7oxwPYXnTXDyspzZyt4vwenNw5ruG", // USDC mint
@@ -180,7 +185,7 @@ export const ISSUERS = [
180
185
  "Hy9168u7b2Toujh8SJKtKK8DWbyaBbXPzxm337R4o4XY", // EURC mint
181
186
  "6FF2CPcL6fat16QVJH6W27DeBcX7TSqbwY3a5wtyVJLC" // EURC freeze
182
187
  ] },
183
- { name: "Tether", keys: [
188
+ { name: "Tether", trusted: true, keys: [
184
189
  // USDT Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB (FAMOUS.USDT)
185
190
  "Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi" // USDT mint and freeze
186
191
  ] }
package/src/i18n/en.js CHANGED
@@ -49,6 +49,7 @@ export default {
49
49
  CON_UNAVAILABLE: () => "Could not check holder concentration (the service did not answer; try again in a while)",
50
50
  CON_ALL_POOLS: (d) => "Almost all the supply is in pools or programs" + prog(d),
51
51
  CON_OK: (d) => "No worrying concentration (largest individual wallet: " + pct(d.w1) + ")" + prog(d),
52
+ CON_TRUSTED: (d) => "Not counted: in a token from " + d.issuer + " the large holders are its treasury, custodians, exchanges and protocols",
52
53
  CON_ISSUER: (d) => "The largest wallet holds " + pct(d.w1) + " and the top 10 hold " + pct(d.w10) + ": with a verified issuer these are usually its treasury, custodians, exchanges or protocols, so it does not count as a risk" + prog(d),
53
54
  CON_HIGH: (d) => "The largest individual wallet holds " + pct(d.w1) + " and the top 10 hold " + pct(d.w10) + " of the supply" + prog(d),
54
55
  CON_UNSPLIT: (d) => "Could not separate pools from people. The largest account holds " + pct(d.top1) + " of the supply",
@@ -66,6 +67,7 @@ export default {
66
67
  DEP_PROGRAM_AUTHORITY: () => "A program controls the mint or freeze (bridge or protocol): you have to trust its code",
67
68
  DEP_PERMANENT_DELEGATE: () => "The issuer can move or burn your tokens (permanent delegate): you trust the issuer",
68
69
  DEP_STABLE_ISSUER: () => "Its value depends on whoever issues and backs it",
70
+ DEP_TRUSTED_ISSUER: (d) => "Issued by " + d.issuer + ", one of the few regulated issuers SolFlag trusts. Like any issuer, it can freeze or claw back tokens under a legal order",
69
71
  DEP_KNOWN_ISSUER: (d) => "Issued by " + d.issuer + ": it can mint, freeze or move tokens, like any regulated issuer. You trust the issuer",
70
72
  DEP_NONE: () => "Does not depend on an issuer or a bridge"
71
73
  },
package/src/i18n/es.js CHANGED
@@ -52,6 +52,7 @@ export default {
52
52
  CON_UNAVAILABLE: () => "No se pudo comprobar la concentración de holders (el servicio no respondió; prueba otra vez en un rato)",
53
53
  CON_ALL_POOLS: (d) => "Casi todo el supply está en pools o programas" + prog(d),
54
54
  CON_OK: (d) => "Sin concentración preocupante (mayor wallet individual: " + pct(d.w1) + ")" + prog(d),
55
+ CON_TRUSTED: (d) => "No cuenta: en un token de " + d.issuer + " los grandes holders son su tesorería, custodios, exchanges y protocolos",
55
56
  CON_ISSUER: (d) => "La mayor wallet tiene el " + pct(d.w1) + " y las 10 mayores el " + pct(d.w10) + ": con un emisor verificado suelen ser su tesorería, custodios, exchanges o protocolos, no cuenta como riesgo" + prog(d),
56
57
  CON_HIGH: (d) => "La mayor wallet individual tiene el " + pct(d.w1) + " y las 10 mayores el " + pct(d.w10) + " del supply" + prog(d),
57
58
  CON_UNSPLIT: (d) => "No se pudo separar pools de personas. La mayor cuenta tiene el " + pct(d.top1) + " del supply",
@@ -69,6 +70,7 @@ export default {
69
70
  DEP_PROGRAM_AUTHORITY: () => "Un programa controla el mint o el freeze (puente o protocolo): hay que confiar en su código",
70
71
  DEP_PERMANENT_DELEGATE: () => "El emisor puede mover o quemar tus tokens (delegado permanente): confías en el emisor",
71
72
  DEP_STABLE_ISSUER: () => "Su valor depende de quien la emite y la respalda",
73
+ DEP_TRUSTED_ISSUER: (d) => "Lo emite " + d.issuer + ", un emisor regulado de los pocos que SolFlag considera de confianza. Como cualquier emisor, puede congelar o retirar tokens por orden legal",
72
74
  DEP_KNOWN_ISSUER: (d) => "Lo emite " + d.issuer + ": puede crear, congelar o mover tokens, como cualquier emisor regulado. Confías en el emisor",
73
75
  DEP_NONE: () => "No depende de un emisor ni de un puente"
74
76
  },
package/src/index.js CHANGED
@@ -48,7 +48,7 @@ export { isOnCurve, onCurve, b58bytes } from "./curve.js";
48
48
  export { getTokens, getMint, getConc, getAuthKinds, isMint, accountKinds, kindOf } from "./tokens.js";
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
- export { score, issuerOf, tokenType, buildLights, levelFrom, dataOnly, reasonsFrom, assess, SERIAL_WARN, SERIAL_BAD, HISTORY_MIN, HISTORY_WARN, HISTORY_BAD_MIN, HISTORY_BAD } from "./risk.js";
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
52
  export { recentLaunches } 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,
package/src/risk.js CHANGED
@@ -1,4 +1,4 @@
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";
@@ -53,6 +53,9 @@ export function tokenType(info, pair, now) {
53
53
  // it needs a stable price, real liquidity and some time trading (otherwise a scammer only has to
54
54
  // name the token "USDX" and keep the price near $1 for a while)
55
55
  if (/USD|DOLLAR/i.test(sym) && price > 0.97 && price < 1.03 && liq >= 200000 && ageDays >= 30) return "STABLE";
56
+ // a verified issuer's dollar or euro token is a stablecoin whatever pair DexScreener found (USDC showed
57
+ // "bridged" because its pair as base token is recent)
58
+ if (/USD|DOLLAR|EUR/i.test(sym) && issuerOf(info, pair && pair.baseToken && pair.baseToken.address)) return "STABLE";
56
59
  if (info.ext.indexOf("permanentDelegate") >= 0) return "TOKENIZED";
57
60
  const walletAuth = (info.mintAuth && ak[info.mintAuth] !== "program") || (info.freezeAuth && ak[info.freezeAuth] !== "program");
58
61
  if (walletAuth) return "CREATOR";
@@ -72,6 +75,12 @@ export function issuerOf(info, mint) {
72
75
  return found ? found.name : null;
73
76
  }
74
77
 
78
+ // Name of the issuer when it is on SolFlag's short trusted list, or null
79
+ export function trustedIssuer(info, mint) {
80
+ const name = issuerOf(info, mint);
81
+ return name && ISSUERS.some(function (i) { return i.trusted && i.name === name; }) ? name : null;
82
+ }
83
+
75
84
  // Extensions that only matter as they are set: a hook with no program and nobody able to add one does
76
85
  // nothing, and new accounts are only born frozen when the default state says "frozen". Mint info without
77
86
  // those details (older callers) keeps the old behaviour: the extension alone is enough
@@ -102,6 +111,7 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
102
111
  now = now || Date.now();
103
112
  const ak = (info && info.authKind) || {};
104
113
  const issuer = issuerOf(info, pair && pair.baseToken && pair.baseToken.address);
114
+ const trusted = issuer && trustedIssuer(info, pair && pair.baseToken && pair.baseToken.address);
105
115
  let it;
106
116
 
107
117
  // 1) creator control
@@ -133,7 +143,8 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
133
143
  // 2) concentration in people (pools and programs do not count)
134
144
  let concentration;
135
145
  const soft = (type === "STABLE" || type === "TOKENIZED" || type === "BRIDGED");
136
- if (conc && conc.skip) concentration = mkLight("concentration", [sig("CON_SKIPPED", "na")]);
146
+ if (trusted) concentration = mkLight("concentration", [sig("CON_TRUSTED", "ok", { issuer: issuer })]);
147
+ else if (conc && conc.skip) concentration = mkLight("concentration", [sig("CON_SKIPPED", "na")]);
137
148
  else if (!conc) concentration = mkLight("concentration", [sig("CON_UNAVAILABLE", "na")]);
138
149
  else if (conc.ok) {
139
150
  let st = "ok";
@@ -172,7 +183,10 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
172
183
  // On Pump.fun's bonding curve there is no LP at all: the curve program holds the liquidity and nobody
173
184
  // can pull it (RugCheck reports 0% locked, which flagged 49 of 75 curve tokens in a 100-token sample)
174
185
  const onCurve = PUMP_CURVE_DEXES.indexOf(pair.dexId) >= 0;
175
- if (rc && !soft && young && !onCurve && liq > 0 && typeof rc.lpLockedPct === "number" && isFinite(rc.lpLockedPct)) {
186
+ // Same in concentrated-liquidity pools: no LP token, RugCheck reports ~0% locked (STONK on Meteora DLMM,
187
+ // $2.4M liquidity in 27 pairs, showed a red flag, 1/11)
188
+ const clPool = (pair.labels || []).some(function (l) { return CL_POOL_LABELS.indexOf(l) >= 0; });
189
+ if (rc && !soft && young && !onCurve && !clPool && liq > 0 && typeof rc.lpLockedPct === "number" && isFinite(rc.lpLockedPct)) {
176
190
  if (rc.lpLockedPct < 20) it.push(sig("MKT_LP_UNLOCKED", "bad", { pct: rc.lpLockedPct }));
177
191
  else if (rc.lpLockedPct < 60) it.push(sig("MKT_LP_PARTLY_LOCKED", "warn", { pct: rc.lpLockedPct }));
178
192
  }
@@ -186,13 +200,14 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
186
200
 
187
201
  // 4) dependency on an issuer or a bridge
188
202
  it = [];
189
- if (issuer) it.push(sig("DEP_KNOWN_ISSUER", "warn", { issuer: issuer }));
203
+ if (trusted) it.push(sig("DEP_TRUSTED_ISSUER", "ok", { issuer: issuer }));
204
+ else if (issuer) it.push(sig("DEP_KNOWN_ISSUER", "warn", { issuer: issuer }));
190
205
  else if (info) {
191
206
  if ((info.mintAuth && ak[info.mintAuth] === "program") || (info.freezeAuth && ak[info.freezeAuth] === "program"))
192
207
  it.push(sig("DEP_PROGRAM_AUTHORITY", "warn"));
193
208
  if (info.ext.indexOf("permanentDelegate") >= 0) it.push(sig("DEP_PERMANENT_DELEGATE", "warn"));
194
209
  }
195
- if (type === "STABLE") it.push(sig("DEP_STABLE_ISSUER", "warn"));
210
+ if (type === "STABLE" && !trusted) it.push(sig("DEP_STABLE_ISSUER", "warn"));
196
211
  const dependency = mkLight("dependency", it, sig("DEP_NONE", "ok"));
197
212
 
198
213
  return [control, concentration, market, dependency];
@@ -259,7 +274,9 @@ export async function assess(client, mint, pair, opts) {
259
274
  if (info && pair && (info.mintAuth || info.freezeAuth)) {
260
275
  try { info.authKind = await getAuthKinds(client, info); } catch (e) { if (isCancel(e)) throw e; }
261
276
  }
262
- if (info && pair && opts.skipConcentration) conc = { skip: true };
277
+ // a trusted issuer's holders are not checked (its treasury and exchanges; from the browser the public RPC
278
+ // often cannot list USDC's holders, which left it "unchecked" and medium)
279
+ if (info && pair && (opts.skipConcentration || trustedIssuer(info, mint))) conc = { skip: true };
263
280
  else if (info && pair) {
264
281
  // In tokens with millions of holders (BONK) getTokenLargestAccounts sometimes times out: a second try usually works
265
282
  for (let tryC = 0; tryC < 2 && !conc; tryC++) {