dexterm-cli 0.0.0-stage → 0.1.1
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/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// dexterm WALLET + HOLDER INTELLIGENCE — the rules (task-1340, built from the task-1286 spike).
|
|
2
|
+
//
|
|
3
|
+
// Pure functions: raw chain facts in, labelled holders + clusters + sample-captioned insights out. No I/O, no
|
|
4
|
+
// clock, no randomness, so a recording replays to the same picture and the tests can assert every rule.
|
|
5
|
+
// Ported from research/wallet-intel/prototype/wallet-intel-proto.mjs `analyze()`; the output shape is the one
|
|
6
|
+
// the approved bubble-map mocks were rendered from (research/wallet-intel/mock/out).
|
|
7
|
+
//
|
|
8
|
+
// Label policy (design §5, binding): a label is a RULE ID + EVIDENCE, never a judgement. Seed-list entries
|
|
9
|
+
// (the typed CEX hot wallets) carry confidence 'seed' and are always printed "unverified". Cabal-class labels
|
|
10
|
+
// (cabal-candidate / smart / bot / follower / call-group) are not produced here at all (W7 measures their
|
|
11
|
+
// precision first); `HIDDEN_LABELS` is the gate the UI applies if a future store supplies them.
|
|
12
|
+
import { SYSTEM_PROGRAM } from "../trade/tx-decode.js";
|
|
13
|
+
import { PUMP_PROGRAM } from "../sources/pump-curve.js";
|
|
14
|
+
import { classifyHolder, EX_POOL_TAGS, BIG_HOLDER_PCT } from "./holder-class.js";
|
|
15
|
+
/** Labels hidden by default until their precision is measured (design §5 item 5, design decision 2026-10-06). */
|
|
16
|
+
export const HIDDEN_LABELS = new Set(['cabal-candidate', 'smart', 'bot', 'follower', 'call-group']);
|
|
17
|
+
/**
|
|
18
|
+
* Program authorities that own every pool vault of their program (SPEC §3.6, same as solana-rpc KNOWN_OWNERS)
|
|
19
|
+
* plus the SEED list of CEX hot wallets. The CEX entries are UNVERIFIED (GoPlus tagged none of them on 3/3
|
|
20
|
+
* tokens in the spike): they are shown grey with "(seed list, unverified)" and never used as a rule INPUT
|
|
21
|
+
* except as a veto (a shared CEX funder never links two wallets).
|
|
22
|
+
*/
|
|
23
|
+
export const INTEL_KNOWN_OWNERS = {
|
|
24
|
+
'5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1': { label: 'LP', name: 'Raydium AMM v4 authority', confidence: 'program-authority' },
|
|
25
|
+
GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL: { label: 'LP', name: 'Raydium CPMM authority', confidence: 'program-authority' },
|
|
26
|
+
'5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9': { label: 'cex', name: 'Binance', confidence: 'seed' },
|
|
27
|
+
'9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM': { label: 'cex', name: 'Binance', confidence: 'seed' },
|
|
28
|
+
H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS: { label: 'cex', name: 'Coinbase', confidence: 'seed' },
|
|
29
|
+
'2AQdpHJ2JpcEgPiATUXjQxA8QmafFegfQwSLWSprPicm': { label: 'cex', name: 'Coinbase', confidence: 'seed' },
|
|
30
|
+
AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2: { label: 'cex', name: 'Bybit', confidence: 'seed' },
|
|
31
|
+
'5VCwKtCXgCJ6kit5FybXjvriW3xELsFDhYrPSqtJNmcD': { label: 'cex', name: 'OKX', confidence: 'seed' },
|
|
32
|
+
FWznbcNXWQuHTawe9RxvQ2LdCENssh12dsznf4RiouN5: { label: 'cex', name: 'Kraken', confidence: 'seed' },
|
|
33
|
+
u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w: { label: 'cex', name: 'Gate.io', confidence: 'seed' },
|
|
34
|
+
BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6: { label: 'cex', name: 'KuCoin', confidence: 'seed' },
|
|
35
|
+
ASTyfSima4LLAdDgoFGkgqoKowG1LZFDr9fAQrg7iaJZ: { label: 'cex', name: 'MEXC', confidence: 'seed' },
|
|
36
|
+
};
|
|
37
|
+
/** How a static-list hit is worded (binding: seed entries say "unverified"). */
|
|
38
|
+
export function knownOwnerRule(k) {
|
|
39
|
+
return k.confidence === 'seed' ? `exchange hot wallet: ${k.name} (seed list, unverified by a second source)` : `static allow-list: ${k.name}`;
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------- helpers
|
|
42
|
+
class UnionFind {
|
|
43
|
+
p = new Map();
|
|
44
|
+
r = new Map();
|
|
45
|
+
add(x) { if (!this.p.has(x)) {
|
|
46
|
+
this.p.set(x, x);
|
|
47
|
+
this.r.set(x, 0);
|
|
48
|
+
} }
|
|
49
|
+
find(x) {
|
|
50
|
+
this.add(x);
|
|
51
|
+
let root = x;
|
|
52
|
+
while (this.p.get(root) !== root)
|
|
53
|
+
root = this.p.get(root);
|
|
54
|
+
while (this.p.get(x) !== root) {
|
|
55
|
+
const n = this.p.get(x);
|
|
56
|
+
this.p.set(x, root);
|
|
57
|
+
x = n;
|
|
58
|
+
}
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
union(a, b) {
|
|
62
|
+
a = this.find(a);
|
|
63
|
+
b = this.find(b);
|
|
64
|
+
if (a === b)
|
|
65
|
+
return false;
|
|
66
|
+
const ra = this.r.get(a), rb = this.r.get(b);
|
|
67
|
+
if (ra < rb)
|
|
68
|
+
[a, b] = [b, a];
|
|
69
|
+
this.p.set(b, a);
|
|
70
|
+
if (ra === rb)
|
|
71
|
+
this.r.set(a, ra + 1);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function accountKeys(tx) {
|
|
76
|
+
return (tx.transaction?.message?.accountKeys ?? []).map((k) => (typeof k === 'string' ? { pubkey: k, signer: false } : { pubkey: k.pubkey, signer: !!k.signer }));
|
|
77
|
+
}
|
|
78
|
+
/** Σ(post − pre) of the mint per owner within one transaction (raw base units). */
|
|
79
|
+
export function tokenDeltasByOwner(tx, mint) {
|
|
80
|
+
const pre = new Map();
|
|
81
|
+
const post = new Map();
|
|
82
|
+
for (const b of tx.meta?.preTokenBalances ?? [])
|
|
83
|
+
if (b.mint === mint)
|
|
84
|
+
pre.set(b.accountIndex, { owner: b.owner, amt: BigInt(b.uiTokenAmount.amount) });
|
|
85
|
+
for (const b of tx.meta?.postTokenBalances ?? [])
|
|
86
|
+
if (b.mint === mint)
|
|
87
|
+
post.set(b.accountIndex, { owner: b.owner, amt: BigInt(b.uiTokenAmount.amount) });
|
|
88
|
+
const byOwner = new Map();
|
|
89
|
+
for (const i of new Set([...pre.keys(), ...post.keys()])) {
|
|
90
|
+
const owner = post.get(i)?.owner ?? pre.get(i)?.owner;
|
|
91
|
+
if (!owner)
|
|
92
|
+
continue;
|
|
93
|
+
const d = (post.get(i)?.amt ?? 0n) - (pre.get(i)?.amt ?? 0n);
|
|
94
|
+
byOwner.set(owner, (byOwner.get(owner) ?? 0n) + d);
|
|
95
|
+
}
|
|
96
|
+
return byOwner;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Funding source of a wallet from its earliest transactions: the first tx where the wallet's SOL rose; the
|
|
100
|
+
* funder is the SIGNER whose SOL fell the most in that tx (measured 2026-10-06: immutable per wallet, cache forever).
|
|
101
|
+
*/
|
|
102
|
+
export function funderOf(wallet, txs) {
|
|
103
|
+
for (const tx of txs) {
|
|
104
|
+
const keys = accountKeys(tx);
|
|
105
|
+
const i = keys.findIndex((k) => k.pubkey === wallet);
|
|
106
|
+
if (i < 0)
|
|
107
|
+
continue;
|
|
108
|
+
const pre = tx.meta?.preBalances ?? [], post = tx.meta?.postBalances ?? [];
|
|
109
|
+
const delta = (post[i] ?? 0) - (pre[i] ?? 0);
|
|
110
|
+
if (delta <= 0)
|
|
111
|
+
continue;
|
|
112
|
+
let best = null, bestDrop = 0;
|
|
113
|
+
keys.forEach((k, j) => { const d = (post[j] ?? 0) - (pre[j] ?? 0); if (k.signer && d < bestDrop && k.pubkey !== wallet) {
|
|
114
|
+
bestDrop = d;
|
|
115
|
+
best = k.pubkey;
|
|
116
|
+
} });
|
|
117
|
+
if (!best)
|
|
118
|
+
continue;
|
|
119
|
+
return { funder: best, lamports: delta, slot: tx.slot, blockTime: tx.blockTime ?? null, sig: tx.transaction?.signatures?.[0] ?? null, firstSlot: txs[0]?.slot ?? null, firstBlockTime: txs[0]?.blockTime ?? null };
|
|
120
|
+
}
|
|
121
|
+
return { funder: null, firstSlot: txs[0]?.slot ?? null, firstBlockTime: txs[0]?.blockTime ?? null, reason: txs.length ? 'no SOL inflow in first 3 txs' : 'no history returned' };
|
|
122
|
+
}
|
|
123
|
+
const short = (a) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '—');
|
|
124
|
+
const round = (n, d = 2) => (n == null || Number.isNaN(n) ? 0 : Math.round(n * 10 ** d) / 10 ** d);
|
|
125
|
+
const uniq = (a) => [...new Set(a)];
|
|
126
|
+
// ---------------------------------------------------------------- the analysis
|
|
127
|
+
/** Run every rule over the raw facts. Deterministic; ~1 ms for 20 holders + 300 txs. */
|
|
128
|
+
export function analyze(raw) {
|
|
129
|
+
const { mint } = raw;
|
|
130
|
+
const supplyRaw = BigInt(raw.supply || '0');
|
|
131
|
+
const supply = Number(supplyRaw);
|
|
132
|
+
// ---- holders: token accounts → owners → kinds → static labels
|
|
133
|
+
const ownerKind = new Map();
|
|
134
|
+
for (const [o, m] of Object.entries(raw.ownerMeta)) {
|
|
135
|
+
let kind = 'wallet';
|
|
136
|
+
if (!m.exists)
|
|
137
|
+
kind = 'no-sol-account';
|
|
138
|
+
else if (m.programOwner !== SYSTEM_PROGRAM)
|
|
139
|
+
kind = 'program-owned';
|
|
140
|
+
ownerKind.set(o, { kind, programOwner: m.programOwner });
|
|
141
|
+
}
|
|
142
|
+
const holdersByOwner = new Map();
|
|
143
|
+
for (const a of raw.largest) {
|
|
144
|
+
const owner = raw.owners[a.address] ?? null;
|
|
145
|
+
const key = owner ?? a.address;
|
|
146
|
+
const h = holdersByOwner.get(key) ?? { owner: key, tokenAccounts: [], amount: 0n };
|
|
147
|
+
h.tokenAccounts.push(a.address);
|
|
148
|
+
h.amount += BigInt(a.amount);
|
|
149
|
+
holdersByOwner.set(key, h);
|
|
150
|
+
}
|
|
151
|
+
const sorted = [...holdersByOwner.values()].sort((a, b) => (a.amount < b.amount ? 1 : a.amount > b.amount ? -1 : 0));
|
|
152
|
+
const holders = sorted.map((h) => {
|
|
153
|
+
const k = ownerKind.get(h.owner) ?? { kind: 'unknown', programOwner: null };
|
|
154
|
+
const labels = [];
|
|
155
|
+
const known = INTEL_KNOWN_OWNERS[h.owner];
|
|
156
|
+
if (known)
|
|
157
|
+
labels.push({ label: known.label, rule: knownOwnerRule(known), confidence: known.confidence });
|
|
158
|
+
else if (k.kind === 'program-owned')
|
|
159
|
+
labels.push({ label: 'contract', rule: `token account owner is program-owned (owner program ${short(k.programOwner)}): pool vault / escrow / staking, not a person`, confidence: 'on-chain' });
|
|
160
|
+
else if (k.kind === 'no-sol-account')
|
|
161
|
+
labels.push({ label: 'unfunded-authority?', rule: 'owner has no SOL account: typically a PDA authority (pool / vault); could be a wallet that never held SOL', confidence: 'weak' });
|
|
162
|
+
return {
|
|
163
|
+
owner: h.owner, tokenAccounts: h.tokenAccounts, amount: h.amount.toString(),
|
|
164
|
+
pct: supply > 0 ? Number(h.amount) / supply * 100 : 0,
|
|
165
|
+
ownerKind: k.kind, programOwner: k.programOwner, labels, funder: null, walletFirstSeen: null, freshAtBuy: null, createdBeforeLaunch: null,
|
|
166
|
+
cls: { tag: 'UNLABELLED', reason: '', program: null, lock: null }, // set after the launch rules
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const holderSet = new Set(holders.map((h) => h.owner));
|
|
170
|
+
const pctOf = (owner) => holders.find((h) => h.owner === owner)?.pct ?? 0;
|
|
171
|
+
// ---- funding sources
|
|
172
|
+
const funding = {};
|
|
173
|
+
for (const [w, txs] of Object.entries(raw.funding))
|
|
174
|
+
funding[w] = funderOf(w, txs);
|
|
175
|
+
// ---- launch window
|
|
176
|
+
const launch = raw.launch;
|
|
177
|
+
const firstSlot = launch.length ? launch[0].slot : null;
|
|
178
|
+
const firstBlockTime = launch.length ? launch[0].blockTime ?? null : null;
|
|
179
|
+
const creator = launch.length ? accountKeys(launch[0]).find((k) => k.signer)?.pubkey ?? null : null;
|
|
180
|
+
const usesPump = launch.some((tx) => accountKeys(tx).some((k) => k.pubkey === PUMP_PROGRAM));
|
|
181
|
+
// P1 pool: the owner that is the SELLING counterparty in ≥ 25 % of launch txs (n ≥ 8) is the venue, never a holder.
|
|
182
|
+
// It must run before S1 or the pump.fun curve becomes "the biggest sniper" (happened in the spike).
|
|
183
|
+
const sellerCount = new Map();
|
|
184
|
+
let txWithMintDeltas = 0;
|
|
185
|
+
const deltasPerTx = launch.map((tx) => {
|
|
186
|
+
const d = tokenDeltasByOwner(tx, mint);
|
|
187
|
+
if (d.size)
|
|
188
|
+
txWithMintDeltas += 1;
|
|
189
|
+
for (const [o, v] of d)
|
|
190
|
+
if (v < 0n)
|
|
191
|
+
sellerCount.set(o, (sellerCount.get(o) ?? 0) + 1);
|
|
192
|
+
return d;
|
|
193
|
+
});
|
|
194
|
+
const pools = new Set([...sellerCount].filter(([, n]) => txWithMintDeltas >= 8 && n >= txWithMintDeltas * 0.25).map(([o]) => o));
|
|
195
|
+
const buyEvents = [];
|
|
196
|
+
launch.forEach((tx, k) => {
|
|
197
|
+
for (const [owner, delta] of deltasPerTx[k])
|
|
198
|
+
if (delta > 0n && !pools.has(owner))
|
|
199
|
+
buyEvents.push({ slot: tx.slot, txIndex: tx.transactionIndex ?? null, sig: tx.transaction?.signatures?.[0] ?? null, owner, delta });
|
|
200
|
+
});
|
|
201
|
+
const slots = uniq(launch.map((t) => t.slot)).sort((a, b) => a - b);
|
|
202
|
+
// S1 sniper window: the first 3 slots after the first POOL trade (not the first mint tx: WIF/POPCAT's first
|
|
203
|
+
// txs are mint/LP setup with zero buyers). For pump.fun the create tx is the first trade, so both coincide.
|
|
204
|
+
const firstPoolTrade = buyEvents.length ? Math.min(...buyEvents.map((e) => e.slot)) : null;
|
|
205
|
+
const sniperSlotMax = firstPoolTrade != null ? firstPoolTrade + 2 : null;
|
|
206
|
+
const sniperSet = new Map();
|
|
207
|
+
for (const e of buyEvents)
|
|
208
|
+
if (sniperSlotMax != null && e.slot <= sniperSlotMax && e.owner !== creator)
|
|
209
|
+
sniperSet.set(e.owner, (sniperSet.get(e.owner) ?? 0n) + e.delta);
|
|
210
|
+
const bySlot = new Map();
|
|
211
|
+
for (const e of buyEvents) {
|
|
212
|
+
const s = bySlot.get(e.slot) ?? new Set();
|
|
213
|
+
s.add(e.owner);
|
|
214
|
+
bySlot.set(e.slot, s);
|
|
215
|
+
}
|
|
216
|
+
const sameSlotCohorts = [...bySlot.entries()].filter(([, s]) => s.size >= 2).map(([slot, s]) => ({ slot, buyers: [...s] })).sort((a, b) => a.slot - b.slot);
|
|
217
|
+
// ---- clustering: union-find over holders + funders + creator
|
|
218
|
+
const uf = new UnionFind();
|
|
219
|
+
const evidence = [];
|
|
220
|
+
const funderIndex = new Map();
|
|
221
|
+
for (const [w, f] of Object.entries(funding))
|
|
222
|
+
if (f.funder) {
|
|
223
|
+
const arr = funderIndex.get(f.funder) ?? [];
|
|
224
|
+
arr.push(w);
|
|
225
|
+
funderIndex.set(f.funder, arr);
|
|
226
|
+
}
|
|
227
|
+
const isCex = (addr) => !!addr && INTEL_KNOWN_OWNERS[addr]?.label === 'cex';
|
|
228
|
+
// R1b: the funder is itself a top holder → one entity, two wallets
|
|
229
|
+
for (const [funder, ws] of funderIndex)
|
|
230
|
+
if (holderSet.has(funder) && !isCex(funder))
|
|
231
|
+
for (const w of ws) {
|
|
232
|
+
uf.union(funder, w);
|
|
233
|
+
evidence.push({ a: funder, b: w, rule: `funded by top holder ${short(funder)} (R1b funder-is-holder)`, linked: true });
|
|
234
|
+
}
|
|
235
|
+
// R1: shared funding source; a CEX funder is a VETO (withdrawals from one exchange are independent people)
|
|
236
|
+
for (const [funder, ws] of funderIndex) {
|
|
237
|
+
if (ws.length < 2)
|
|
238
|
+
continue;
|
|
239
|
+
if (isCex(funder)) {
|
|
240
|
+
evidence.push({ a: ws[0], b: ws[1], rule: `shared funder ${short(funder)} is an exchange hot wallet (seed list, unverified): NOT linked (withdrawals from one exchange are independent people)`, linked: false });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const rule = `shared funding source ${short(funder)} (R1 funder)`;
|
|
244
|
+
for (let i = 1; i < ws.length; i++) {
|
|
245
|
+
uf.union(ws[0], ws[i]);
|
|
246
|
+
evidence.push({ a: ws[0], b: ws[i], rule, linked: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// R2 bundle: same-slot buys AND a shared funder (same-slot alone is a cohort, never a bundle: 55–146 cohorts per 300 txs)
|
|
250
|
+
for (const c of sameSlotCohorts) {
|
|
251
|
+
// every pair in the cohort (the spike only compared against the first buyer and missed bundles behind a sniper)
|
|
252
|
+
for (let i = 0; i < c.buyers.length; i++)
|
|
253
|
+
for (let j = i + 1; j < c.buyers.length; j++) {
|
|
254
|
+
const a = c.buyers[i], b = c.buyers[j];
|
|
255
|
+
const fa = funding[a]?.funder, fb = funding[b]?.funder;
|
|
256
|
+
if (fa && fb && fa === fb && !isCex(fa)) {
|
|
257
|
+
uf.union(a, b);
|
|
258
|
+
evidence.push({ a, b, rule: `same-slot buy (slot ${c.slot}) AND shared funder ${short(fa)} (R2 bundle)`, linked: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// R4 dev-linked: funded by the creator wallet
|
|
263
|
+
if (creator)
|
|
264
|
+
for (const [w, f] of Object.entries(funding))
|
|
265
|
+
if (f.funder === creator) {
|
|
266
|
+
uf.union(creator, w);
|
|
267
|
+
evidence.push({ a: creator, b: w, rule: `funded by the creator wallet ${short(creator)} (R4 dev-linked)`, linked: true });
|
|
268
|
+
}
|
|
269
|
+
// R5 same-slot creation: weak, never links alone; surfaced as a label so the user can judge
|
|
270
|
+
const byFirstSlot = new Map();
|
|
271
|
+
for (const [w, f] of Object.entries(funding))
|
|
272
|
+
if (f.firstSlot != null) {
|
|
273
|
+
const a = byFirstSlot.get(f.firstSlot) ?? [];
|
|
274
|
+
a.push(w);
|
|
275
|
+
byFirstSlot.set(f.firstSlot, a);
|
|
276
|
+
}
|
|
277
|
+
const sameSlotCreated = new Map();
|
|
278
|
+
for (const [slot, ws] of byFirstSlot)
|
|
279
|
+
if (ws.length >= 2) {
|
|
280
|
+
for (const w of ws)
|
|
281
|
+
sameSlotCreated.set(w, { slot, with: ws.filter((x) => x !== w) });
|
|
282
|
+
evidence.push({ a: ws[0], b: ws[1], rule: `wallets first seen in the same slot ${slot} (R5 same-slot creation, weak: not linked alone)`, linked: false });
|
|
283
|
+
}
|
|
284
|
+
// R3 transfer: a direct token move between two top holders inside the launch window (one sender, ≤ 2 receivers,
|
|
285
|
+
// no swap). A pool / curve (P1) is never a party: on a fresh launch the curve is still a top holder and every
|
|
286
|
+
// buy is "curve → buyer", which is a swap, not a transfer.
|
|
287
|
+
for (const tx of launch) {
|
|
288
|
+
const d = tokenDeltasByOwner(tx, mint);
|
|
289
|
+
if ([...d.keys()].some((o) => pools.has(o)))
|
|
290
|
+
continue;
|
|
291
|
+
const senders = [...d].filter(([o, v]) => v < 0n && holderSet.has(o)).map(([o]) => o);
|
|
292
|
+
const receivers = [...d].filter(([o, v]) => v > 0n && holderSet.has(o)).map(([o]) => o);
|
|
293
|
+
if (senders.length === 1 && receivers.length >= 1 && receivers.length <= 2 && d.size <= 3)
|
|
294
|
+
for (const r of receivers) {
|
|
295
|
+
uf.union(senders[0], r);
|
|
296
|
+
evidence.push({ a: senders[0], b: r, rule: `direct token transfer in ${short(tx.transaction?.signatures?.[0] ?? '')} (R3 transfer)`, linked: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const clusters = new Map();
|
|
300
|
+
for (const h of holders) {
|
|
301
|
+
const root = uf.find(h.owner);
|
|
302
|
+
const c = clusters.get(root) ?? { members: [], pct: 0, labels: new Set(), evidence: [] };
|
|
303
|
+
c.members.push(h.owner);
|
|
304
|
+
c.pct += h.pct;
|
|
305
|
+
clusters.set(root, c);
|
|
306
|
+
}
|
|
307
|
+
for (const e of evidence)
|
|
308
|
+
if (e.linked) {
|
|
309
|
+
const c = clusters.get(uf.find(e.a));
|
|
310
|
+
if (c)
|
|
311
|
+
c.evidence.push(e);
|
|
312
|
+
}
|
|
313
|
+
// ---- per-holder derived labels (after the launch rules so the cluster labels see them)
|
|
314
|
+
for (const h of holders) {
|
|
315
|
+
const f = funding[h.owner];
|
|
316
|
+
const firstTxs = raw.funding[h.owner] ?? null;
|
|
317
|
+
if (f?.funder)
|
|
318
|
+
h.funder = { address: f.funder, label: INTEL_KNOWN_OWNERS[f.funder] ? `${INTEL_KNOWN_OWNERS[f.funder].name}${INTEL_KNOWN_OWNERS[f.funder].confidence === 'seed' ? ' (seed, unverified)' : ''}` : null, blockTime: f.blockTime ?? null, sig: f.sig ?? null };
|
|
319
|
+
if (f?.firstBlockTime)
|
|
320
|
+
h.walletFirstSeen = f.firstBlockTime;
|
|
321
|
+
if (pools.has(h.owner))
|
|
322
|
+
h.labels.push({ label: 'pool', rule: `selling counterparty in ${sellerCount.get(h.owner)} of ${txWithMintDeltas} launch-window txs: bonding curve / AMM vault (P1 pool)`, confidence: 'on-chain' });
|
|
323
|
+
if (sniperSet.has(h.owner))
|
|
324
|
+
h.labels.push({ label: 'sniper', rule: `bought in the first 3 slots after the first pool trade (slot ≤ ${sniperSlotMax}) (S1 sniper)`, confidence: 'on-chain' });
|
|
325
|
+
if (creator && h.owner === creator)
|
|
326
|
+
h.labels.push({ label: 'dev', rule: `fee payer of the first transaction referencing the mint (${short(launch[0]?.transaction?.signatures?.[0] ?? '')}) (D1 creator)`, confidence: 'on-chain' });
|
|
327
|
+
if (firstTxs) {
|
|
328
|
+
const includesMint = firstTxs.some((tx) => (tx.meta?.postTokenBalances ?? []).some((b) => b.mint === mint));
|
|
329
|
+
h.freshAtBuy = includesMint;
|
|
330
|
+
if (includesMint)
|
|
331
|
+
h.labels.push({ label: 'fresh-at-buy', rule: `wallet's first ${firstTxs.length} transactions already include this token: created to hold it (F1 fresh)`, confidence: 'on-chain' });
|
|
332
|
+
if (f?.firstBlockTime && firstBlockTime)
|
|
333
|
+
h.createdBeforeLaunch = f.firstBlockTime < firstBlockTime;
|
|
334
|
+
}
|
|
335
|
+
const ssc = sameSlotCreated.get(h.owner);
|
|
336
|
+
if (ssc)
|
|
337
|
+
h.labels.push({ label: 'same-slot-created', rule: `first seen in slot ${ssc.slot}, same slot as ${ssc.with.map(short).join(', ')} (R5, weak)`, confidence: 'weak' });
|
|
338
|
+
const cross = raw.crossToken?.[h.owner];
|
|
339
|
+
if (cross && cross.length >= 2)
|
|
340
|
+
h.labels.push({ label: 'multi-token-top20', rule: `inside the top-20 of ${cross.length} analysed tokens (${cross.join(', ')}): exchange / market maker / fund pattern, not launch coordination (X1)`, confidence: 'derived' });
|
|
341
|
+
}
|
|
342
|
+
for (const h of holders) {
|
|
343
|
+
const c = clusters.get(uf.find(h.owner));
|
|
344
|
+
if (c)
|
|
345
|
+
for (const l of h.labels)
|
|
346
|
+
c.labels.add(l.label);
|
|
347
|
+
}
|
|
348
|
+
// task-1367: one class per holder from the owner facts + the launch rules above (never a guess)
|
|
349
|
+
for (const h of holders) {
|
|
350
|
+
const c = clusters.get(uf.find(h.owner));
|
|
351
|
+
const link = c && c.members.length > 1 ? c.evidence.find((e) => e.linked && (e.a === h.owner || e.b === h.owner)) ?? c.evidence.find((e) => e.linked) : undefined;
|
|
352
|
+
const known = INTEL_KNOWN_OWNERS[h.owner];
|
|
353
|
+
h.cls = classifyHolder({
|
|
354
|
+
mint, owner: h.owner, tokenAccounts: h.tokenAccounts, meta: raw.ownerMeta[h.owner],
|
|
355
|
+
cex: known?.label === 'cex' ? known.name : null,
|
|
356
|
+
dev: h.labels.find((l) => l.label === 'dev')?.rule ?? null,
|
|
357
|
+
poolRule: h.labels.find((l) => l.label === 'pool')?.rule ?? null,
|
|
358
|
+
linked: link ? `linked to ${c.members.length - 1} other top holder${c.members.length > 2 ? 's' : ''}: ${link.rule}` : null,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const clusterList = [...clusters.values()]
|
|
362
|
+
.map((c) => ({
|
|
363
|
+
members: c.members, pct: c.pct, evidence: c.evidence, labels: [...c.labels],
|
|
364
|
+
kind: (c.members.length > 1
|
|
365
|
+
? (c.evidence.some((e) => /\(R4 /.test(e.rule)) ? 'dev-linked' : c.evidence.some((e) => /\(R2 /.test(e.rule)) ? 'bundle' : c.evidence.some((e) => /\(R3 /.test(e.rule)) ? 'transfer' : 'funder')
|
|
366
|
+
: 'single'),
|
|
367
|
+
}))
|
|
368
|
+
.sort((a, b) => b.pct - a.pct)
|
|
369
|
+
.map((c, i) => ({ ...c, name: `C${i + 1}`, pct: round(c.pct) }));
|
|
370
|
+
// ---- insights with coverage (every number is sample-bounded and says so)
|
|
371
|
+
const topByAccount = raw.largest.slice(0, 10).reduce((s, a) => s + (supply > 0 ? Number(BigInt(a.amount)) / supply * 100 : 0), 0);
|
|
372
|
+
const topByOwner = holders.slice(0, 10).reduce((s, h) => s + h.pct, 0);
|
|
373
|
+
const excluded = new Set(['LP', 'cex', 'contract', 'pool']);
|
|
374
|
+
const adjustable = holders.filter((h) => !h.labels.some((l) => excluded.has(l.label)));
|
|
375
|
+
const topClusterAdjusted = clusterList.filter((c) => !c.labels.some((l) => excluded.has(l))).slice(0, 10).reduce((s, c) => s + c.pct, 0);
|
|
376
|
+
const coveredPct = holders.reduce((s, h) => s + h.pct, 0);
|
|
377
|
+
const fundedCoverPct = Object.keys(funding).reduce((s, w) => s + pctOf(w), 0);
|
|
378
|
+
const sniperHoldNow = holders.filter((h) => sniperSet.has(h.owner)).reduce((s, h) => s + h.pct, 0);
|
|
379
|
+
const sniperLaunchShare = supply > 0 ? [...sniperSet.values()].reduce((s, v) => s + Number(v), 0) / supply * 100 : 0;
|
|
380
|
+
const devNow = creator ? holders.find((h) => h.owner === creator)?.pct ?? null : null;
|
|
381
|
+
const smallestTop = holders.length ? holders[holders.length - 1].pct : null;
|
|
382
|
+
const freshPct = holders.filter((h) => h.freshAtBuy === true).reduce((s, h) => s + h.pct, 0);
|
|
383
|
+
const freshMeasured = holders.filter((h) => h.freshAtBuy != null);
|
|
384
|
+
const freshMeasuredPct = freshMeasured.reduce((s, h) => s + h.pct, 0);
|
|
385
|
+
const bundleClusters = clusterList.filter((c) => c.kind === 'bundle');
|
|
386
|
+
const people = holders.filter((h) => !EX_POOL_TAGS.has(h.cls.tag));
|
|
387
|
+
const exPool = people.slice(0, 10).reduce((s, h) => s + h.pct, 0);
|
|
388
|
+
const dropped = holders.filter((h) => EX_POOL_TAGS.has(h.cls.tag));
|
|
389
|
+
const big = people.find((h) => h.pct > BIG_HOLDER_PCT) ?? null;
|
|
390
|
+
const insights = {
|
|
391
|
+
supply: supplyRaw.toString(), decimals: raw.decimals,
|
|
392
|
+
top10: {
|
|
393
|
+
byTokenAccount: round(topByAccount), byOwner: round(topByOwner), clusterAdjusted: round(topClusterAdjusted),
|
|
394
|
+
excludedLabels: [...excluded], excludedCount: holders.length - adjustable.length,
|
|
395
|
+
note: 'byTokenAccount = what explorers show; byOwner merges token accounts of one owner; clusterAdjusted merges linked owners and drops LP vaults, program-owned accounts and seed-list CEX wallets; exPool = top-10 owners without POOL/CURVE/VAULT/LOCK/BURN owners',
|
|
396
|
+
exPool: round(exPool), exPoolDroppedPct: round(dropped.reduce((s, h) => s + h.pct, 0)), exPoolDropped: dropped.length,
|
|
397
|
+
},
|
|
398
|
+
bigHolder: big ? { owner: big.owner, pct: round(big.pct), tag: big.cls.tag, reason: big.cls.reason } : null,
|
|
399
|
+
coverage: {
|
|
400
|
+
topHoldersFetched: holders.length, supplyCoveredPct: round(coveredPct), fundingLookups: Object.keys(funding).length, fundingCoveredPct: round(fundedCoverPct),
|
|
401
|
+
launchTxs: launch.length, launchTxsWithMintBalanceChange: txWithMintDeltas, launchSlots: slots.length, launchSlotSpan: slots.length ? slots[slots.length - 1] - slots[0] : 0, launchMorePages: raw.launchMore,
|
|
402
|
+
},
|
|
403
|
+
snipers: { count: sniperSet.size, launchWindowSharePct: round(sniperLaunchShare), stillInTop: holders.filter((h) => sniperSet.has(h.owner)).length, holdNowPctOfTopSample: round(sniperHoldNow), definition: 'distinct owners whose balance rose in the first 3 slots after the first pool trade, creator and pools excluded (S1)', honest: 'holdNow is only known for snipers inside the fetched top sample' },
|
|
404
|
+
bundle: { sameSlotCohorts: sameSlotCohorts.length, largestCohort: sameSlotCohorts.reduce((m, c) => Math.max(m, c.buyers.length), 0), bundleClustersFound: bundleClusters.length, bundlePctOfTopSample: round(bundleClusters.reduce((s, c) => s + c.pct, 0)), definition: 'same-slot buyers that ALSO share a funding source (R2); same-slot alone is a cohort, not a bundle', honest: 'funder lookups ran for the top wallets only; cohorts outside the sample are unresolved' },
|
|
405
|
+
dev: { creator, creatorPctNow: devNow == null ? null : round(devNow), creatorUpperBoundPct: devNow == null && creator && smallestTop != null ? round(smallestTop) : null, usesPumpProgram: usesPump, definition: 'creator = fee payer of the first succeeded tx referencing the mint; dev % = creator share now' },
|
|
406
|
+
fresh: { freshAtBuyPctOfTopSample: round(freshPct), measuredWallets: freshMeasured.length, measuredPct: round(freshMeasuredPct), notMeasuredPct: round(coveredPct - freshMeasuredPct), definition: "fresh-at-buy = the wallet's first transactions already move this token (created to hold it); one history call per wallet", honest: 'only measured for the funded sample' },
|
|
407
|
+
launch: { firstSlot, firstBlockTime, firstBlockTimeIso: firstBlockTime ? new Date(firstBlockTime * 1000).toISOString() : null, creator, pools: [...pools], buyers: uniq(buyEvents.map((e) => e.owner)).length, sameSlotCohorts: sameSlotCohorts.slice(0, 10).map((c) => ({ slot: c.slot, buyers: c.buyers.length })), sameSlotCreatedPairs: [...byFirstSlot.values()].filter((ws) => ws.length >= 2).length },
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
mint,
|
|
411
|
+
holders: holders.map((h) => ({ ...h, pct: round(h.pct) })),
|
|
412
|
+
clusters: clusterList,
|
|
413
|
+
funding: Object.fromEntries(Object.entries(funding).map(([w, f]) => [w, { ...f }])),
|
|
414
|
+
insights,
|
|
415
|
+
buyEvents: buyEvents.slice(0, 500).map((e) => ({ ...e, delta: e.delta.toString() })),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/** Drop labels the policy hides by default (cabal-class) unless the user opted in. */
|
|
419
|
+
export function visibleLabels(labels, showHidden = false) {
|
|
420
|
+
return showHidden ? labels : labels.filter((l) => !HIDDEN_LABELS.has(l.label));
|
|
421
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { systemClock } from "../clock.js";
|
|
2
|
+
import { HeliusHistory } from "./helius-history.js";
|
|
3
|
+
import { analyze, INTEL_KNOWN_OWNERS } from "./rules.js";
|
|
4
|
+
import { SYSTEM_PROGRAM } from "../trade/tx-decode.js";
|
|
5
|
+
export const SCAN_DEPTHS = {
|
|
6
|
+
light: { funders: 7, launchTxs: 100 },
|
|
7
|
+
deep: { funders: 20, launchTxs: 300 },
|
|
8
|
+
};
|
|
9
|
+
/** Cache namespaces. Funders are immutable (cached a year); launch windows too (first N txs never change). */
|
|
10
|
+
const NS = { funder: 'intel:funder', launch: 'intel:launch', scan: 'intel:scan', ownerMeta: 'intel:owner' };
|
|
11
|
+
const FUNDER_TTL_MS = 365 * 86_400_000;
|
|
12
|
+
const LAUNCH_TTL_MS = 30 * 86_400_000;
|
|
13
|
+
const OWNER_META_TTL_MS = 86_400_000;
|
|
14
|
+
export const SCAN_TTL_MS = 60_000;
|
|
15
|
+
export const KEY_FIX = 'free Helius key → ~/.dexterm/helius.key (1M credits/month, no card; `dexterm doctor` walks through it)';
|
|
16
|
+
export class WalletIntel {
|
|
17
|
+
cache;
|
|
18
|
+
pool;
|
|
19
|
+
detail;
|
|
20
|
+
health;
|
|
21
|
+
credits;
|
|
22
|
+
clock;
|
|
23
|
+
history;
|
|
24
|
+
inflight = new Map();
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.cache = deps.cache;
|
|
27
|
+
this.pool = deps.pool;
|
|
28
|
+
this.detail = deps.detail;
|
|
29
|
+
this.health = deps.health;
|
|
30
|
+
this.credits = deps.credits ?? null;
|
|
31
|
+
this.clock = deps.clock ?? systemClock;
|
|
32
|
+
if (deps.history !== undefined)
|
|
33
|
+
this.history = deps.history;
|
|
34
|
+
else {
|
|
35
|
+
const url = deps.pool.heliusUrl;
|
|
36
|
+
this.history = url ? new HeliusHistory(deps.http, url) : null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** True when the history leg (funders + launch window) can run right now. */
|
|
40
|
+
historyAvailable() {
|
|
41
|
+
if (!this.history)
|
|
42
|
+
return { ok: false, why: `funders and launch window need a ${KEY_FIX}` };
|
|
43
|
+
if (!this.health.isAvailable('helius'))
|
|
44
|
+
return { ok: false, why: `Helius is ${this.health.state('helius')} (${this.credits && this.credits.level() === 'degraded' ? 'daily credit budget spent' : 'rate limited or down'}); funders and launch window paused` };
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Scan one mint. Results are cached SCAN_TTL_MS per (mint, depth) and in-flight scans coalesce, so a 30 fps
|
|
49
|
+
* UI asking every frame costs nothing. Never throws: every failure is an `unavailable` result with a reason.
|
|
50
|
+
*/
|
|
51
|
+
scan(mint, depth = 'light', opts = {}) {
|
|
52
|
+
const key = `${mint}|${depth}`;
|
|
53
|
+
if (!opts.force) {
|
|
54
|
+
const hit = this.cache.get(NS.scan, key);
|
|
55
|
+
if (hit && !hit.stale)
|
|
56
|
+
return Promise.resolve({ ...hit.value, fromCache: true });
|
|
57
|
+
}
|
|
58
|
+
const running = this.inflight.get(key);
|
|
59
|
+
if (running)
|
|
60
|
+
return running;
|
|
61
|
+
const p = this.run(mint, depth).finally(() => this.inflight.delete(key));
|
|
62
|
+
this.inflight.set(key, p);
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
async run(mint, depth) {
|
|
66
|
+
const now = this.clock.now();
|
|
67
|
+
const plan = SCAN_DEPTHS[depth];
|
|
68
|
+
// ---- B–D: ranked holder list through the existing TokenDetail (cached 60 s, pool rotation, aggregate fallback)
|
|
69
|
+
let top;
|
|
70
|
+
try {
|
|
71
|
+
top = await this.detail.holders(mint);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const keyless = !this.pool.hasKeyed;
|
|
75
|
+
return { state: 'unavailable', depth, reason: `holder list unavailable: ${msg(e)}`, fix: keyless ? KEY_FIX : null, fetchedAt: now };
|
|
76
|
+
}
|
|
77
|
+
const th = top.value;
|
|
78
|
+
if (th.aggregateOnly || th.holders.length === 0) {
|
|
79
|
+
return { state: 'unavailable', depth, reason: th.aggregateOnly ? 'no RPC endpoint serves the ranked holder list (public RPCs throttle getTokenLargestAccounts)' : 'the holder list came back empty', fix: this.pool.hasKeyed ? null : KEY_FIX, fetchedAt: now };
|
|
80
|
+
}
|
|
81
|
+
const owners = {};
|
|
82
|
+
for (const h of th.holders)
|
|
83
|
+
if (h.owner)
|
|
84
|
+
owners[h.tokenAccount] = h.owner;
|
|
85
|
+
const ownerList = [...new Set(Object.values(owners))];
|
|
86
|
+
// owner kinds (System-owned wallet vs program-owned vault); cached a day per owner
|
|
87
|
+
const ownerMeta = {};
|
|
88
|
+
const missing = [];
|
|
89
|
+
for (const o of ownerList) {
|
|
90
|
+
const c = this.cache.get(NS.ownerMeta, o);
|
|
91
|
+
if (c && !c.stale)
|
|
92
|
+
ownerMeta[o] = c.value;
|
|
93
|
+
else
|
|
94
|
+
missing.push(o);
|
|
95
|
+
}
|
|
96
|
+
if (missing.length) {
|
|
97
|
+
try {
|
|
98
|
+
const m = await this.pool.accountMeta(missing);
|
|
99
|
+
for (const [o, meta] of m) {
|
|
100
|
+
ownerMeta[o] = meta;
|
|
101
|
+
this.cache.set(NS.ownerMeta, o, meta, { ttlMs: OWNER_META_TTL_MS, source: this.pool.primary.source });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* kinds stay unknown: labels fall back to the static list only */ }
|
|
105
|
+
}
|
|
106
|
+
// ---- E–F: funders + launch window (Helius only)
|
|
107
|
+
const funding = {};
|
|
108
|
+
let launch = [];
|
|
109
|
+
let launchMore = false;
|
|
110
|
+
let degraded = null;
|
|
111
|
+
let history = null;
|
|
112
|
+
const hist = this.historyAvailable();
|
|
113
|
+
if (!hist.ok)
|
|
114
|
+
degraded = hist.why;
|
|
115
|
+
else {
|
|
116
|
+
history = 'helius';
|
|
117
|
+
const wallets = ownerList.filter((o) => { const m = ownerMeta[o]; return (!m || m.programOwner === SYSTEM_PROGRAM || !m.exists) && !INTEL_KNOWN_OWNERS[o]; }).slice(0, plan.funders);
|
|
118
|
+
const errors = [];
|
|
119
|
+
for (const w of wallets) {
|
|
120
|
+
const c = this.cache.get(NS.funder, w);
|
|
121
|
+
if (c) {
|
|
122
|
+
funding[w] = c.value;
|
|
123
|
+
continue;
|
|
124
|
+
} // immutable: stale is fine
|
|
125
|
+
try {
|
|
126
|
+
const page = await this.history.firstTxs(w);
|
|
127
|
+
funding[w] = page.data;
|
|
128
|
+
this.cache.set(NS.funder, w, page.data, { ttlMs: FUNDER_TTL_MS, source: 'helius' });
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
errors.push(msg(e));
|
|
132
|
+
if (errors.length >= 2)
|
|
133
|
+
break;
|
|
134
|
+
} // two failures: stop spending
|
|
135
|
+
}
|
|
136
|
+
const lk = `${mint}|${plan.launchTxs}`;
|
|
137
|
+
const lc = this.cache.get(NS.launch, lk);
|
|
138
|
+
if (lc) {
|
|
139
|
+
launch = lc.value.data;
|
|
140
|
+
launchMore = lc.value.more;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
try {
|
|
144
|
+
const page = await this.history.launchWindow(mint, plan.launchTxs);
|
|
145
|
+
launch = page.data;
|
|
146
|
+
launchMore = page.paginationToken !== null;
|
|
147
|
+
this.cache.set(NS.launch, lk, { data: launch, more: launchMore }, { ttlMs: LAUNCH_TTL_MS, source: 'helius' });
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
errors.push(`launch window: ${msg(e)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (errors.length)
|
|
154
|
+
degraded = `history partial (${errors[0]})`;
|
|
155
|
+
}
|
|
156
|
+
// Raw amounts: the holder list carries whole-token floats and a bigint-exact pct; rebuild base units from
|
|
157
|
+
// pct × supply so an 18-digit supply keeps its digits (the rules only ever compare shares).
|
|
158
|
+
const supply = BigInt(th.supply);
|
|
159
|
+
const raw = {
|
|
160
|
+
mint, supply: th.supply, decimals: th.decimals,
|
|
161
|
+
largest: th.holders.map((h) => ({ address: h.tokenAccount, amount: (supply > 0n ? (BigInt(Math.round(h.pct * 1e6)) * supply) / 100000000n : 0n).toString() })),
|
|
162
|
+
owners, ownerMeta, funding, launch, launchMore,
|
|
163
|
+
};
|
|
164
|
+
const analysis = analyze(raw);
|
|
165
|
+
const ok = { state: 'ok', depth, analysis, via: { holders: th.source, history }, degraded, fetchedAt: this.clock.now(), fromCache: false };
|
|
166
|
+
this.cache.set(NS.scan, `${mint}|${depth}`, ok, { ttlMs: SCAN_TTL_MS, source: th.source });
|
|
167
|
+
return ok;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function msg(e) { return e instanceof Error ? e.message : String(e); }
|