solana-risk-engine 0.4.9 → 0.4.10

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.10",
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
@@ -157,7 +157,9 @@ export const OFFICIAL_TOKENS = {
157
157
  // (anyone can hand the mint authority to someone else's wallet) is not enough. Their control is the point of
158
158
  // the product: it is shown as dependency on the issuer, not as a creator who can rug
159
159
  export const ISSUERS = [
160
- { name: "xStocks (Backed)", keys: [
160
+ // trusted: a short list SolFlag vouches for (1/11/2026: Circle, Paxos, Tether, xStocks): every light green,
161
+ // holders not checked. Other verified issuers and official tokens keep the dependency in yellow
162
+ { name: "xStocks (Backed)", trusted: true, keys: [
161
163
  "7pt9tkctJPK7PPNQJ77GKg8ZffSF6QxoMiCFYHxrtaCj", // mint
162
164
  "JDq14BWvqCRFNu1krb12bcRpbGtJZ1FLEakMw6FdxJNs", // freeze and pause (program)
163
165
  "5aMNNLQJwAEeoemTEMkv5NVjqKwvvefRYCQ5Z67HFvEq", // permanent delegate, transfer hook, confidential transfers
@@ -165,14 +167,14 @@ export const ISSUERS = [
165
167
  ] },
166
168
  // Each mint below is listed by its issuer (URL); the keys are the ones those mints carry on-chain (1/11/2026).
167
169
  // An issuer is only added with an official page for its mints: FDUSD was left out for lack of one.
168
- { name: "Paxos", keys: [
170
+ { name: "Paxos", trusted: true, keys: [
169
171
  // PYUSD 2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo https://docs.paxos.com/guides/stablecoin/pyusd/mainnet
170
172
  // USDG 2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH https://docs.paxos.com/guides/stablecoin/usdg/mainnet
171
173
  "AxJkHRY3puoofyj7Pn512M1SGBgWroXMkEdNPQypkzbu", // PYUSD mint
172
174
  "2e99QgqSDtiJSi5omGRVeZJuqNFRZdpy2YgG3mBd1AB4", // USDG mint
173
175
  "2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk" // freeze, permanent delegate, hook, fee (both)
174
176
  ] },
175
- { name: "Circle", keys: [
177
+ { name: "Circle", trusted: true, keys: [
176
178
  // USDC EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, EURC HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr
177
179
  // https://developers.circle.com/stablecoins/usdc-contract-addresses , .../eurc-contract-addresses
178
180
  "BJE5MMbqXjVwjAF7oxwPYXnTXDyspzZyt4vwenNw5ruG", // USDC mint
@@ -180,7 +182,7 @@ export const ISSUERS = [
180
182
  "Hy9168u7b2Toujh8SJKtKK8DWbyaBbXPzxm337R4o4XY", // EURC mint
181
183
  "6FF2CPcL6fat16QVJH6W27DeBcX7TSqbwY3a5wtyVJLC" // EURC freeze
182
184
  ] },
183
- { name: "Tether", keys: [
185
+ { name: "Tether", trusted: true, keys: [
184
186
  // USDT Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB (FAMOUS.USDT)
185
187
  "Q6XprfkF8RQQKoQVG33xT88H7wi8Uk1B1CC7YAs69Gi" // USDT mint and freeze
186
188
  ] }
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
@@ -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";
@@ -186,13 +197,14 @@ export function buildLights(info, conc, pair, value, type, rc, now, launches) {
186
197
 
187
198
  // 4) dependency on an issuer or a bridge
188
199
  it = [];
189
- if (issuer) it.push(sig("DEP_KNOWN_ISSUER", "warn", { issuer: issuer }));
200
+ if (trusted) it.push(sig("DEP_TRUSTED_ISSUER", "ok", { issuer: issuer }));
201
+ else if (issuer) it.push(sig("DEP_KNOWN_ISSUER", "warn", { issuer: issuer }));
190
202
  else if (info) {
191
203
  if ((info.mintAuth && ak[info.mintAuth] === "program") || (info.freezeAuth && ak[info.freezeAuth] === "program"))
192
204
  it.push(sig("DEP_PROGRAM_AUTHORITY", "warn"));
193
205
  if (info.ext.indexOf("permanentDelegate") >= 0) it.push(sig("DEP_PERMANENT_DELEGATE", "warn"));
194
206
  }
195
- if (type === "STABLE") it.push(sig("DEP_STABLE_ISSUER", "warn"));
207
+ if (type === "STABLE" && !trusted) it.push(sig("DEP_STABLE_ISSUER", "warn"));
196
208
  const dependency = mkLight("dependency", it, sig("DEP_NONE", "ok"));
197
209
 
198
210
  return [control, concentration, market, dependency];
@@ -259,7 +271,9 @@ export async function assess(client, mint, pair, opts) {
259
271
  if (info && pair && (info.mintAuth || info.freezeAuth)) {
260
272
  try { info.authKind = await getAuthKinds(client, info); } catch (e) { if (isCancel(e)) throw e; }
261
273
  }
262
- if (info && pair && opts.skipConcentration) conc = { skip: true };
274
+ // a trusted issuer's holders are not checked (its treasury and exchanges; from the browser the public RPC
275
+ // often cannot list USDC's holders, which left it "unchecked" and medium)
276
+ if (info && pair && (opts.skipConcentration || trustedIssuer(info, mint))) conc = { skip: true };
263
277
  else if (info && pair) {
264
278
  // In tokens with millions of holders (BONK) getTokenLargestAccounts sometimes times out: a second try usually works
265
279
  for (let tryC = 0; tryC < 2 && !conc; tryC++) {