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 +1 -1
- package/package.json +1 -1
- package/src/constants.js +6 -4
- package/src/i18n/en.js +2 -0
- package/src/i18n/es.js +2 -0
- package/src/index.js +1 -1
- package/src/risk.js +18 -4
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.
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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++) {
|