solana-risk-engine 0.4.11 → 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.11",
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/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
@@ -4,6 +4,7 @@ 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.
@@ -264,7 +265,10 @@ export async function assess(client, mint, pair, opts) {
264
265
  const rcPromise = pair ? rugcheckSummary(client, mint) : Promise.resolve(null); // in parallel with the RPC calls below
265
266
  // Serial launching only matters while a token is new; RugCheck's full report is small (~10 KB) then
266
267
  const fresh = pair && !opts.skipCreator && (!pair.pairCreatedAt || Date.now() - pair.pairCreatedAt < 7 * DAY);
267
- 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);
268
272
  // Copies of the same name and ticker: one search per token, so only when asked (a single token, not a wallet)
269
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);
270
274
  let info = null, conc = null;