dexterm-cli 0.1.7 → 0.2.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/dist/packages/agents/src/spectator.js +31 -3
- package/dist/packages/agents/src/tiers.js +12 -0
- package/dist/packages/agents/src/tools.js +13 -5
- package/dist/packages/autotrade/assets/limits.schema.json +569 -0
- package/dist/packages/autotrade/examples/executor/README.md +70 -0
- package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
- package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
- package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
- package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
- package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
- package/dist/packages/autotrade/src/backtest.js +7 -2
- package/dist/packages/autotrade/src/chat/config.js +4 -2
- package/dist/packages/autotrade/src/chat/control.js +1 -0
- package/dist/packages/autotrade/src/chat/notify.js +16 -6
- package/dist/packages/autotrade/src/chat/secrets.js +25 -18
- package/dist/packages/autotrade/src/chat/service.js +3 -0
- package/dist/packages/autotrade/src/engine.js +16 -2
- package/dist/packages/autotrade/src/executor-health.js +217 -0
- package/dist/packages/autotrade/src/fleet.js +5 -2
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +21 -0
- package/dist/packages/autotrade/src/journey.js +477 -0
- package/dist/packages/autotrade/src/limits.js +682 -0
- package/dist/packages/autotrade/src/outbox.js +394 -0
- package/dist/packages/autotrade/src/paper-limits.js +87 -0
- package/dist/packages/autotrade/src/real-mode.js +688 -0
- package/dist/packages/autotrade/src/receipts.js +330 -0
- package/dist/packages/autotrade/src/report.js +168 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +194 -28
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +215 -0
- package/dist/packages/autotrade/src/service-recover.js +232 -0
- package/dist/packages/autotrade/src/service.js +289 -0
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +15 -2
- package/dist/packages/autotrade/src/trade-ledger.js +2 -2
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +220 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +18 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/analyst/backend.js +3 -2
- package/dist/packages/cli/src/analyst/codex.js +3 -2
- package/dist/packages/cli/src/app-help-gen.js +26 -2
- package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
- package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
- package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
- package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
- package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
- package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
- package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
- package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
- package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
- package/dist/packages/cli/src/autotrade-cmd.js +50 -4
- package/dist/packages/cli/src/classifier-cmd.js +353 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/ledger-cmd.js +4 -2
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/pnl-card.js +4 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
- package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
- package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
- package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/telegram-cmd.js +4 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/data/src/trade/ledger/schema.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/autotrade-tools.js +168 -5
- package/dist/packages/mcp/src/engine/restrictions.js +3 -3
- package/dist/packages/mcp/src/index.js +1 -1
- package/dist/packages/mcp/src/ledger-tools.js +155 -7
- package/dist/packages/mcp/src/server.js +125 -10
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +12 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// dexterm autotrade — the rug-proof probes (task-1748): the network half of rugproof.ts. READ-ONLY, PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// DexScreener tokens/v1 the coin's deepest pool: dex, base / quote mint, creation time, liquidity keyless
|
|
4
|
+
// Solana RPC the PumpSwap pool account (creator, lp mint, vaults), the LP mint's supply and
|
|
5
|
+
// largest holders, the pool's newest transactions (Withdraw scan), the SOL vault public or the user's own key
|
|
6
|
+
// Jupiter lite quote token -> SOL for a given raw amount keyless
|
|
7
|
+
//
|
|
8
|
+
// No wallet, no key of any wallet, no signing, no send: only reads. An RPC URL may carry an api key: it is never
|
|
9
|
+
// printed (redactSecrets) and never written to the ledger. Every read has a timeout; a read that fails is `error`
|
|
10
|
+
// and the gate that needed it refuses (fail closed). The vet runs the gates in rugproof.ts order and stops at the first
|
|
11
|
+
// refusal, so a coin refused by a free gate costs no network call.
|
|
12
|
+
import { redactSecrets, base58 } from '../../data/src/index.js';
|
|
13
|
+
import { PUMPSWAP_PROGRAM, WSOL_MINT, drainVerdict, gateHolders, gateLiquidityRecheck, gateLpLocked, gatePoolAge, gatePoolStructure, gateSellRoute, gateWithdrawHistory, verdictOf, } from "./rugproof.js";
|
|
14
|
+
export const defaultFetchJson = (timeoutMs = 12_000) => async (url, init) => {
|
|
15
|
+
const r = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
16
|
+
let json = null;
|
|
17
|
+
try {
|
|
18
|
+
json = await r.json();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
json = null;
|
|
22
|
+
}
|
|
23
|
+
return { status: r.status, json };
|
|
24
|
+
};
|
|
25
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
|
+
const isObj = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
|
27
|
+
const str = (x) => (typeof x === 'string' && x ? x : null);
|
|
28
|
+
const fin = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
29
|
+
export class RugProbe {
|
|
30
|
+
id = 0;
|
|
31
|
+
d;
|
|
32
|
+
/** Per-run cache: a pool's structure does not change, its LP and history are re-read at most every 10 min. */
|
|
33
|
+
lpCache = new Map();
|
|
34
|
+
decimals = new Map();
|
|
35
|
+
rpcCalls = 0;
|
|
36
|
+
/** Backoff unit after a rate limit (ms); tests set 0. */
|
|
37
|
+
backoffMs;
|
|
38
|
+
constructor(d) { this.d = d; this.backoffMs = d.backoffMs ?? 1500; }
|
|
39
|
+
async rpc(method, params) {
|
|
40
|
+
if (!this.d.rpcUrl)
|
|
41
|
+
throw new Error('no RPC endpoint');
|
|
42
|
+
let r = { status: 0, json: null };
|
|
43
|
+
// A rate limit (HTTP 429 or JSON-RPC -32429) is retried twice with a backoff before the read counts as failed.
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
45
|
+
this.rpcCalls++;
|
|
46
|
+
try {
|
|
47
|
+
r = await this.d.fetchJson(this.d.rpcUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++this.id, method, params }) });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(redactSecrets(`rpc ${method}: ${e.message}`).slice(0, 200));
|
|
51
|
+
}
|
|
52
|
+
const limited = r.status === 429 || (isObj(r.json) && isObj(r.json.error) && r.json.error.code === -32429);
|
|
53
|
+
if (!limited || attempt === 2)
|
|
54
|
+
break;
|
|
55
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
56
|
+
}
|
|
57
|
+
if (!isObj(r.json))
|
|
58
|
+
throw new Error(`rpc ${method}: HTTP ${r.status}`);
|
|
59
|
+
if (r.json.error)
|
|
60
|
+
throw new Error(redactSecrets(`rpc ${method}: ${JSON.stringify(r.json.error)}`).slice(0, 200));
|
|
61
|
+
return r.json.result;
|
|
62
|
+
}
|
|
63
|
+
pairCache = new Map();
|
|
64
|
+
scanCache = new Map();
|
|
65
|
+
/** The coin's deepest DexScreener pool (shared for 30 s: every fleet member asks about the same coin). */
|
|
66
|
+
pair(mint, now = Date.now()) {
|
|
67
|
+
const hit = this.pairCache.get(mint);
|
|
68
|
+
if (hit && now - hit.t < 30_000)
|
|
69
|
+
return hit.v;
|
|
70
|
+
const v = this.readPair(mint);
|
|
71
|
+
this.pairCache.set(mint, { t: now, v });
|
|
72
|
+
v.catch(() => this.pairCache.delete(mint));
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
async readPair(mint) {
|
|
76
|
+
const r = await this.d.fetchJson(`${this.d.dexscreenerBase ?? 'https://api.dexscreener.com'}/tokens/v1/solana/${mint}`);
|
|
77
|
+
if (!Array.isArray(r.json))
|
|
78
|
+
throw new Error(`dexscreener HTTP ${r.status}`);
|
|
79
|
+
const pairs = r.json.filter((p) => isObj(p) && str(p.pairAddress)).sort((a, b) => (fin(b.liquidity?.usd) ?? 0) - (fin(a.liquidity?.usd) ?? 0));
|
|
80
|
+
const p = pairs[0];
|
|
81
|
+
if (!p)
|
|
82
|
+
return null;
|
|
83
|
+
return { address: p.pairAddress, dex: str(p.dexId) ?? 'unknown', baseMint: str(p.baseToken?.address) ?? '', quoteMint: str(p.quoteToken?.address) ?? '', createdAt: fin(p.pairCreatedAt), liqUsd: fin(p.liquidity?.usd) };
|
|
84
|
+
}
|
|
85
|
+
/** The PumpSwap pool account + its LP mint (supply, largest holders' owners). null = not a PumpSwap account. */
|
|
86
|
+
async poolLp(pool, now = Date.now()) {
|
|
87
|
+
const hit = this.lpCache.get(pool);
|
|
88
|
+
if (hit && now - hit.t < 10 * 60e3 && hit.v !== 'error')
|
|
89
|
+
return hit.v;
|
|
90
|
+
let v;
|
|
91
|
+
try {
|
|
92
|
+
v = await this.readPoolLp(pool);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
v = 'error';
|
|
96
|
+
}
|
|
97
|
+
this.lpCache.set(pool, { t: now, v });
|
|
98
|
+
return v;
|
|
99
|
+
}
|
|
100
|
+
async readPoolLp(pool) {
|
|
101
|
+
const acc = await this.rpc('getAccountInfo', [pool, { encoding: 'base64' }]);
|
|
102
|
+
const value = isObj(acc) ? acc.value : null;
|
|
103
|
+
if (!value)
|
|
104
|
+
return null;
|
|
105
|
+
if (value.owner !== PUMPSWAP_PROGRAM)
|
|
106
|
+
return null;
|
|
107
|
+
const data = Buffer.from(String(value.data[0] ?? ''), 'base64');
|
|
108
|
+
const lp = decodePumpSwapPool(data);
|
|
109
|
+
const sup = await this.rpc('getTokenSupply', [lp.lpMint]);
|
|
110
|
+
const lpSupply = BigInt(String((sup?.value?.amount) ?? '0'));
|
|
111
|
+
let holders = [];
|
|
112
|
+
if (lpSupply > 0n) {
|
|
113
|
+
const big = await this.rpc('getTokenLargestAccounts', [lp.lpMint]);
|
|
114
|
+
const rows = ((big?.value ?? []).filter(isObj)).slice(0, 5);
|
|
115
|
+
const accs = rows.length ? await this.rpc('getMultipleAccounts', [rows.map((x) => x.address), { encoding: 'jsonParsed' }]) : null;
|
|
116
|
+
const vals = accs?.value ?? [];
|
|
117
|
+
holders = rows.map((x, i) => {
|
|
118
|
+
const owner = str(vals[i]?.data?.parsed?.info?.owner) ?? 'unknown';
|
|
119
|
+
return { owner, amount: BigInt(String(x.amount ?? '0')) };
|
|
120
|
+
}).sort((a, b) => (b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0));
|
|
121
|
+
}
|
|
122
|
+
return { ...lp, lpSupply, holders };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `Withdraw` instructions among the pool's newest `n` successful transactions (and how many the creator signed).
|
|
126
|
+
* Fails closed: one transaction that cannot be read (after the rpc's rate-limit retries) could be the creator's
|
|
127
|
+
* Withdraw, so any unread one makes the whole scan `error` (the gate refuses, the vet looks again in 5 min).
|
|
128
|
+
*/
|
|
129
|
+
async withdraws(pool, creator, n, now = Date.now()) {
|
|
130
|
+
const hit = this.scanCache.get(pool);
|
|
131
|
+
if (hit && now - hit.t < 10 * 60e3)
|
|
132
|
+
return hit.v;
|
|
133
|
+
const v = await this.scanWithdraws(pool, creator, n);
|
|
134
|
+
if (v !== 'error')
|
|
135
|
+
this.scanCache.set(pool, { t: now, v });
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
async scanWithdraws(pool, creator, n) {
|
|
139
|
+
try {
|
|
140
|
+
const sigs = await this.rpc('getSignaturesForAddress', [pool, { limit: Math.min(100, n * 2) }]);
|
|
141
|
+
const ok = (Array.isArray(sigs) ? sigs : []).filter((x) => isObj(x) && !x.err).slice(0, n);
|
|
142
|
+
let withdraws = 0;
|
|
143
|
+
let byCreator = 0;
|
|
144
|
+
let scanned = 0;
|
|
145
|
+
for (const s of ok) {
|
|
146
|
+
// maxSupportedTransactionVersion 1: Helius answers -32015 for a version-1 transaction under 0 (task-1394).
|
|
147
|
+
let tx = null;
|
|
148
|
+
try {
|
|
149
|
+
tx = await this.rpc('getTransaction', [s.signature, { encoding: 'jsonParsed', maxSupportedTransactionVersion: 1 }]);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return 'error';
|
|
153
|
+
}
|
|
154
|
+
if (!isObj(tx))
|
|
155
|
+
return 'error';
|
|
156
|
+
scanned++;
|
|
157
|
+
const logs = tx.meta?.logMessages ?? [];
|
|
158
|
+
if (!logs.some((l) => l === 'Program log: Instruction: Withdraw'))
|
|
159
|
+
continue;
|
|
160
|
+
withdraws++;
|
|
161
|
+
const keys = tx.transaction?.message?.accountKeys ?? [];
|
|
162
|
+
const signer = keys.map((k) => (isObj(k) ? k.pubkey : k))[0];
|
|
163
|
+
if (signer === creator)
|
|
164
|
+
byCreator++;
|
|
165
|
+
}
|
|
166
|
+
return { scanned, withdraws, byCreator };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return 'error';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async tokenDecimals(mint) {
|
|
173
|
+
const hit = this.decimals.get(mint);
|
|
174
|
+
if (hit !== undefined)
|
|
175
|
+
return hit;
|
|
176
|
+
try {
|
|
177
|
+
const sup = await this.rpc('getTokenSupply', [mint]);
|
|
178
|
+
const d = fin(sup?.value?.decimals);
|
|
179
|
+
if (d !== null) {
|
|
180
|
+
this.decimals.set(mint, d);
|
|
181
|
+
return d;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* below */ }
|
|
185
|
+
// pump.fun mints are 6 decimals by construction.
|
|
186
|
+
return mint.endsWith('pump') ? 6 : null;
|
|
187
|
+
}
|
|
188
|
+
/** Jupiter token -> SOL quote for `raw` units. HTTP 429 / a network failure is not a quote error. */
|
|
189
|
+
async route(mint, raw) {
|
|
190
|
+
if (raw <= 0n)
|
|
191
|
+
return { ok: false, kind: 'no-route', error: 'zero amount' };
|
|
192
|
+
let r = { status: 0, json: null };
|
|
193
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
r = await this.d.fetchJson(`${this.d.jupiterBase ?? 'https://lite-api.jup.ag'}/swap/v1/quote?inputMint=${mint}&outputMint=${WSOL_MINT}&amount=${raw}&slippageBps=1000`);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return { ok: false, kind: 'network', error: e.message.slice(0, 120) };
|
|
199
|
+
}
|
|
200
|
+
if (r.status !== 429 || attempt === 2)
|
|
201
|
+
break;
|
|
202
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
203
|
+
}
|
|
204
|
+
if (r.status === 429)
|
|
205
|
+
return { ok: false, kind: 'rate-limited', error: 'HTTP 429' };
|
|
206
|
+
const j = isObj(r.json) ? r.json : {};
|
|
207
|
+
if (str(j.outAmount) && !j.errorCode && !j.error)
|
|
208
|
+
return { ok: true, outLamports: BigInt(j.outAmount), impactPct: fin(Number(j.priceImpactPct)) === null ? null : Math.round(Number(j.priceImpactPct) * 10000) / 100 };
|
|
209
|
+
if (r.status >= 500 && !j.errorCode)
|
|
210
|
+
return { ok: false, kind: 'network', error: `HTTP ${r.status}` };
|
|
211
|
+
return { ok: false, kind: 'no-route', error: String(j.errorCode ?? j.error ?? `HTTP ${r.status}`).slice(0, 120) };
|
|
212
|
+
}
|
|
213
|
+
/** The pool's SOL vault balance (SOL). */
|
|
214
|
+
async vaultSol(vault) {
|
|
215
|
+
try {
|
|
216
|
+
const b = await this.rpc('getTokenAccountBalance', [vault]);
|
|
217
|
+
const v = b?.value ?? {};
|
|
218
|
+
return fin(v.uiAmount) ?? (str(v.amount) ? Number(v.amount) / 1e9 : null);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** PumpSwap `Pool`: disc 8, bump u8, index u16, creator, base_mint, quote_mint, lp_mint, base vault, quote vault, lp_supply u64, coin_creator. */
|
|
226
|
+
export function decodePumpSwapPool(d) {
|
|
227
|
+
if (d.length < 211)
|
|
228
|
+
throw new Error(`PumpSwap pool account too short (${d.length} bytes)`);
|
|
229
|
+
const key = (o) => base58(d.subarray(o, o + 32));
|
|
230
|
+
return { creator: key(11), baseMint: key(43), quoteMint: key(75), lpMint: key(107), baseVault: key(139), quoteVault: key(171) };
|
|
231
|
+
}
|
|
232
|
+
// ───────────────────────────── the vet ─────────────────────────────
|
|
233
|
+
export const UNKNOWN_RETRY_MS = 5 * 60e3;
|
|
234
|
+
/**
|
|
235
|
+
* The strategy's rug-proof gates as a runner vet: free gates first (tick + history), then the pair, the pool account,
|
|
236
|
+
* the withdraw scan and the Jupiter route. The first refusal ends it; a time gate asks to be retried.
|
|
237
|
+
*/
|
|
238
|
+
export function rugproofVet(r, o) {
|
|
239
|
+
return async (k, ctx) => {
|
|
240
|
+
const results = [];
|
|
241
|
+
const now = k.t;
|
|
242
|
+
const done = (x) => {
|
|
243
|
+
results.push(x);
|
|
244
|
+
if (x.pass)
|
|
245
|
+
return null;
|
|
246
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, gate: x.gate, rule: x.rule, detail: x.detail, results: results.map((y) => `${y.gate}:${y.pass ? 'ok' : 'no'}`) });
|
|
247
|
+
// A read that failed (`-unknown`: a rate limit, an RPC outage) refuses now (fail closed) and looks again in 5 min,
|
|
248
|
+
// instead of skipping the coin for the rest of the run; a structural refusal is for good.
|
|
249
|
+
const retry = x.retryAfterMs ?? (x.rule.endsWith('-unknown') ? UNKNOWN_RETRY_MS : undefined);
|
|
250
|
+
return { ok: false, rule: x.rule, detail: x.detail, ...(retry !== undefined ? { retryAfterMs: retry } : {}) };
|
|
251
|
+
};
|
|
252
|
+
const history = ctx?.history ?? [];
|
|
253
|
+
const sizeUsd = ctx?.sizeUsd ?? 0;
|
|
254
|
+
const floor = ctx?.exitFloorUsd ?? 2000;
|
|
255
|
+
// 1-3: free.
|
|
256
|
+
let f = done(gatePoolAge(r, k, undefined, now));
|
|
257
|
+
if (f)
|
|
258
|
+
return f;
|
|
259
|
+
f = done(gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders));
|
|
260
|
+
if (f)
|
|
261
|
+
return f;
|
|
262
|
+
f = done(gateLiquidityRecheck(r, history, k, floor));
|
|
263
|
+
if (f)
|
|
264
|
+
return f;
|
|
265
|
+
// 4: the pool as listed.
|
|
266
|
+
let pair = null;
|
|
267
|
+
let pairErr = null;
|
|
268
|
+
try {
|
|
269
|
+
pair = await o.probe.pair(k.mint);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
pairErr = e.message.slice(0, 160);
|
|
273
|
+
}
|
|
274
|
+
results.shift(); // the token-age pass above is replaced by the pool's own age
|
|
275
|
+
if (pairErr !== null)
|
|
276
|
+
return done({ gate: 'pool-structure', pass: false, rule: 'rugproof:pool-structure-unknown', detail: `the pool could not be read: ${pairErr}` });
|
|
277
|
+
f = done(gatePoolAge(r, k, pair ? pair.createdAt : null, now));
|
|
278
|
+
if (f)
|
|
279
|
+
return f;
|
|
280
|
+
f = done(gatePoolStructure(r, k.mint, k.launchpad, pair));
|
|
281
|
+
if (f)
|
|
282
|
+
return f;
|
|
283
|
+
// 5-6: the chain.
|
|
284
|
+
let lp = null;
|
|
285
|
+
if (r.requireSafePool && pair) {
|
|
286
|
+
lp = o.probe.d.rpcUrl ? await o.probe.poolLp(pair.address, now) : 'error';
|
|
287
|
+
f = done(gateLpLocked(r, pair, lp, k.mint));
|
|
288
|
+
if (f)
|
|
289
|
+
return f;
|
|
290
|
+
const poolLp = lp;
|
|
291
|
+
const scan = poolLp.lpSupply === 0n && r.maxWithdraws === 0 ? null : await o.probe.withdraws(pair.address, poolLp.creator, r.withdrawScanTx);
|
|
292
|
+
f = done(gateWithdrawHistory(r, scan, poolLp));
|
|
293
|
+
if (f)
|
|
294
|
+
return f;
|
|
295
|
+
}
|
|
296
|
+
// 7: a sell route at routeX x the size.
|
|
297
|
+
if (r.routeX !== null) {
|
|
298
|
+
const dec = await o.probe.tokenDecimals(k.mint);
|
|
299
|
+
let route = null;
|
|
300
|
+
if (dec !== null && k.price) {
|
|
301
|
+
const tokens = (sizeUsd * r.routeX) / k.price;
|
|
302
|
+
route = await o.probe.route(k.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** dec))));
|
|
303
|
+
}
|
|
304
|
+
f = done(gateSellRoute(r, route, sizeUsd));
|
|
305
|
+
if (f)
|
|
306
|
+
return f;
|
|
307
|
+
}
|
|
308
|
+
const v = verdictOf(r, results, pair?.createdAt ? now - pair.createdAt : null);
|
|
309
|
+
const note = `rugproof ok: ${results.map((x) => `${x.gate}${x.partial ? '~' : ''}`).join(', ')}${v.sizeCapPct !== null ? `; size cap ${v.sizeCapPct}% (young or partly verified pool)` : ''}`;
|
|
310
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, pass: true, sizeCapPct: v.sizeCapPct, results: results.map((x) => `${x.gate}:${x.partial ? 'partial' : 'ok'}`) });
|
|
311
|
+
return { ok: true, note, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}), ...(pair ? { pool: { address: pair.address, quoteVault: lp && lp !== 'error' ? lp.quoteVault : null } } : {}) };
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Polls every open position's pool (SOL vault when the pool is known, plus a Jupiter quote for the held amount)
|
|
316
|
+
* every pollSec, outside the candle / tick cadence. The runner reads `verdict()` on every step and sells the whole
|
|
317
|
+
* position on an exit verdict. Timers are unref'd: they never keep a process alive.
|
|
318
|
+
*/
|
|
319
|
+
export class DrainPoller {
|
|
320
|
+
w = new Map();
|
|
321
|
+
g;
|
|
322
|
+
probe;
|
|
323
|
+
log;
|
|
324
|
+
constructor(g, probe, log) { this.g = g; this.probe = probe; this.log = log; }
|
|
325
|
+
watch(p) {
|
|
326
|
+
if (this.w.has(p.positionId))
|
|
327
|
+
return;
|
|
328
|
+
const x = { pos: p, base: { reserveSol: null, liqUsd: p.entryLiqUsd }, timer: null, verdict: null, reads: 0, quoteErrors: 0, noRoute: 0, busy: false, decimals: null };
|
|
329
|
+
this.w.set(p.positionId, x);
|
|
330
|
+
void this.poll(x, true);
|
|
331
|
+
x.timer = setInterval(() => { void this.poll(x, false); }, Math.max(2, Math.min(10, this.g.pollSec)) * 1000);
|
|
332
|
+
x.timer.unref?.();
|
|
333
|
+
}
|
|
334
|
+
unwatch(positionId) {
|
|
335
|
+
const x = this.w.get(positionId);
|
|
336
|
+
if (x?.timer)
|
|
337
|
+
clearInterval(x.timer);
|
|
338
|
+
this.w.delete(positionId);
|
|
339
|
+
}
|
|
340
|
+
verdict(positionId) {
|
|
341
|
+
const v = this.w.get(positionId)?.verdict ?? null;
|
|
342
|
+
return v && v.exit ? v : null;
|
|
343
|
+
}
|
|
344
|
+
stop() { for (const id of [...this.w.keys()])
|
|
345
|
+
this.unwatch(id); }
|
|
346
|
+
/** One probe of one position (also called by tests). */
|
|
347
|
+
async poll(x, first) {
|
|
348
|
+
if (x.busy)
|
|
349
|
+
return;
|
|
350
|
+
x.busy = true;
|
|
351
|
+
try {
|
|
352
|
+
let vault = x.pos.quoteVault ?? null;
|
|
353
|
+
if (!vault && x.pos.poolAddress && this.probe.d.rpcUrl) {
|
|
354
|
+
const lp = await this.probe.poolLp(x.pos.poolAddress);
|
|
355
|
+
if (lp && lp !== 'error')
|
|
356
|
+
vault = x.pos.quoteVault = lp.quoteVault;
|
|
357
|
+
}
|
|
358
|
+
const reserveSol = vault && this.probe.d.rpcUrl ? await this.probe.vaultSol(vault) : null;
|
|
359
|
+
if (first || x.base.reserveSol === null) {
|
|
360
|
+
if (reserveSol !== null)
|
|
361
|
+
x.base.reserveSol = reserveSol;
|
|
362
|
+
}
|
|
363
|
+
x.decimals ??= await this.probe.tokenDecimals(x.pos.mint);
|
|
364
|
+
const tokens = x.pos.tokens();
|
|
365
|
+
const route = this.g.exitOnNoRoute && x.decimals !== null && tokens > 0 ? await this.probe.route(x.pos.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** x.decimals)))) : null;
|
|
366
|
+
x.reads++;
|
|
367
|
+
if (route && !route.ok && route.kind !== 'no-route')
|
|
368
|
+
x.quoteErrors++;
|
|
369
|
+
let v = drainVerdict(this.g, x.base, { t: Date.now(), reserveSol, liqUsd: null, route });
|
|
370
|
+
// A quote error alone needs two reads in a row (one pollSec apart): a single odd answer is not a pull. A reserve
|
|
371
|
+
// read decides at once. The poller keeps polling after an exit: a route that comes back lifts the verdict.
|
|
372
|
+
x.noRoute = v.exit && v.reason === 'drain-no-route' ? x.noRoute + 1 : 0;
|
|
373
|
+
if (v.exit && v.reason === 'drain-no-route' && x.noRoute < 2)
|
|
374
|
+
v = { exit: false, note: 'no route once: re-checking' };
|
|
375
|
+
const was = x.verdict?.exit ?? false;
|
|
376
|
+
x.verdict = v;
|
|
377
|
+
if (v.exit && !was)
|
|
378
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, drain: v.reason, detail: v.detail, reads: x.reads });
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, probeError: e.message.slice(0, 160) });
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
x.busy = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
stats() {
|
|
388
|
+
let reads = 0;
|
|
389
|
+
let quoteErrors = 0;
|
|
390
|
+
for (const x of this.w.values()) {
|
|
391
|
+
reads += x.reads;
|
|
392
|
+
quoteErrors += x.quoteErrors;
|
|
393
|
+
}
|
|
394
|
+
return { watched: this.w.size, reads, quoteErrors };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** Run vets in order (the rug-proof gates first: they are the cheapest); the first refusal wins; notes and caps merge. */
|
|
398
|
+
export function chainVets(...vets) {
|
|
399
|
+
const vs = vets.filter((v) => !!v);
|
|
400
|
+
if (vs.length <= 1)
|
|
401
|
+
return vs[0];
|
|
402
|
+
return async (k, ctx) => {
|
|
403
|
+
const notes = [];
|
|
404
|
+
let out = { ok: true };
|
|
405
|
+
for (const v of vs) {
|
|
406
|
+
const r = await v(k, ctx);
|
|
407
|
+
if (!r.ok)
|
|
408
|
+
return r;
|
|
409
|
+
if (r.note)
|
|
410
|
+
notes.push(r.note);
|
|
411
|
+
out = { ...out, ...r, recheck: !!(out.ok && out.recheck) || !!r.recheck, ...(out.ok && out.sizeCapPct !== undefined && (r.sizeCapPct === undefined || out.sizeCapPct < r.sizeCapPct) ? { sizeCapPct: out.sizeCapPct } : {}) };
|
|
412
|
+
}
|
|
413
|
+
return notes.length ? { ...out, note: notes.join('; ') } : out;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// dexterm autotrade — rug-proofing (task-1748): who can pull the pool, and a fast exit when it starts to go. PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// The control's two 2026-10-26 losses ($NOTHING, $AMERICA) and the three on 10-27 were creator LP pulls on hand-made
|
|
4
|
+
// PumpSwap pools: WSOL as the pool's BASE mint, launchpad unknown, the mint not ending in `pump`, the creator holding the
|
|
5
|
+
// LP tokens (FINDINGS-2026-10-27 §1-2). A pull is ONE transaction with no build-up: every post-entry signal (the pool's
|
|
6
|
+
// SOL vault, a Jupiter quote, DexScreener liquidity) fires only after it. So the PRIMARY defense is pre-entry and
|
|
7
|
+
// structural; the drain guard below only shortens the time a position sits in a pool that is being emptied and makes
|
|
8
|
+
// the book honest. A same-block creator pull cannot be front-run by anyone reading the chain.
|
|
9
|
+
//
|
|
10
|
+
// Pre-entry gates, cheapest first, fail closed (a gate that cannot be evaluated refuses the coin with `-unknown`):
|
|
11
|
+
// pool-age pool (pair) age >= minPoolAgeMin. Token age is a free lower bound first (a token younger
|
|
12
|
+
// than the minimum has no older pool); the pair's own creation time decides. tick, pair
|
|
13
|
+
// holders top-10 / dev share under the strategy's safety values (unknown = refuse) tick
|
|
14
|
+
// liquidity-recheck two liquidity reads >= afterSec apart: both >= floorX x the exit floor, the newer
|
|
15
|
+
// not more than maxDropPct under the older history
|
|
16
|
+
// pool-structure the pool is a pump.fun migration (PumpSwap, token = base, WSOL = quote) or a pool
|
|
17
|
+
// whose LP is verifiably burned / locked; WSOL as the BASE mint = hand-made: refuse pair
|
|
18
|
+
// lp-locked PumpSwap pool account -> lp mint: supply 0 (burned) passes; supply > 0 with the
|
|
19
|
+
// largest holder the pool creator (or any wallet over maxHolderLpPct) refuses RPC
|
|
20
|
+
// withdraw-history no `Withdraw` instruction among the pool's newest transactions (bounded scan) RPC
|
|
21
|
+
// sell-route a Jupiter token -> SOL quote at routeX x the position size returns a route Jupiter
|
|
22
|
+
// Every result has a named rule (`rugproof:<gate>[-why]`) that lands in the ledger; time gates (age, recheck) say
|
|
23
|
+
// when to look again instead of skipping the coin for good.
|
|
24
|
+
//
|
|
25
|
+
// Post-entry (drainVerdict): exit the whole position at once on liquidity / reserve down > liqDropPct from the entry
|
|
26
|
+
// read, a reserve near 0, or ANY Jupiter quote error (NO_ROUTES_FOUND, CANNOT_COMPUTE_…). A rate limit or a network
|
|
27
|
+
// failure is not a quote error (no exit on it; it is counted). Young or partly-verified pools get a smaller size
|
|
28
|
+
// (youngPoolSizeCapUsd).
|
|
29
|
+
//
|
|
30
|
+
// Pure: no I/O. The probes (DexScreener pair, PumpSwap pool account, LP mint, signatures, Jupiter) are in rugproof-probe.ts.
|
|
31
|
+
export const WSOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
32
|
+
export const PUMPSWAP_PROGRAM = 'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA';
|
|
33
|
+
export const RUGPROOF_DEFAULTS = {
|
|
34
|
+
minPoolAgeMin: 10,
|
|
35
|
+
requireSafePool: true,
|
|
36
|
+
maxHolderLpPct: 10,
|
|
37
|
+
maxWithdraws: 0,
|
|
38
|
+
withdrawScanTx: 25,
|
|
39
|
+
liquidityRecheck: { afterSec: 60, floorX: 2, maxDropPct: 15 },
|
|
40
|
+
routeX: 2,
|
|
41
|
+
youngPool: { underMin: 30, maxSizePct: 50 },
|
|
42
|
+
drainGuard: { pollSec: 10, liqDropPct: 25, exitOnNoRoute: true, minReserveSol: 0.5 },
|
|
43
|
+
};
|
|
44
|
+
export const GATE_ORDER = ['pool-age', 'holders', 'liquidity-recheck', 'pool-structure', 'lp-locked', 'withdraw-history', 'sell-route'];
|
|
45
|
+
const pass = (gate, detail, partial = false) => ({ gate, pass: true, rule: `rugproof:${gate}`, detail, ...(partial ? { partial: true } : {}) });
|
|
46
|
+
const fail = (gate, why, detail, retryAfterMs) => ({ gate, pass: false, rule: `rugproof:${gate}${why ? `-${why}` : ''}`, detail, ...(retryAfterMs !== undefined ? { retryAfterMs: Math.max(1000, Math.round(retryAfterMs)) } : {}) });
|
|
47
|
+
const m = (ms) => `${(ms / 60e3).toFixed(1)} min`;
|
|
48
|
+
const usd = (x) => `$${Math.round(x).toLocaleString('en-US')}`;
|
|
49
|
+
/** A pump.fun mint (vanity suffix) or Jupiter's launchpad tag says pump.fun. */
|
|
50
|
+
export const isPumpMint = (mint, launchpad) => mint.endsWith('pump') || launchpad === 'pump' || launchpad === 'pump.fun';
|
|
51
|
+
// ───────────────────────────── free gates (tick + history) ─────────────────────────────
|
|
52
|
+
/** Token age is a lower bound on pool age; the pair's creation time (when known) decides. */
|
|
53
|
+
export function gatePoolAge(r, k, pairCreatedAt, now = k.t) {
|
|
54
|
+
const min = r.minPoolAgeMin * 60e3;
|
|
55
|
+
if (k.createdAt !== null && now - k.createdAt < min)
|
|
56
|
+
return fail('pool-age', null, `token age ${m(now - k.createdAt)} < ${r.minPoolAgeMin} min (no older pool can exist)`, min - (now - k.createdAt));
|
|
57
|
+
if (pairCreatedAt === undefined)
|
|
58
|
+
return pass('pool-age', 'token age ok; pool age not read yet', true);
|
|
59
|
+
if (pairCreatedAt === null)
|
|
60
|
+
return fail('pool-age', 'unknown', 'the pool has no creation time');
|
|
61
|
+
const age = now - pairCreatedAt;
|
|
62
|
+
if (age < min)
|
|
63
|
+
return fail('pool-age', null, `pool age ${m(age)} < ${r.minPoolAgeMin} min`, min - age);
|
|
64
|
+
return pass('pool-age', `pool age ${m(age)}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The strategy's own holder values (entry.safety, the GUARDRAILS numbers), with its own unknown rules: an unknown top-10
|
|
68
|
+
* share refuses unless unknownHolders is allow; an unknown dev share passes, as the engine's screen has always done (the
|
|
69
|
+
* holder sources rarely report it: refusing it would refuse nearly every coin), and is marked partial.
|
|
70
|
+
*/
|
|
71
|
+
export function gateHolders(k, maxTop10Pct, maxDevPct, unknownHolders = 'reject') {
|
|
72
|
+
let partial = false;
|
|
73
|
+
if (maxTop10Pct !== null) {
|
|
74
|
+
if (k.top10 === null) {
|
|
75
|
+
if (unknownHolders === 'reject')
|
|
76
|
+
return fail('holders', 'unknown', 'top-10 share unknown');
|
|
77
|
+
partial = true;
|
|
78
|
+
}
|
|
79
|
+
else if (k.top10 > maxTop10Pct)
|
|
80
|
+
return fail('holders', 'top10', `top 10 hold ${k.top10.toFixed(1)}% > ${maxTop10Pct}%`);
|
|
81
|
+
}
|
|
82
|
+
if (maxDevPct !== null && k.dev !== null && k.dev > maxDevPct)
|
|
83
|
+
return fail('holders', 'dev', `dev holds ${k.dev.toFixed(1)}% > ${maxDevPct}%`);
|
|
84
|
+
return pass('holders', `top10 ${k.top10 ?? '?'}%, dev ${k.dev ?? 'unknown'}%`, partial);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Two liquidity reads at least afterSec apart (the newest and the newest one at or before now - afterSec, from the
|
|
88
|
+
* coin's own tick history): both >= floorX x the exit floor, the newer not more than maxDropPct under the older.
|
|
89
|
+
*/
|
|
90
|
+
export function gateLiquidityRecheck(r, history, k, exitFloorUsd) {
|
|
91
|
+
const rc = r.liquidityRecheck;
|
|
92
|
+
if (!rc)
|
|
93
|
+
return pass('liquidity-recheck', 'not required');
|
|
94
|
+
const floor = rc.floorX * exitFloorUsd;
|
|
95
|
+
const liqNow = k.liq;
|
|
96
|
+
if (liqNow === null)
|
|
97
|
+
return fail('liquidity-recheck', 'unknown', 'no liquidity read');
|
|
98
|
+
if (liqNow < floor)
|
|
99
|
+
return fail('liquidity-recheck', 'floor', `liquidity ${usd(liqNow)} < ${rc.floorX}x the ${usd(exitFloorUsd)} exit floor`);
|
|
100
|
+
const cutoff = k.t - rc.afterSec * 1000;
|
|
101
|
+
let older = null;
|
|
102
|
+
for (const h of history)
|
|
103
|
+
if (h.mint === k.mint && h.t <= cutoff && h.liq !== null && (!older || h.t > older.t))
|
|
104
|
+
older = h;
|
|
105
|
+
if (!older) {
|
|
106
|
+
const first = history.find((h) => h.mint === k.mint && h.liq !== null);
|
|
107
|
+
const wait = first ? first.t + rc.afterSec * 1000 - k.t : rc.afterSec * 1000;
|
|
108
|
+
return fail('liquidity-recheck', 'pending', `no read ${rc.afterSec} s before this one yet`, wait > 0 ? wait : rc.afterSec * 1000);
|
|
109
|
+
}
|
|
110
|
+
if (older.liq < floor)
|
|
111
|
+
return fail('liquidity-recheck', 'floor', `liquidity ${usd(older.liq)} ${Math.round((k.t - older.t) / 1000)} s ago < ${rc.floorX}x the exit floor`);
|
|
112
|
+
const drop = (older.liq - liqNow) / older.liq * 100;
|
|
113
|
+
if (drop > rc.maxDropPct)
|
|
114
|
+
return fail('liquidity-recheck', 'falling', `liquidity ${usd(older.liq)} -> ${usd(liqNow)} in ${Math.round((k.t - older.t) / 1000)} s (-${drop.toFixed(1)}% > ${rc.maxDropPct}%)`);
|
|
115
|
+
return pass('liquidity-recheck', `${usd(older.liq)} -> ${usd(liqNow)} over ${Math.round((k.t - older.t) / 1000)} s`);
|
|
116
|
+
}
|
|
117
|
+
/** Owners that hold LP tokens without being able to withdraw them (burn / incinerator). Locks are not trusted by name. */
|
|
118
|
+
export const BURN_OWNERS = new Set(['1nc1nerator11111111111111111111111111111111', '11111111111111111111111111111111']);
|
|
119
|
+
export function gatePoolStructure(r, mint, launchpad, pair) {
|
|
120
|
+
if (!r.requireSafePool)
|
|
121
|
+
return pass('pool-structure', 'not required');
|
|
122
|
+
if (!pair)
|
|
123
|
+
return fail('pool-structure', 'unknown', 'no pool listed for the coin');
|
|
124
|
+
if (pair.baseMint === WSOL_MINT)
|
|
125
|
+
return fail('pool-structure', 'wsol-base', `WSOL is the pool's base mint (${pair.dex} ${pair.address.slice(0, 8)}…): a hand-made pool, not a pump.fun migration`);
|
|
126
|
+
if (pair.baseMint !== mint)
|
|
127
|
+
return fail('pool-structure', 'not-base', `the coin is not the pool's base mint (${pair.dex})`);
|
|
128
|
+
if (pair.quoteMint !== WSOL_MINT)
|
|
129
|
+
return fail('pool-structure', 'quote', `the pool's quote is not WSOL (${pair.quoteMint.slice(0, 8)}…)`);
|
|
130
|
+
const dex = pair.dex.toLowerCase();
|
|
131
|
+
if (dex === 'pumpswap') {
|
|
132
|
+
return isPumpMint(mint, launchpad)
|
|
133
|
+
? pass('pool-structure', 'pump.fun migration shape (PumpSwap, token = base, WSOL = quote)')
|
|
134
|
+
: pass('pool-structure', `PumpSwap, token = base, WSOL = quote, launchpad ${launchpad ?? 'unknown'}: LP must be verified`, true);
|
|
135
|
+
}
|
|
136
|
+
// Other venues (Raydium, Meteora, …): the LP lock cannot be verified here, so it is refused (fail closed).
|
|
137
|
+
return fail('pool-structure', 'unverifiable', `${pair.dex} pool: its LP lock cannot be verified by dexterm (only PumpSwap pools are read)`);
|
|
138
|
+
}
|
|
139
|
+
export function gateLpLocked(r, pair, lp, mint) {
|
|
140
|
+
if (!r.requireSafePool)
|
|
141
|
+
return pass('lp-locked', 'not required');
|
|
142
|
+
if (lp === 'error' || lp === null)
|
|
143
|
+
return fail('lp-locked', 'unknown', `the PumpSwap pool account ${pair.address.slice(0, 8)}… could not be read`);
|
|
144
|
+
if (lp.baseMint === WSOL_MINT)
|
|
145
|
+
return fail('lp-locked', 'wsol-base', 'the pool account says WSOL is the base mint (hand-made pool)');
|
|
146
|
+
if (lp.baseMint !== mint || lp.quoteMint !== WSOL_MINT)
|
|
147
|
+
return fail('lp-locked', 'mismatch', 'the pool account does not match the listed pair');
|
|
148
|
+
if (lp.lpSupply === 0n)
|
|
149
|
+
return pass('lp-locked', 'LP supply 0: every LP token is burned, nobody can withdraw');
|
|
150
|
+
const live = lp.holders.filter((h) => !BURN_OWNERS.has(h.owner) && h.amount > 0n);
|
|
151
|
+
const share = (a) => Number((a * 1000000n) / lp.lpSupply) / 10_000;
|
|
152
|
+
const byCreator = live.find((h) => h.owner === lp.creator);
|
|
153
|
+
if (byCreator)
|
|
154
|
+
return fail('lp-locked', 'creator', `the pool creator ${lp.creator.slice(0, 8)}… holds ${share(byCreator.amount).toFixed(2)}% of the LP: it can withdraw at any time`);
|
|
155
|
+
const top = live[0];
|
|
156
|
+
if (top && share(top.amount) > r.maxHolderLpPct)
|
|
157
|
+
return fail('lp-locked', 'holder', `one wallet ${top.owner.slice(0, 8)}… holds ${share(top.amount).toFixed(2)}% of the LP (> ${r.maxHolderLpPct}%)`);
|
|
158
|
+
if (!lp.holders.length)
|
|
159
|
+
return fail('lp-locked', 'unknown', 'LP supply > 0 but the holder list could not be read');
|
|
160
|
+
return pass('lp-locked', `LP supply > 0, largest live holder ${top ? share(top.amount).toFixed(2) : 0}%`, true);
|
|
161
|
+
}
|
|
162
|
+
export function gateWithdrawHistory(r, scan, lp) {
|
|
163
|
+
if (lp && lp.lpSupply === 0n && r.maxWithdraws === 0 && scan === null)
|
|
164
|
+
return pass('withdraw-history', 'LP burned: a withdraw is impossible');
|
|
165
|
+
if (scan === 'error' || scan === null)
|
|
166
|
+
return fail('withdraw-history', 'unknown', 'the pool\'s transactions could not be read');
|
|
167
|
+
if (scan.byCreator > 0)
|
|
168
|
+
return fail('withdraw-history', 'creator', `${scan.byCreator} creator Withdraw among the pool's newest ${scan.scanned} transactions`);
|
|
169
|
+
if (scan.withdraws > r.maxWithdraws)
|
|
170
|
+
return fail('withdraw-history', null, `${scan.withdraws} Withdraw among the pool's newest ${scan.scanned} transactions (max ${r.maxWithdraws})`);
|
|
171
|
+
return pass('withdraw-history', `no Withdraw among the newest ${scan.scanned} transactions`, scan.scanned < Math.min(5, r.withdrawScanTx));
|
|
172
|
+
}
|
|
173
|
+
export function gateSellRoute(r, route, sizeUsd) {
|
|
174
|
+
if (r.routeX === null)
|
|
175
|
+
return pass('sell-route', 'not required');
|
|
176
|
+
if (!route)
|
|
177
|
+
return fail('sell-route', 'unknown', 'no sell quote');
|
|
178
|
+
if (!route.ok)
|
|
179
|
+
return fail('sell-route', route.kind === 'no-route' ? 'none' : 'unknown', `Jupiter token -> SOL at ${r.routeX}x the ${usd(sizeUsd)} size: ${route.error}`);
|
|
180
|
+
return pass('sell-route', `Jupiter routes ${r.routeX}x the size (impact ${route.impactPct ?? '?'}%)`);
|
|
181
|
+
}
|
|
182
|
+
export function verdictOf(r, results, poolAgeMs) {
|
|
183
|
+
const refused = results.find((x) => !x.pass) ?? null;
|
|
184
|
+
const partial = results.some((x) => x.partial);
|
|
185
|
+
const young = r.youngPool && (poolAgeMs === null || poolAgeMs < r.youngPool.underMin * 60e3);
|
|
186
|
+
return { pass: !refused, results, refused, sizeCapPct: !refused && r.youngPool && (partial || young) ? r.youngPool.maxSizePct : null };
|
|
187
|
+
}
|
|
188
|
+
/** Exit the whole position at once on a reserve near 0, a reserve / liquidity drop > liqDropPct, or a quote error. */
|
|
189
|
+
export function drainVerdict(g, base, read) {
|
|
190
|
+
const ratio = base.reserveSol && read.reserveSol !== null ? read.reserveSol / base.reserveSol : null;
|
|
191
|
+
const estLiq = ratio !== null && base.liqUsd !== null ? base.liqUsd * ratio : read.liqUsd;
|
|
192
|
+
if (read.reserveSol !== null && read.reserveSol < g.minReserveSol)
|
|
193
|
+
return { exit: true, reason: 'drain-reserve', detail: `pool SOL reserve ${read.reserveSol.toFixed(3)} < ${g.minReserveSol} SOL`, liqUsd: estLiq ?? 0 };
|
|
194
|
+
if (ratio !== null && (1 - ratio) * 100 > g.liqDropPct)
|
|
195
|
+
return { exit: true, reason: 'drain-liquidity', detail: `pool SOL reserve ${base.reserveSol.toFixed(2)} -> ${read.reserveSol.toFixed(2)} (-${((1 - ratio) * 100).toFixed(1)}% > ${g.liqDropPct}%)`, liqUsd: estLiq };
|
|
196
|
+
if (base.liqUsd && read.liqUsd !== null && (1 - read.liqUsd / base.liqUsd) * 100 > g.liqDropPct)
|
|
197
|
+
return { exit: true, reason: 'drain-liquidity', detail: `liquidity ${usd(base.liqUsd)} -> ${usd(read.liqUsd)} (-${((1 - read.liqUsd / base.liqUsd) * 100).toFixed(1)}% > ${g.liqDropPct}% since entry)`, liqUsd: read.liqUsd };
|
|
198
|
+
if (g.exitOnNoRoute && read.route && !read.route.ok && read.route.kind === 'no-route')
|
|
199
|
+
return { exit: true, reason: 'drain-no-route', detail: `Jupiter has no sell route: ${read.route.error}`, liqUsd: estLiq ?? 0 };
|
|
200
|
+
if (read.route && !read.route.ok)
|
|
201
|
+
return { exit: false, note: `quote ${read.route.kind}: ${read.route.error}` };
|
|
202
|
+
return { exit: false };
|
|
203
|
+
}
|
|
204
|
+
/** The size a young or partly-verified pool may take: maxSizePct of what the strategy picked. */
|
|
205
|
+
export function youngPoolSizeCapUsd(v, usdPicked) {
|
|
206
|
+
return v.sizeCapPct === null ? null : (usdPicked * v.sizeCapPct) / 100;
|
|
207
|
+
}
|
|
208
|
+
// ───────────────────────────── offline (replays, backtests) ─────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* The gates a recorded tape can evaluate: pool age (the tape's createdAt: the token's, else the pair's), holders, the
|
|
211
|
+
* second liquidity read (the coin's own recorded rows; the recorder's ~2-min cadence makes afterSec at least one row
|
|
212
|
+
* gap), and the pool-structure PROXY: a pump.fun mint (`…pump` / launchpad pump) passes, anything else is refused as
|
|
213
|
+
* `rugproof:pool-structure-offline` (the tape has no pool account, LP mint or transactions). Used by backtest() for a
|
|
214
|
+
* rug-proof strategy and by the control's replay.
|
|
215
|
+
*/
|
|
216
|
+
export function rugproofTapeVet(r) {
|
|
217
|
+
return async (k, ctx) => {
|
|
218
|
+
const res = [
|
|
219
|
+
gatePoolAge(r, k, k.createdAt, k.t),
|
|
220
|
+
gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders),
|
|
221
|
+
// the structural proxy is free on a tape: it goes before the second read (a refusal for good beats a wait)
|
|
222
|
+
!r.requireSafePool || isPumpMint(k.mint, k.launchpad) ? pass('pool-structure', 'pump.fun mint (offline proxy)') : fail('pool-structure', 'offline', `not a pump.fun mint (launchpad ${k.launchpad}): the tape cannot verify its LP`),
|
|
223
|
+
gateLiquidityRecheck(r, ctx?.history ?? [], k, ctx?.exitFloorUsd ?? 2000),
|
|
224
|
+
];
|
|
225
|
+
const bad = res.find((x) => !x.pass);
|
|
226
|
+
if (bad)
|
|
227
|
+
return { ok: false, rule: bad.rule, detail: bad.detail, ...(bad.retryAfterMs !== undefined ? { retryAfterMs: bad.retryAfterMs } : {}) };
|
|
228
|
+
const v = verdictOf(r, res, k.createdAt !== null ? k.t - k.createdAt : null);
|
|
229
|
+
return { ok: true, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}) };
|
|
230
|
+
};
|
|
231
|
+
}
|