hertzflow — Crypto trade-decision intelligence (single skill, multi-domain) is HertzFlow's umbrella skill for on-chain + CEX trading research. Like Surf, it's one skill with internal routing — read the table below to see which sub-domain handles the user's request, then follow that sub-domain's . 🌐 Language — MANDATORY, read first Detect the language of the user's most recent message and use that language for everything: your conversation replies AND the generated report. This is not optional and not a preference the user has to ask for. - Message contains CJK characters ( , kana, hangul) →…

|| {\n echo \"INSUFFICIENT_DATA: CA failed regex ^0x[a-fA-F0-9]{40}$\"; exit 1\n}\nCA_LOWER=$(echo \"$CA_INPUT\" | tr 'A-F' 'a-f')\n```\n\n### Windows runtime notes (PowerShell / ARM)\n\n**EN** — the pipeline runs unchanged on Windows, including Surface Laptop\nARM via Prism emulation, but PowerShell 5.x has three gotchas worth\ncalling out before you blame the pipeline:\n\n1. **`curl` is a PowerShell alias for `Invoke-WebRequest`.** Always use\n the bundled binary `curl.exe` (e.g. `curl.exe --version`); otherwise\n `--fail` and `-sSL` are silently reinterpreted and the surf installer\n one-liner is wrong.\n2. **Anything written to `stderr` triggers `NativeCommandError`** in\n PowerShell 5.x even on exit 0, so `forensic_pipeline.py` writes its\n banner to **`stdout` since v0.7.17**. Do **not** redirect with\n `2>&1 | Tee-Object` — it loses the per-stream split that the\n pipeline relies on. Use `python3 ... > skeleton.log 2> skeleton.err`\n or run inside `cmd.exe`.\n3. **Tail-section hangs leave the skeleton missing.** As of v0.7.17 the\n pipeline writes `.work/skeleton_partial.json` after every section\n plus `_status: in_progress` + `_last_section_completed`, so even a\n hard kill / SIGINT leaves a partial skeleton you can inspect. The\n slowest tail section (`section_wash_infra`) obeys\n `BINANCE_ALPHA_WASH_INFRA_MAX_SECONDS` (default 300s).\n **v0.7.18** parallelized Steps 2-5 of `wash_infra_detector` via an\n 8-way `ThreadPoolExecutor` so BEAT-sized tokens (70 candidates after\n Step 1 filter) drop from ~420s serial to ~50-60s parallel. Override\n the worker count via `BINANCE_ALPHA_WASH_INFRA_WORKERS` (default 8).\n On ARM-emulated Python you generally do NOT need to bump the budget\n any more; if you still see truncation, drop workers to 4 first\n (surf rate-limit, not wall-clock).\n\n4. **Chinese narrative in your fill step needs UTF-8 I/O.** Windows\n Python defaults to `cp936` for stdin/stdout when running under the\n classic Windows console / piped through tools that don't set the\n code page. Symptom: your fill script wrote\n `\"内幕方仍持 86.89% 总供应\"` but the file reads back as\n `\"???????? 86.89% ????\"` and `render_report.py` either crashes on\n non-UTF-8 bytes or emits a report full of `?`.\n\n Apply **all three** layers — Python env, host console, file open\n — because each closes a different gap (Python interpreter, OS\n console buffer, file-level encoding):\n\n **Python env (always):**\n ```cmd\n set PYTHONIOENCODING=utf-8\n set PYTHONUTF8=1\n ```\n\n **Host console (`cmd.exe`):**\n ```cmd\n chcp 65001\n ```\n\n **Host console (PowerShell 5.1 / 7):**\n ```powershell\n $OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)\n [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n ```\n\n **In any fill script you author**, open files explicitly UTF-8:\n ```python\n with open(path, \"w\", encoding=\"utf-8\") as f:\n json.dump(data, f, ensure_ascii=False)\n ```\n\n (`ensure_ascii=False` keeps Chinese as Chinese instead of escaping\n to `\\uXXXX` bloat; the validator handles both, but the file is\n smaller and inspectable. The script-level fix alone is **not**\n enough if `cmd.exe` / PowerShell still pipes through a non-UTF-8\n code page on the way in.)\n\n**中文** — pipeline 在 Windows (含 Surface Laptop 7 ARM, Prism 模拟) 跑\n没问题, 但 PowerShell 5.x 有 3 个坑出问题先看这里, 别先怪 pipeline:\n\n1. **`curl` 在 PowerShell 是 `Invoke-WebRequest` 的别名**. 永远显式调\n `curl.exe` (例: `curl.exe --version`), 否则 `--fail` / `-sSL` 被静默\n 重新解释, surf 安装命令直接错.\n2. **任何写到 `stderr` 都会触发 `NativeCommandError`** (PowerShell 5.x,\n 即使 exit 0). 所以 v0.7.17 起 `forensic_pipeline.py` 的 banner 改写\n `stdout`. 不要用 `2>&1 | Tee-Object` 把两个流合并 — pipeline 依赖\n stdout/stderr 分流. 推荐: `python3 ... > skeleton.log 2> skeleton.err`,\n 或者直接在 `cmd.exe` 里跑.\n3. **尾部 section 卡住时 skeleton 文件会丢**. v0.7.17 起 pipeline 每跑\n 完一个 section 就写 `.work/skeleton_partial.json` (含 `_status:\n in_progress` + `_last_section_completed`), 即使 SIGINT / 硬杀也能\n inspect 部分结果. 最慢的尾 section (`section_wash_infra`) 现在受\n `BINANCE_ALPHA_WASH_INFRA_MAX_SECONDS` env (默认 300s) 控制. ARM\n 模拟下每条 surf SQL 慢 2-3 倍, 建议调到 600-900s 而不是直接杀进程.\n\n## Report language — MANDATORY, derive from the user's input language\n\n> **This is the bridge the agent MUST close: the report body language is a\n> SEPARATE i18n system from the `onboarding_i18n` prompt strings, and it\n> does NOT auto-detect.** The pipeline owns the report-body locale via\n> `forensic_pipeline.py --lang {zh|en}` (equivalently the\n> `BINANCE_ALPHA_LANG` env var). **Both default to `zh`.** A full `en.json`\n> language pack ships and is at parity with `zh.json` — but nothing in the\n> pipeline inspects the user's message, so if you don't pass `--lang`, every\n> report comes out Chinese regardless of what language the user wrote in.\n\n**Rule (same locale logic as `onboarding_i18n`): before running the\npipeline, pick `--lang` from the user's most recent message:**\n\n- Message contains CJK characters (`一-鿿` etc.) → `--lang zh`\n- Otherwise (English / Latin-script) → `--lang en`\n\nLocked report-body strings are baked into the skeleton at pipeline time, so\n**the language cannot be changed at render or fill time** — it must be set on\nthe `forensic_pipeline.py` call. Authoring narrative fills in step 2 must\nmatch the chosen `--lang` (English narrative for `--lang en`). Getting this\nwrong means a full pipeline re-run (re-spends Surf credits), so set it once,\nup front.\n\n## Quick start (3 commands)\n\n```bash\n# Paths below are relative to this `alpha/` sub-domain root inside the\n# installed skill (~/.claude/skills/hertzflow/alpha/). Run them from\n# there, or prefix with the absolute install path.\n\n# 0. Pick LANG from the user's most recent message (see rule above):\n# CJK chars → zh, otherwise → en.\nLANG_FLAG=en # or zh\n\n# 1. Pipeline — produces skeleton + initial monitoring_wallets export\npython3 v06/forensic_pipeline.py \"$CA_LOWER\" --lang \"$LANG_FLAG\" --out /tmp/skeleton.json\n\n# 2. LLM fills writable slots (see \"LLM fill guide\" below)\n# Read /tmp/skeleton.json, replace every \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n# with a real narrative string IN THE SAME LANGUAGE as --lang,\n# write to /tmp/filled.json.\n\n# 3. Render — validator runs in-process first; render aborts on any error\n# Also re-emits monitoring_wallets/* with LLM-filled alert text\npython3 v06/render_report.py \\\n --skeleton /tmp/skeleton.json \\\n --filled /tmp/filled.json \\\n --out /tmp/report.md\n```\n\nTotal wall clock: ~12-25s pipeline + LLM fill (varies) + \u003c1s render.\n\n### 🚫 Step 2 must be authored by you — DO NOT invoke `tests/smoke_fill.py`\n\n**EN** — `tests/smoke_fill.py` is a CI/E2E test fixture that produces\nplaceholder narrative strings (e.g. `(alpha variant)`, `(bravo variant)`).\nIt exists to exercise the pipeline → render plumbing in unit tests;\n**it is not a production fill step**.\n\n**中文** — `tests/smoke_fill.py` 是 CI/E2E 测试用的 fixture, 产出 NATO\n后缀占位符 (`(alpha variant)` / `(bravo variant)` ...). 它只用来跑\npipeline → render 的单元测试管道, **不是生产环境的 fill 步骤**.\n\n`render_report.py` enforces this at the code level (v0.7.2) /\n`render_report.py` 在代码层硬挡 (v0.7.2):\n\n- **EN**: Smoke fixtures carry `_smoke_test_fixture: true` at top level\n of `filled.json`. Render exits 3 (\"REFUSED\") on detection. Even if\n the flag is stripped, render fingerprint-detects NATO-suffix stubs\n (≥3 distinct words → exit 3). Override requires BOTH\n `BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1` AND\n `BINANCE_ALPHA_SMOKE_OVERRIDE_REASON=\"\u003ctext>\"` (CI/E2E only).\n- **中文**: Smoke fixture 在 `filled.json` 顶层注入\n `_smoke_test_fixture: true`. Render 检到 → exit 3 (\"REFUSED\"). 即使\n 把 flag 删了, render 还会扫 NATO 后缀指纹 (≥3 个不同词 → exit 3).\n Override 必须同时设两个 env var:\n `BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1` 和\n `BINANCE_ALPHA_SMOKE_OVERRIDE_REASON=\"\u003c理由>\"` (仅 CI/E2E 用).\n\n**The correct workflow for step 2 / Step 2 正确流程:**\n\n1. **EN**: Read `/tmp/skeleton.json` directly (it's plain JSON).\n **中文**: 直接读 `/tmp/skeleton.json` (普通 JSON).\n2. **EN**: For every value equal to the literal string\n `\"\u003cLLM_NARRATIVE_PLACEHOLDER>\"`, write a real narrative string based\n on the locked data adjacent to it. See *LLM fill guide* below for\n per-slot length minimums and content rules.\n **中文**: 把每个等于字面量 `\"\u003cLLM_NARRATIVE_PLACEHOLDER>\"` 的字段\n 替换成基于相邻 locked 数据的真实 narrative. 每个 slot 的最小长度\n 和内容规则见下面 *LLM fill guide*.\n3. **EN**: Save to `/tmp/filled.json` and run `render_report.py`.\n **中文**: 写到 `/tmp/filled.json`, 跑 `render_report.py`.\n\n**Background / 背景**: cross-LLM acceptance testing of v0.7.1 found that\nmultiple AI agents defaulted to running\n`python3 tests/smoke_fill.py` as their fill step, yielding reports full\nof placeholder text that looked \"filled\" but carried no analytical\ncontent. v0.7.1 跨 LLM 验收时发现多个 agent 都默认调\n`tests/smoke_fill.py` 当 fill 步骤, 产出看似\"已填\"实际无 analytical\ncontent 的占位符报告. The v0.7.2 render gate exists because docs alone\nwere not sufficient — agents read code paths over prose. v0.7.2 的\nrender gate 就是因为光靠 docs 拦不住 — agent 读代码路径优先级 > 读文档.\nIf your fill step is correct, the gate is invisible. / 如果你的 fill\n步骤正确, gate 不会出现.\n\n## Output files\n\nPipeline + render produce, in addition to the report.md, a `monitoring/`\ndirectory next to the skeleton/report with 4 wallet-tracker import files:\n\n| File | Format | Importable by |\n|---|---|---|\n| `monitoring_wallets.json` | canonical v0.6 schema | own scripts / analytics — NOT for tracker import |\n| **Paste route** (text-area paste, no file upload) |\n| `monitoring_paste.json` | `[{address, name, emoji}, ...]` JSON | **Binance Wallet + OKX wallet** paste (verified). Binance UI calls it \"GMGN format\" |\n| `monitoring_gmgn.txt` | `[0xabc, 0xdef, ...]` address-only | **GMGN paste probe 1** (per docs Import section) |\n| `monitoring_gmgn_quoted.txt` | `[\"0xabc\", \"0xdef\", ...]` quoted array | **GMGN paste probe 2** (alternative interpretation of docs) |\n| **File upload route** (CSV/XLSX required) |\n| `monitoring_binance_web3.csv` | `name,address,network` | **Binance Web3 Wallet file upload** (user reported file upload route accepts CSV/XLSX, not JSON) |\n| `monitoring_okx.csv` | `Network,Address,Label,Note` | **OKX wallet file upload** (CSV/XLSX route) |\n\n**GMGN known issue**: GMGN's bulk import widget is likely Solana-only — docs example uses Solana base58 addresses, and our BSC 0x... addresses got \"Invalid format\" on both paste probes. If GMGN BSC bulk import is unsupported, monitor those wallets via GMGN UI one-by-one OR use Binance Wallet / OKX instead.\n\n**Wallet filter (beta.12)**: Only wallets with non-zero balance, critical role (OPERATOR_RELAY / 潜伏钱包), or recent 72h activity are emitted. 已派完 wallets with 0 balance are excluded (no longer actionable — just historical). Full list stays in `monitoring_wallets.json` for analytical reference.\n\n**Use paste before upload**: paste route is generally more reliable; only fall back to file upload when paste fails. Match file to the route you're using:\n- Paste route: open `monitoring_paste.json` / `monitoring_gmgn*.txt`, Ctrl+A Ctrl+C, paste in platform dialog\n- Upload route: drag/select the `.csv` file in the platform's file picker\n\nLabel format: `\u003cSYM>-\u003cROLE>-\u003caddr5>` (e.g., `ZEST-Deployer-3a6dc`, `ZEST-Dumper-1de14`).\n≤25 chars to clear all known tracker char limits. ROLE token enum:\n`Deployer / Dumper / PDumper / Quiet / LP / Anomaly / Other`.\n\nPipeline-time emit uses empty `alert` field; render-time re-emit fills it\nwith the LLM's monitoring narrative.\n\n## What goes in writable slots — LLM fill guide\n\nThe skeleton emits `\u003cLLM_NARRATIVE_PLACEHOLDER>` at every writable path.\nLLM must replace each with a real narrative string that:\n\n1. References the locked data adjacent to it (don't invent numbers).\n2. For `*.events[].nature`, the description must be consistent with the\n `evt_ref` it sits next to — validator looks up the evt_ref in\n evidence_graph and rejects amount/ts/from/to contradictions within\n 5% tolerance.\n3. Meets the min_length specified in `field_authority.yaml`.\n4. Does NOT contain META commentary (see anti-patterns).\n5. Does NOT contain hypothetical fallback language (\"如果主链不是 BNB\n Chain ...\" — META corpus rejects this).\n\n### 🚫 No hardcoded numbers in fill scripts (v0.7.19.5)\n\nIf you author a `fill.py` script that gets reused across runs (multiple\ntokens, repeated runs of the same token), **every number cited in\nnarrative MUST be read from the skeleton dict, not written as a\nliteral**. The token's price, volume, LP, holdings %, wallet counts,\nmedian price, etc. all change between runs; a literal in the script\nsilently ships stale data on the next run, with the locked tables\nright next to it showing fresh data — self-contradicting report.\n\n**Wrong (literal):**\n```python\nsk[\"liq\"][\"interpretation\"] = (\n \"Alpha 24h vol $16,678,802 看似充沛, 但 DEX 主池 LP 仅 $635K, \"\n \"5% 滑点 ~$8K, ...\"\n)\n```\n\n**Right (skeleton-bound):**\n```python\nmeta = sk[\"meta\"]; liq = sk[\"liq\"]\nvol_24h = meta.get(\"alpha_vol_24h_usd\")\nmain_lp = liq.get(\"dex_pool_liquidity_usd\")\nentry_cap = liq.get(\"alpha_5pct_depth_usd_est\")\nsk[\"liq\"][\"interpretation\"] = (\n f\"Alpha 24h vol ${vol_24h/1e6:.2f}M 看似充沛, 但 DEX 主池 LP \"\n f\"{('

hertzflow — Crypto trade-decision intelligence (single skill, multi-domain) is HertzFlow's umbrella skill for on-chain + CEX trading research. Like Surf, it's one skill with internal routing — read the table below to see which sub-domain handles the user's request, then follow that sub-domain's . 🌐 Language — MANDATORY, read first Detect the language of the user's most recent message and use that language for everything: your conversation replies AND the generated report. This is not optional and not a preference the user has to ask for. - Message contains CJK characters ( , kana, hangul) →…

+ format(main_lp, ',.0f')) if main_lp else '本次拉取失败'}, \"\n f\"5% 滑点 ~${entry_cap:,}, ...\"\n)\n```\n\n**v0.7.19.5 validator enforces this:** `V_NARRATIVE_NUMERIC_HALLUCINATION`\nis now a STRUCTURAL error (hard fail, `render_report.py` exits 1)\nwhen any narrative number does not match a locked field within 5%.\nThe pre-v0.7.19.5 soft-warning behavior is opt-in via\n`BINANCE_ALPHA_ALLOW_NUMERIC_WARNINGS=1` (use only for dev / CI flows\nthat intentionally test against stale fixtures).\n\n**Self-check before publishing your fill script:**\n\n```bash\n# Any literal $-number or N.N% in fill script body?\ngrep -nE '\\$[0-9][0-9,]*(\\.[0-9]+)?[KM]?\\b' fill.py | grep -v '^[0-9]*:.*#'\ngrep -nE '\\b[0-9]+\\.[0-9]+%' fill.py | grep -v '^[0-9]*:.*#'\n# Both should return 0 hits (excluding comments).\n```\n\nThen `python3 render_report.py ...` — if `V_NARRATIVE_NUMERIC_HALLUCINATION`\nfires, your narrative is referencing a number that does not exist in\nthe skeleton's locked pool (either from a previous run or hallucinated).\n\nWritable slots (15 categories):\n\n| Path | Min len | Notes |\n|---|---|---|\n| `verdict.one_liner` | 100 | One sentence summary of the verdict. Must mention the dominant signal (quiet wallet size, anomaly count, etc.) |\n| `anomaly.waves[].status_text` | 2 | e.g. \"已完成\", \"进行中\", \"未启动\" |\n| `anomaly.waves[].events[].hours_ago_text` | 3 | e.g. \"约 12 小时前\", \"约 3 天前\" |\n| `anomaly.waves[].events[].nature` | 10 | Narrative of the transfer. MUST match evt_ref data (5% tolerance enforced) |\n| `anomaly.detector_summary[].detail` | 10 | One-line interpretation per detector category |\n| `anomaly.rhythm.title` | 10 | Title for the 分发 (on-chain transfer) rhythm narrative |\n| `anomaly.rhythm.waves[].detail` | 10 | Per-wave rhythm commentary |\n| `anomaly.verdict_impact` | 80 | How anomaly findings shape the verdict |\n| `multi_chain.interpretation` | 30 | Comment on chain coverage / cross-chain risk |\n| `tge.interpretation` | 30 | Comment on TGE timing + current vs anchor multiple |\n| `alloc.interpretation` | 30 | Comment on Rule 11 分发 (on-chain dispersal) breakdown |\n| `cex_trace.interpretation` | 30 | Comment on tier + CEX catalyst |\n| `liq.interpretation` | 30 | Comment on entry-size cap + LP flow |\n| `holdings_distribution.key_takeaways[]` (≥3) | 20 | 3+ bullets summarizing the role distribution |\n| `lineage.m4_notes[]` (≥2) | 20 | LP-前预分配 解读 bullets |\n| `lineage.m6.rows[].identity_narrative` | 5 | Brief identity guess per m6 row |\n| `lineage.m6.rows[].status_narrative` | 5 | Current status per m6 row |\n| `monitoring_wallets[].alert` | 15 | One-line alert condition |\n| `monitoring_footer` | 30 | Overall monitoring purpose statement |\n| `decision_action_block.immediate_action.narrative` | 30 | Why this action, plain language |\n| `decision_action_block.stop_loss.rationale` | 20 | Why this stop trigger price |\n| `decision_action_block.re_entry_conditions[].narrative` | 15 | What the condition means in plain language |\n\n## 大白话 narrative — avoid internal jargon (beta.8)\n\nWhen filling writable slots, write in 中文大白话 — your reader is a\ntrader, not a forensic engineer. Do NOT use these internal schema\nnames in narrative text.\n\n### 严格术语契约 — **派发 ≠ 分发** (v0.7.15)\n\nThese two words are NOT interchangeable. Mixing them in narrative is a\nfactual error, not a style preference:\n\n| 术语 | 含义 | 用在 |\n|---|---|---|\n| **分发** (distribute / on-chain transfer) | 链上 token 从 A 转到 B,**还没卖** | rule_11 / lineage / m6 / holdings / anomaly waves — every description of an on-chain transfer between wallets. `dumped_pct` in 中文 narrative reads as **\"已分 X%\"** (转出率, NOT 卖出率). |\n| **派发** (dump / sell-out / realized exit) | 链下/CEX 卖出, 或 DEX swap 已 realized | dump_tracker's confirmed-sold output (`confirmed_est_profit_usd` = \"估算套现\") and sell-pressure / 砸盘风险 narrative ONLY. |\n\n**Examples**:\n- ✓ \"项目方上线前向内幕地址**分发** 1B token\" (on-chain transfer)\n- ✗ \"项目方上线前**派发**\" (wrong — they didn't sell, they only moved)\n- ✓ \"潜伏钱包开始**派发**会引发砸盘\" (sell-pressure risk)\n- ✗ \"潜伏钱包累计**派发** 10 亿\" (wrong — should be 分发 / 转出)\n- ✓ \"已确认**派发** $3.6M (CEX 充值 + DEX swap)\" (confirmed sell)\n- ✗ \"已确认**分发** $3.6M\" (wrong — $ figures are sell-out, not transfer)\n\n### Internal-jargon → 大白话 table\n\n| Internal jargon | 大白话替换 |\n|---|---|\n| `Rule 11` / `Rule 11 backward trace` | \"项目方分发追溯\" / \"上线前内幕分发追溯\" |\n| `OPERATOR_RELAY` / `Operator relay` | \"庄家中转地\" |\n| `DUMPER_DEST` / `dumper-destination` | \"散户接收\" / \"分发下游\" |\n| `RULE11_QUIET` / `Quiet wallet` / `quiet wallet` | \"潜伏钱包\" (内幕未分发) |\n| `RULE11_PARTIAL` / `Partial Dumper` | \"分发中钱包\" |\n| `RULE11_FULL` / `Full Dumper` | \"已分完钱包\" |\n| `evt_ref` / `m6_ref` / `node_ref` | \"事件 #X\" / \"内幕 #X\" / \"钱包 #X\" |\n| `dumped_pct` | \"已分 X%\" (转出率) |\n| `confirmed_est_profit_usd` | \"估算套现 ~$X\" / \"确认派发金额\" |\n| `Pre-launch` | \"上线前\" |\n\nThe pipeline already writes these 中文 labels in locked fields (e.g.,\nholdings_distribution.role_rows[].role_label uses \"庄家中转地\"). Your\nwritable narrative MUST match that style — don't reintroduce English\nenum names, and don't mix 派发/分发.\n\nOK to keep: hex addresses (0xabc...), USD $ symbols, EXIT_IF_HOLDING /\nAVOID enum strings (those are verdict enums, by design machine-readable\n+ user-displayed atomically).\n\n## META anti-patterns (validator rejects)\n\nThe semantic check loads `schema/meta_blacklist_corpus.txt` and rejects\nwritable values matching any of these patterns. **Do not write any of\nthis content into narrative slots**:\n\n- Self-referential commentary (\"以下分析基于...\", \"本报告...\")\n- Hedged fallback templates (\"如果主链不是 BNB Chain, 则...\")\n- Skill-version mentions (\"本工具 v0.5.9 ...\")\n- Methodology notes mid-narrative (\"Rule 11 backward trace 的做法是...\")\n- Empty placeholders (\"详见上述...\", \"请见...\")\n- KOL / external analyst cites (\"据 @某某 推文\", \"根据某 KOL 分析...\")\n\nThe corpus is loaded at validator construction; new patterns are added\nin commits, never inline.\n\n## Provenance contract (V_PROVENANCE_NARRATIVE_MISMATCH)\n\nEvery `anomaly.waves[].events[].nature` field sits adjacent to an\n`evt_ref` (e.g., `evt_034`). The validator looks up `evt_034` in\n`evidence_graph` and reads its `amount`, `ts`, `from`, `to`. If the\nnarrative claims a different amount (>5% tolerance) or a different\ntimestamp (>10 min) or a wrong from/to address, validator emits\n`V_PROVENANCE_NARRATIVE_MISMATCH` and render aborts.\n\nPractical rule: when narrating an event, quote the exact numbers from\nthe skeleton row, don't paraphrase.\n\nBad: \"deployer dumped ~$7M to some wallet recently\"\nGood: \"deployer `0x27eac580…` 向 `0x2deb0290…` 转出 30 tokens (evt_004,\n2026-02-26 12:34 UTC)\"\n\n## The 9 forensic Decision Anchor rules\n\nThese are content-level requirements every report MUST satisfy.\nv0.6 enforces most via locked-field structure; LLM narrative must\nalign with the locked data, not invent alternatives.\n\n### Rule 0 — Spot graduation is terminal\n\nBinance Alpha and Binance Spot are mutually exclusive. Once a token\ngraduates to Spot, it is REMOVED from Alpha. Forensic methodology\n(S1/S2/S3 tiers, 庄家归集 detector, vesting unlock anchor) is\n**scoped to Alpha only**. For Spot-graduated CAs, Section A aborts\nwith `SPOT_GRADUATED` and the skill stops.\n\nS1/S2/S3 tier definitions:\n- **S1** — Alpha only, no CEX perp anywhere.\n- **S2** — Alpha + Binance perpetual futures.\n- **S3** — Alpha + at least one non-Binance CEX perp (Aster / Bitget\n / OPG). **S3 does NOT imply S2** — a token can reach S3 via Aster\n without Binance ever touching it. `s2_date` is nullable when\n `tier=S3`.\n\nSection CEX-TRACE renders Binance row based on `s2_date`, NOT on tier\nalone.\n\n### Rule 11 — Deployment backward trace BEFORE current snapshot\n\nAlpha tokens are deployed weeks before listing. Forensic value lives\nin the deployment→listing window, not the current-holders snapshot.\n\n`rule_11_backward_trace` runs 4 steps automatically:\n\n1. **Find mint event** (`from = 0x0`). Capture mint timestamp +\n deployer wallet. Window: `block_date >= alpha_listing_date - 90d`.\n2. **Trace deployer outflows** in `[mint_date, alpha_listing_date]`.\n Every receiver is a pre-launch allocation insider (M4 class).\n3. **Per receiver, compute** `total_received` / `current_balance` /\n `dumped_pct = total_out / total_received * 100`. Sort desc by\n dumped_pct.\n4. **For top dumpers, trace destinations** via BscScan label probe\n (Section X). Categorize: CEX hot wallet → \"已开始 CEX 派发\"; DEX\n LP pool → \"DEX 抛压\"; unlabeled EOA → \"OTC 清洗\".\n\nPipeline output:\n\n- `lineage.m6.rows[]` — every pre-launch receiver, with stable\n `m6_ref` provenance ID.\n- `anomaly.waves[]` — wave 1 (pre-launch dispersal), wave 2 (dumper\n distribution), wave 3 (recent 72h).\n- `lineage.flowchart_nodes/edges[]` — mermaid graph from deployer →\n receivers → top destinations.\n- `alloc.rows[]` — receiver bucket aggregation (quiet / partial /\n full dumpers).\n\nQuiet (0% dumped) receivers are **future supply risk**. Their\ncumulative size = the verdict's primary downgrade trigger.\n\n### Rule 5 — 3-tier price anchors (current vs anchor multiple)\n\nEvery report MUST surface the current-price anchor against TGE\nbenchmarks:\n- (a) LP creation first-swap price (Section TGE)\n- (b) Alpha open price (Section TGE)\n- (c) Current price (DexScreener / Alpha API)\n\nDecision anchor = current ÷ Alpha-open multiplier. v0.6 emits the\ntimes in `tge.rows[]`; if first-swap price isn't directly indexed,\nTGE row shows `—` and LLM narrates the multiplier from context.\n\n### Rule 4 — Liquidity = entry size cap\n\nEvery report MUST emit the **Alpha 5% slippage cap** (= Alpha vol_24h\n/ 96 × 0.05 heuristic) as the headlined entry-size anchor. NOT\n\"liquidity OK\", NOT TVL. A concrete USD figure the trader can act on.\n\n`section_liq` emits this in `liq.rows[0]` + as\n`decision_action_block.immediate_action.tranche_max_usd` /\n`decision_anchors[0]`.\n\n### Rule 6 — Allocation as bargaining power\n\nAlpha allocation % vs industry baseline (3-10% typical). \u003c3% = STRONG\nproject bargaining power (project doesn't need Alpha flow → catalyst\nlikely on bigger venues). >10% = WEAK.\n\nIf Alpha API doesn't publish allocation, `section_alloc` infers from\nRule 11 receiver distribution. Do NOT fabricate from external sources.\n\n### Rule 7 — CEX traces = short-term catalyst\n\n14-day scan for project wallets sending to Coinbase / Kraken / Bybit /\nKuCoin / Binance major deposit addresses. ≥2 exchanges hit → HIGH\nsignal (1-2 weeks listing likely).\n\nv0.6 phase B emits Binance perp listing check only; Aster + Bitget\nare `NOT_IMPLEMENTED` stubs that surface in `cex_trace.rows` for\nmanual verification.\n\n### Rule 8 — Explicit blindspot disclosure\n\nSections that couldn't run (data gap / API failure / cost cap / RPC\ndown) MUST be listed in a \"Data Gap\" appendix. Do NOT silently omit.\nDo NOT fabricate data from external sources to fill gaps.\n\n### Rule 9 — Every finding → one decision\n\nEvery numeric finding in the report must map to a specific trading\ndecision anchor. If a number can't be answered with \"what do I do\nwith this?\" — drop it.\n\nKeep:\n- \"$10,228 5% slippage cap\" — direct size anchor\n- \"current vs Alpha open 1.87×\" — vs anchor multiple\n- \"Alpha 配额 1%\" — bargaining power anchor\n- \"Rule 11 Quiet 500 万 tokens (0.5% 总供应)\" — future抛压 size\n\nDrop:\n- \"84 wallets holding\" (no actionability without baseline)\n- \"sniper PNL $50k\" (user can't act on it)\n\n### Rule 10 — Hard prohibition: no external KOL cites\n\nReports MUST NOT reference external analyst / KOL by name. No \"per\n@...\" or \"from some-KOL intel\" or \"based on community research\". This\nskill's output is independent forensic — if a data point can't be\nderived from on-chain / Alpha API / RPC, it goes in Data Gap, never\ncherry-picked from external opinion.\n\nReason: (a) maintains skill independence + audit trail (b) avoids\nselection bias from external sourcing (c) prevents the \"borrowed\nconviction\" failure mode.\n\n### Rule MULTI-CHAIN — single-chain BSC minimal coverage\n\nv0.6 phase B.3 ships with `section_multi_chain` minimal: single-chain\nBSC check + RPC totalSupply sanity. Cross-chain detection (CoinGecko\nplatforms lookup for ETH wrapper / Base wrapper / Solana primary)\ndeferred to v0.7.\n\nIf chain != BSC: `multi_chain.gate_note` triggers verdict downgrade\nto ADVISORY (acknowledges partial coverage).\n\n## Verdict heuristic (pipeline-derived, LLM cannot override)\n\n`_derive_verdict_enum` reads Rule 11 + anomaly72 signals:\n\n- **EXIT_IF_HOLDING (建议卖出)** if:\n - Any quiet wallet holds ≥ 5M tokens (size threshold), OR\n - Any pre_launch_receiver with `dumped_pct ≥ 95%` is \"active\" (still\n has nonzero balance + recent activity), OR\n - `anomaly72.n_recent_events ≥ 10`\n- **WAIT (等等看)** if `anomaly72.n_recent_events ≥ 3`\n- **ADVISORY (中性无方向)** otherwise\n\nDowngrade chain: baseline → verdict → next_tier_enum (one step further\ndown). LLM narrates the verdict via `verdict.one_liner`; LLM cannot\nmodify enum / baseline / next_tier — those are derived_locked and\nvalidator rejects any change.\n\n## decision_action_block structure (new in v0.6)\n\nReplaces v0.5's freeform \"action narrative\" with structured slots:\n\n```json\n{\n \"immediate_action\": {\n \"action_enum\": \"sell\" | \"hold\" | \"wait\" | \"buy\", // derived_locked\n \"venue_enum\": \"alpha\", // locked\n \"tranches_n\": 3, // locked\n \"tranche_max_usd\": 3409, // locked (from Alpha 5% cap)\n \"horizon_hours\": 48, // locked\n \"slippage_pct_cap\": 3, // locked\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" // writable\n },\n \"stop_loss\": {\n \"trigger_price_usd\": 0.952, // locked (from liq overrides)\n \"current_price_usd\": 1.12, // locked\n \"delta_pct\": -15.0, // locked\n \"rationale\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" // writable\n },\n \"re_entry_conditions\": [\n {\n \"condition_type\": \"rule11_quiet_wallets_dumped_pct\", // locked enum\n \"threshold\": 80, // locked\n \"current_value\": 0, // locked\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" // writable\n }\n ]\n}\n```\n\nLLM writes only the 3 `narrative` / `rationale` fields. Everything\nelse is pipeline-derived from Rule 11 + section_liq.\n\n## Validator error reference\n\nCommon errors and what they mean:\n\n| Error code | Meaning | LLM action |\n|---|---|---|\n| `V_SCHEMA_VERSION` | skeleton/filled schema_version don't match | Don't modify `_schema_version` |\n| `V_LOCKED_FIELD_MODIFIED: path=X` | LLM changed a locked or derived_locked field | Restore the original value from skeleton; only narrative slots are yours |\n| `V_LOCKED_FIELD_ABSENT: path=X` | A locked scalar path is missing in both skeleton + filled | Pipeline regression — re-run forensic_pipeline.py |\n| `V_LOCKED_FIELD_COUNT: path=X` | Array length mismatch between skeleton + filled | Don't add or remove array elements; only fill the existing placeholders |\n| `V_WRITABLE_PLACEHOLDER` | A writable slot still contains `\u003cLLM_NARRATIVE_PLACEHOLDER>` | Fill that slot |\n| `V_WRITABLE_TOO_SHORT: min_length=N got=M` | Narrative shorter than spec | Expand to meet min_length |\n| `V_WRITABLE_TOO_FEW: min_count=N got=M` | List has fewer items than spec (e.g., `key_takeaways[]` needs ≥3) | Add more bullets |\n| `V_SEMANTIC_META_FAIL: path=X matches blacklist pattern Y` | Narrative contains META commentary | Rewrite to remove self-referential / hedged-template language |\n| `V_PROVENANCE_NARRATIVE_MISMATCH: evt_ref=X claims A but evidence has B` | Narrative contradicts the evt_ref it cites | Quote the exact numbers from the skeleton row |\n| `V_R11_M6_WAVES_INCONSISTENT` | `m6.rows` has dumpers that don't appear in `anomaly.waves` | Pipeline bug — re-run forensic_pipeline.py |\n| `V_R12_PLACEHOLDER: path=X` | A `\u003cLLM_NARRATIVE_PLACEHOLDER>` or `\u003cPIPELINE_PHASE_*>` marker survived to filled | Fill the slot |\n\n## File tree\n\n```\nv06/\n├── DESIGN_v06.md # architectural decisions\n├── SKILL_v06.md # this file\n├── forensic_pipeline.py # orchestrator (3 rounds, ~10 helpers)\n├── validate_report_data.py # validator (~22 checks, hardcoded schema/)\n├── render_report.py # jinja2 template engine + security hardening\n├── helpers/\n│ ├── evidence_graph.py # stable-ID assignment (evt/m6/node/mon/anc)\n│ ├── parallel_surf.py # concurrent surf onchain-sql runner\n│ ├── section_a_scope.py # Alpha API + Spot graduation cross-probe\n│ ├── rule_11_backward_trace.py # mint → deployer → receivers → dumps\n│ ├── section_anomaly_72h.py # recent 24-72h large transfers\n│ ├── section_cex_trace.py # Binance perp probe → tier derivation\n│ ├── section_liq.py # DexScreener + Alpha 5% depth + LP flow\n│ ├── section_tge.py # LP creation + Alpha open + current px\n│ ├── section_alloc.py # Rule 11 receivers → bucket aggregation\n│ ├── section_multi_chain.py # single-chain BSC minimal\n│ ├── section_f_holders.py # top-50 current balance\n│ └── section_l_distribution.py # role classification + flowchart build\n├── schema/\n│ ├── field_authority.yaml # 3-tier registry (locked/derived_locked/writable)\n│ └── meta_blacklist_corpus.txt # META commentary anti-patterns\n└── tests/\n ├── smoke_fill.py # E2E fixture (CI ONLY — render refuses by default)\n └── test_derived_locked_enforcement.py # 11-case regression suite\n```\n\n## Cost model\n\nPer-CA cost (Surf credits, ~$0.0079/credit):\n\n| Section | ~Credits | Wall clock |\n|---|---|---|\n| section_a_scope | 0 (API + RPC) | ~0.5s |\n| rule_11_backward_trace | ~60-120 | ~5-11s |\n| section_anomaly_72h | ~30-50 | ~1-2s |\n| section_cex_trace | 0 (Binance API) | ~0.5s |\n| section_liq | ~20-30 | ~1-2s |\n| section_tge | ~10-20 | ~1-2s |\n| section_alloc | 0 (aggregation) | \u003c1s |\n| section_multi_chain | 0 (RPC) | \u003c1s |\n| section_f_holders | ~50-80 | ~2-3s |\n| section_l_distribution | 0 (aggregation) | \u003c1s |\n| **Total** | **~200-300 credits** | **~12-25s** |\n\nUSD: ~$1.6-2.4 per forensic in surf credits + LLM API tokens for the\nfill step (~5-10k input tokens, ~2-3k output tokens — under $0.05 on\na frontier LLM).\n\n## What v0.6 does NOT cover (acknowledged limitations)\n\n1. **Cross-chain detection** — `section_multi_chain` v0.6 is minimal\n single-chain BSC. CoinGecko platforms lookup for ETH/Base/Solana\n wrappers is deferred. If primary chain isn't BSC, the verdict\n downgrades to ADVISORY (acknowledges partial coverage).\n2. **Aster + Bitget perp listing detection** — `section_cex_trace`\n v0.6 implements Binance perp only. Aster/Bitget surface as\n `NOT_IMPLEMENTED` for manual verification.\n3. **Sniper average buy price** — `section_tge` v0.6 doesn't compute\n first-20-LP-swap weighted-avg sniper price (requires per-tx price\n indexing). Current vs Alpha-open multiple still surfaced.\n4. **Convergence hub detection** — v0.5 had a `convergence_hubs`\n section that v0.6 hasn't yet wired. Pipeline emits empty stub for\n forward-compat.\n5. **Embedding-based META check** — v0.6 uses substring + fuzzy\n matching on the META blacklist corpus. Embedding cosine similarity\n is a v0.7 polish.\n6. **Per-tx evidence_graph storage** — evidence_graph holds aggregated\n summaries per actor; raw transfer hashes aren't currently in the\n graph. Provenance traceability is at the section level, not tx level.\n\n## Validator-bypass surfaces removed (paranoid list)\n\nThese were all removed at various alpha milestones, **never re-add**:\n\n- `--bypass-validation` flag (v0.5.0-alpha.3, adversarial review round 3)\n- `--yaml` / `--corpus` constructor overrides (v0.6.0-alpha.3,\n cross-LLM audit)\n- subprocess validator on file paths (v0.6.0-alpha.10, adversarial review alpha.9\n TOCTOU race)\n- Silent unknown-tier handling in field_authority.yaml\n (v0.6.0-alpha.11, adversarial review alpha.10 audit)\n- Silent skip on locked-paths absent in both skeleton + filled\n (v0.6.0-alpha.11, adversarial review alpha.10 audit)\n\nThe validator now hardcodes schema paths + rejects unknown tier names\n+ enforces both locked AND derived_locked + requires scalar locked\npaths to be present. Construct → fail closed; validate → fail closed.\n\n## Cross-LLM test matrix (target)\n\nThe skill targets identical-quality output across:\n- **A frontier LLM** (reference)\n- **A second frontier LLM** (cross-model bias check)\n- **A non-frontier LLM** (lower-cost verification)\n\nE2E pass criteria per LLM:\n1. Pipeline runs successfully on a known-bad CA (e.g., BSB\n `0x595deaad1eb5476ff1e649fdb7efc36f1e4679cc`).\n2. LLM fills writable slots; validator returns 0 errors.\n3. Render produces a 14-20KB report with all 11 H2 sections.\n4. `verdict.enum` matches across LLMs (it's derived_locked, so the\n pipeline picks it — divergence here = pipeline bug, not LLM).\n5. `verdict.one_liner` content varies (writable) but all 3 mention\n the dominant signal (e.g., quiet wallet size for BSB).\n\nCross-LLM test runs at major milestones; results in v06/CROSS_LLM_TEST_v06.md.\n\n## Maintainability hooks\n\n- New section helper → add to `helpers/section_*.py` + wire into\n `forensic_pipeline.py` round N + add locked paths to\n `schema/field_authority.yaml` + smoke-test E2E.\n- New writable slot → add to `field_authority.yaml` writable list\n (with min_length / semantic_meta_check / provenance_check spec) +\n add `\u003cLLM_NARRATIVE_PLACEHOLDER>` to skeleton emission in pipeline.\n- New META pattern → append to `schema/meta_blacklist_corpus.txt`;\n validator picks it up on next construction.\n- Locked-field tampering regression → add test case to\n `tests/test_derived_locked_enforcement.py`.\n\n## How to invoke this skill in an AI agent session\n\nUser pastes a 0x address. Skill auto-starts preflight + Section A\n(free). Surfaces estimated cost (~$1.5-2.5 for full forensic).\nUser confirms. Skill runs `forensic_pipeline.py`, then fills writable\nslots based on the skeleton, then runs validator + render.\n\nThe LLM is responsible for filling the writable slots **only**.\nEverything else is pipeline / validator code. If validator fails,\nthe LLM reads the error code from this doc's error reference and\nfixes the offending narrative slot.\n\nEnd of SKILL_v06.md.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":48810,"content_sha256":"9f8df930b1a47a5a6e0bf7687506d47f4c2f8c4c0b59144746d39a7eb5b38b80"},{"filename":"alpha/v06/examples/fartcoin_v0_7_21_8_holder_snapshot_skeleton.json","content":"{\n \"_schema_version\": \"0.7.21.8\",\n \"_pipeline_status\": \"ok\",\n \"_field_authority\": {\n \"locked\": [\n \"meta\",\n \"tier_classification\",\n \"evidence_graph\",\n \"anomaly.waves[].events[].evt_ref\",\n \"anomaly.waves[].events[].ts\",\n \"anomaly.waves[].events[].amount\",\n \"anomaly.waves[].events[].from_to\",\n \"lineage.m6.rows\",\n \"cross_sym.whales[].address\",\n \"cross_sym.whales[].this_token_pct\",\n \"cross_sym.whales[].this_token_balance\",\n \"cross_sym.whales[].cross_sym_count\",\n \"cross_sym.whales[].cross_sym_tokens\",\n \"cross_sym.whales[].top_cross_sym_token\",\n \"cross_sym.whales[].arkham_label\",\n \"cross_sym.whales[].pre_launch_insider_count\",\n \"cross_sym.whales[].pre_launch_insider_tokens\",\n \"cross_sym.whales[].behavior_signature\"\n ],\n \"derived_locked\": [\n \"cross_sym.whales[].identity_classification_enum\",\n \"cross_sym.whales[].confidence_score\",\n \"cross_sym.whales[].evidence_required_fields\",\n \"cross_sym.whales[].role_classification\",\n \"wash_infrastructure.setups[].executor_X\",\n \"wash_infrastructure.setups[].maker_buy_P\",\n \"wash_infrastructure.setups[].maker_sell_Q\",\n \"wash_infrastructure.setups[].atomic_pair_ratio\",\n \"wash_infrastructure.setups[].p_drift_pct\",\n \"wash_infrastructure.setups[].q_drift_pct\",\n \"wash_infrastructure.setups[].p_tok_in\",\n \"wash_infrastructure.setups[].q_tok_in\",\n \"wash_infrastructure.setups[].tx_from_diversity\",\n \"wash_infrastructure.setups[].classification\"\n ],\n \"writable\": [\n \"verdict.one_liner\",\n \"anomaly.waves[].events[].nature\",\n \"anomaly.detector_summary[].detail\",\n \"anomaly.verdict_impact\",\n \"*.interpretation\",\n \"holdings_distribution.key_takeaways\",\n \"lineage.m4_notes\",\n \"monitoring_wallets[].alert\",\n \"monitoring_footer\",\n \"cross_sym.whales[].identity_narrative\",\n \"cross_sym.whales[].risk_assessment_narrative\",\n \"cross_sym.summary_narrative\",\n \"wash_infrastructure.setups[].investigation_narrative\",\n \"wash_infrastructure.summary_narrative\"\n ]\n },\n \"meta\": {\n \"symbol\": \"Fartcoin\",\n \"name\": \"Fartcoin\",\n \"contract_address\": \"9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump\",\n \"chain\": \"Solana\",\n \"chain_id\": \"CT_501\",\n \"alpha_listing_date_utc\": \"2024-12-18\",\n \"total_supply\": 1000000000,\n \"circulating_supply\": 999998256,\n \"circ_ratio\": 0.999998256,\n \"alpha_vol_24h_usd\": 5851805.688354953,\n \"alpha_price_usd\": 0.15320606101528864,\n \"alpha_percent_change_24h\": -4.65,\n \"alpha_market_cap_usd\": 153205793.82391822,\n \"alpha_fdv_usd\": 153206061.01528862,\n \"alpha_liquidity_usd\": 10175817.699170379,\n \"alpha_price_high_24h\": 0.16250402523602592,\n \"alpha_price_low_24h\": 0.14652237201465146,\n \"alpha_count_24h\": 43650.0,\n \"alpha_holders\": 161089.0,\n \"token_type_initial\": \"MEME_LIKELY\",\n \"single_chain\": true,\n \"primary_chain\": \"solana\",\n \"primary_chain_derivation\": \"holder_snapshot_uses_active_chain\",\n \"coingecko_platforms\": {\n \"base\": \"0x2f6c17fa9f9bc3600346ab4e48c0701e1d5962ae\"\n },\n \"chain_lp_realtime\": {\n \"base\": {\n \"lp_tokens\": 18423078322.347076,\n \"lp_usd\": 2799939443.430309,\n \"n_dex_pools\": 1,\n \"top_pool_addr\": \"0xfdbaf04326acc24e3d1788333826b71e3291863a\",\n \"top_pool_balance\": 18423078322.347076,\n \"surf_supported\": true,\n \"ca_on_chain\": \"0x2f6c17fa9f9bc3600346ab4e48c0701e1d5962ae\",\n \"surf_chain\": \"base\"\n }\n },\n \"realtime_token_info\": {\n \"price_usd\": 0.15198,\n \"volume_24h_usd\": 44128441,\n \"market_cap_usd\": 151352411,\n \"fdv_usd\": 151352411,\n \"price_change_24h_pct\": -5.43918,\n \"surf_project_id\": \"efe1459e-98fb-44bb-ac65-56824f18feaa\",\n \"surf_chains\": [\n \"Solana\"\n ],\n \"surf_match_reason\": \"ca_in_contracts\",\n \"fetch_ok\": true\n },\n \"data_freshness\": {\n \"latest_surf_block_time\": null,\n \"latest_surf_date_utc\": null,\n \"lag_hours\": null,\n \"warning\": null\n },\n \"report_lang_locked\": \"zh\",\n \"_holder_snapshot_mode\": true,\n \"_sql_supported\": false\n },\n \"tier_classification\": {\n \"tier\": \"S1\",\n \"s1_date\": \"2024-12-18\",\n \"s2_date\": null,\n \"s3_date\": null\n },\n \"evidence_graph\": {\n \"node_001\": {\n \"kind\": \"node\",\n \"type\": \"lp_pool\",\n \"addr\": \"0xfdbaf04326acc24e3d1788333826b71e3291863a\",\n \"balance\": null,\n \"usd_value\": null,\n \"label_hint\": \"DEX 主池\"\n }\n },\n \"lineage\": {\n \"_pipeline_source\": \"rule_11_backward_trace\",\n \"deployer_addr\": null,\n \"mint_evt_ref\": null,\n \"m6\": {\n \"rows\": [],\n \"n_quiet\": 0,\n \"n_partial_dumper\": 0,\n \"n_full_dumper\": 0\n },\n \"m4_notes\": [\n \"\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n ],\n \"summary_locked_numbers\": {},\n \"dumper_destinations_summary\": {},\n \"flowchart_nodes\": [\n {\n \"node_ref\": \"node_001\",\n \"role\": \"DEX_POOL\",\n \"addr_short\": \"0xfdbaf043…\"\n }\n ],\n \"flowchart_edges\": []\n },\n \"anomaly\": {\n \"waves\": [],\n \"rhythm\": {\n \"title\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"waves\": []\n },\n \"verdict_impact\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"detector_summary\": [\n {\n \"emoji\": \"⚪\",\n \"label\": \"上线前内幕分发\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"已分完内幕钱包\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"潜伏钱包 (未分发)\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"近 72h 异常大单\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n }\n ]\n },\n \"monitoring_wallets\": [],\n \"decision_summary\": {\n \"_pipeline_source\": \"forensic_pipeline.decision_summary\",\n \"primary_chain\": \"base\",\n \"primary_chain_derivation\": \"lp_usd_max\",\n \"primary_chain_lp_usd\": 2799939443.430309,\n \"realtime_24h_vol_usd\": 44128441,\n \"realtime_price_usd\": 0.15198,\n \"verdict_enum\": \"ADVISORY\",\n \"verdict_cn_label\": \"观察\",\n \"entry_size_cap_usd\": 3047,\n \"short_term_catalysts\": [],\n \"blindspots\": [\n \"主战场 solana 的链上 SQL 不在 surf 覆盖, forensic SQL 段全部跳过, 仅保留 Alpha 实时 + token-holders 快照\"\n ],\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"verdict\": {\n \"enum\": \"ADVISORY\",\n \"cn_label\": \"观察\",\n \"baseline\": \"ADVISORY\",\n \"downgrade_applied\": 0,\n \"next_tier_enum\": \"ADVISORY\",\n \"next_tier_cn\": \"观察\",\n \"one_liner\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"decision_action_block\": {\n \"immediate_action\": {\n \"action_enum\": \"wait\",\n \"venue_enum\": \"alpha\",\n \"tranches_n\": 1,\n \"tranche_max_usd\": 1015,\n \"horizon_hours\": 0,\n \"slippage_pct_cap\": 3,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"stop_loss\": {\n \"trigger_price_usd\": 0.129183,\n \"current_price_usd\": 0.15198,\n \"delta_pct\": -15.0,\n \"rationale\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"re_entry_conditions\": [\n {\n \"condition_type\": \"alpha_anomaly_72h_event_count\",\n \"threshold\": 0,\n \"current_value\": 0,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n }\n ]\n },\n \"cross_sym\": {\n \"_pipeline_source\": \"section_cross_sym\",\n \"whales\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_scope\": null,\n \"_registry_age_secs\": null,\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\"\n },\n \"wash_infrastructure\": {\n \"_pipeline_source\": \"section_wash_infra\",\n \"setups\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\",\n \"_truncated\": false,\n \"_truncation_meta\": null,\n \"_n_candidates_scanned\": 0\n },\n \"flow_operators\": {\n \"_pipeline_source\": \"section_flow_operators\",\n \"operators\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\",\n \"_n_candidates_scanned\": 0\n },\n \"holdings_distribution\": {\n \"_pipeline_source\": \"section_f_holders + section_l_distribution\",\n \"role_rows\": [\n {\n \"role\": \"DEX_POOL\",\n \"role_label\": \"DEX 主池\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"DEPLOYER\",\n \"role_label\": \"项目方钱包\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"RULE11_QUIET\",\n \"role_label\": \"潜伏钱包 (内幕未分发)\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"OTHER\",\n \"role_label\": \"其他 (散户+未分类)\",\n \"n_wallets\": 40,\n \"total_balance\": 595723171782330.0,\n \"pct_of_total\": 59572317.178233005,\n \"usd_value\": 90538007647478.52,\n \"top_addr_short\": \"9SLPTL41SP…\",\n \"top_addr_full\": \"9SLPTL41SPsYkgdsMzdfJsxymEANKr5bYoBsQzJyKpKS\"\n }\n ],\n \"progress_bars\": [\n {\n \"role\": \"DEX_POOL\",\n \"label\": \"DEX 主池\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"DEPLOYER\",\n \"label\": \"项目方钱包\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"RULE11_QUIET\",\n \"label\": \"潜伏钱包 (内幕未分发)\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"OTHER\",\n \"label\": \"其他 (散户+未分类)\",\n \"pct\": 59572317.178233005,\n \"bar\": \"████████████████████\",\n \"bar_absolute\": \"████████████████████\",\n \"value_text\": \"595,723,171,782,330 tokens (59572317.18% / $90,538,007,647,479)\"\n }\n ],\n \"n_top_holders_classified\": 40,\n \"key_takeaways\": [\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n ]\n },\n \"monitoring_footer\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"multi_chain\": {\n \"rows\": [\n {\n \"item\": \"**主战场链**\",\n \"value\": \"**base** (主战场, 非 BSC; BSC 为镜像 / 交易端)\"\n },\n {\n \"item\": \"铸造链 (supply_chain)\",\n \"value\": \"base, totalSupply 1,000,000,000\"\n },\n {\n \"item\": \"交易链 (trading_venue_chain)\",\n \"value\": \"base (Alpha + DEX 主池)\"\n },\n {\n \"item\": \"跨链分布\",\n \"value\": \"**跨链镜像** — 主战场在 base, BSC 仅为镜像端; 本报告所有 forensic 数据 (m6 / transfers / wash / holdings) 均来自 **base** 主链, BSC 镜像侧不纳入\"\n },\n {\n \"item\": \"报告覆盖范围\",\n \"value\": \"**完整覆盖 (主链 base)** — v0.7.20 起 pipeline 已按 Alpha API 报的 chainId 路由到主链, 所有 forensic SQL 直接查主链分区\"\n }\n ],\n \"gate_note\": \"✅ 主链 base, pipeline 已路由到主链分区, 无 verdict 降级\",\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"tge\": {\n \"rows\": [\n {\n \"label\": \"LP 创建首笔 (DEX 启动)\",\n \"time\": \"—\",\n \"price\": \"—\",\n \"vs_current\": \"—\"\n },\n {\n \"label\": \"Alpha 上线首笔\",\n \"time\": \"2024-12-18 12:00 UTC\",\n \"price\": \"—\",\n \"vs_current\": \"—\"\n },\n {\n \"label\": \"**当前价**\",\n \"time\": \"2026-06-01\",\n \"price\": \"**$0.1520**\",\n \"vs_current\": \"1.00×\"\n }\n ],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"alloc\": {\n \"rows\": [\n {\n \"item\": \"Alpha 配额比例 (官方披露)\",\n \"value\": \"**未披露**\",\n \"source\": \"Binance Alpha 官方接口当前未给出该字段\"\n },\n {\n \"item\": \"项目方钱包 `—` 当前余额\",\n \"value\": \"未知\",\n \"source\": \"项目方分发追溯\"\n },\n {\n \"item\": \"上线前内幕接收方 0 个累计余额\",\n \"value\": \"0 tokens (= 0.00% 总供应)\",\n \"source\": \"内幕钱包累计\"\n },\n {\n \"item\": \"潜伏钱包 0 个持仓 (核心未来风险)\",\n \"value\": \"**0 tokens (= 0.00% 总供应 / $0)**\",\n \"source\": \"未分发内幕钱包\"\n },\n {\n \"item\": \"已分完内幕钱包 0 个 (分发完成)\",\n \"value\": \"已 100% 分完, 剩余 0\",\n \"source\": \"已转出 ≥95% 的内幕钱包\"\n }\n ],\n \"n_quiet\": 0,\n \"quiet_balance_pct_total\": 0.0,\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"dump_tracking\": {\n \"_pipeline_source\": \"dump_tracker\",\n \"insider_n_wallets\": 0,\n \"confirmed_cex_tokens\": 0,\n \"confirmed_cex_pct\": null,\n \"confirmed_cex_labels\": [],\n \"confirmed_dex_tokens\": 0,\n \"confirmed_dex_pct\": null,\n \"confirmed_dex_swaps\": 0,\n \"confirmed_total_tokens\": 0,\n \"confirmed_total_pct\": null,\n \"confirmed_est_profit_usd\": null,\n \"confirmed_capped\": false,\n \"tree_holds_tokens\": 0,\n \"tree_holds_pct_supply\": null,\n \"pure_insider_holds_tokens\": 0,\n \"pure_insider_holds_pct_supply\": null,\n \"insider_holds_tokens\": 0,\n \"insider_holds_pct_supply\": null,\n \"median_price_usd\": null,\n \"wash_dominated\": false,\n \"n_dex_sellers\": 0,\n \"total_dex_swaps\": 0,\n \"top_seller_swaps\": 0,\n \"buckets_complete\": false,\n \"interpretation\": \"未检测到 insider 向 CEX/DEX 的确认卖出 (链上无法确认的转账不计入).\"\n },\n \"cex_trace\": {\n \"rows\": [\n {\n \"exchange\": \"Binance\",\n \"status\": \"未上线\",\n \"ts\": \"—\",\n \"since\": \"—\"\n },\n {\n \"exchange\": \"Aster\",\n \"status\": \"未核实\",\n \"ts\": \"—\",\n \"since\": \"—\"\n },\n {\n \"exchange\": \"Bitget\",\n \"status\": \"未核实\",\n \"ts\": \"—\",\n \"since\": \"—\"\n }\n ],\n \"new_catalyst\": \"无 14 天内新催化剂\",\n \"tier_explanation\": \"当前 S1 (Alpha only)\",\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"liq\": {\n \"rows\": [\n {\n \"anchor\": \"**不超 5% 滑点的最大单笔买入 (估算)**\",\n \"value\": \"$3,047\",\n \"note\": \"由 Alpha 24h vol 推导 (vol_24h / 96 × 0.05 启发式估算)\"\n },\n {\n \"anchor\": \"DEX 主池流动性\",\n \"value\": \"$2,799,939,443\",\n \"note\": \"surf 0xfdbaf043…\"\n },\n {\n \"anchor\": \"DEX 主池 24h 交易量\",\n \"value\": \"$44,128,441\",\n \"note\": \"surf project-detail (跨链 CEX+DEX 实时聚合)\"\n },\n {\n \"anchor\": \"LP 24h 净流\",\n \"value\": \"—\",\n \"note\": \"surf agent.solana_transfers\"\n },\n {\n \"anchor\": \"DEX 主池地址\",\n \"value\": \"`0xfdbaf04326acc24e3d1788333826b71e3291863a`\",\n \"note\": \"FDV $151,352,411\"\n }\n ],\n \"current_price_usd\": 0.15198,\n \"dex_pool_addr\": \"0xfdbaf04326acc24e3d1788333826b71e3291863a\",\n \"dex_pool_liquidity_usd\": 2799939443.430309,\n \"alpha_5pct_depth_usd_est\": 3047,\n \"lp_24h_flow\": {\n \"_skip_reason\": \"surf_no_sql_solana\"\n },\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"decision_anchors\": [\n {\n \"anchor\": \"Alpha 5% 撤单上限\",\n \"value\": \"$3,047\",\n \"status\": \"🔴 超薄\"\n },\n {\n \"anchor\": \"DEX 主池 USD 流动性\",\n \"value\": \"$2,799,939,443\",\n \"status\": \"🟢\"\n },\n {\n \"anchor\": \"LP 24h 净流\",\n \"value\": \"—\",\n \"status\": \"🟢\"\n }\n ],\n \"_pipeline_timings\": {\n \"section_a\": 20.146151959022973,\n \"rule_11\": 7.958034984767437e-06,\n \"pool_discovery\": 1.1417025234550238e-05,\n \"section_anomaly_72h\": 6.582995411008596e-06,\n \"section_cex_trace\": 0.2630662500159815,\n \"section_liq\": 8.166697807610035e-05,\n \"section_tge_alloc_multi_chain\": 1.3299356249626726,\n \"dump_tracker\": 1.425005029886961e-05,\n \"section_f_l_distribution\": 9.970797691494226e-05,\n \"section_cross_sym\": 2.2916006855666637e-05,\n \"section_wash_infra\": 5.125009920448065e-06,\n \"section_flow_operators\": 1.167005393654108e-06\n },\n \"_pipeline_total_seconds\": 21.745559375034645\n}","content_type":"application/json; charset=utf-8","language":"json","size":17257,"content_sha256":"3978f84dc904bde1d321ac6ca9b127f92a82aecd2a2228062babb45cbaf9e611"},{"filename":"alpha/v06/examples/jellyjelly_v0_7_21_8_holder_snapshot_skeleton.json","content":"{\n \"_schema_version\": \"0.7.21.8\",\n \"_pipeline_status\": \"ok\",\n \"_field_authority\": {\n \"locked\": [\n \"meta\",\n \"tier_classification\",\n \"evidence_graph\",\n \"anomaly.waves[].events[].evt_ref\",\n \"anomaly.waves[].events[].ts\",\n \"anomaly.waves[].events[].amount\",\n \"anomaly.waves[].events[].from_to\",\n \"lineage.m6.rows\",\n \"cross_sym.whales[].address\",\n \"cross_sym.whales[].this_token_pct\",\n \"cross_sym.whales[].this_token_balance\",\n \"cross_sym.whales[].cross_sym_count\",\n \"cross_sym.whales[].cross_sym_tokens\",\n \"cross_sym.whales[].top_cross_sym_token\",\n \"cross_sym.whales[].arkham_label\",\n \"cross_sym.whales[].pre_launch_insider_count\",\n \"cross_sym.whales[].pre_launch_insider_tokens\",\n \"cross_sym.whales[].behavior_signature\"\n ],\n \"derived_locked\": [\n \"cross_sym.whales[].identity_classification_enum\",\n \"cross_sym.whales[].confidence_score\",\n \"cross_sym.whales[].evidence_required_fields\",\n \"cross_sym.whales[].role_classification\",\n \"wash_infrastructure.setups[].executor_X\",\n \"wash_infrastructure.setups[].maker_buy_P\",\n \"wash_infrastructure.setups[].maker_sell_Q\",\n \"wash_infrastructure.setups[].atomic_pair_ratio\",\n \"wash_infrastructure.setups[].p_drift_pct\",\n \"wash_infrastructure.setups[].q_drift_pct\",\n \"wash_infrastructure.setups[].p_tok_in\",\n \"wash_infrastructure.setups[].q_tok_in\",\n \"wash_infrastructure.setups[].tx_from_diversity\",\n \"wash_infrastructure.setups[].classification\"\n ],\n \"writable\": [\n \"verdict.one_liner\",\n \"anomaly.waves[].events[].nature\",\n \"anomaly.detector_summary[].detail\",\n \"anomaly.verdict_impact\",\n \"*.interpretation\",\n \"holdings_distribution.key_takeaways\",\n \"lineage.m4_notes\",\n \"monitoring_wallets[].alert\",\n \"monitoring_footer\",\n \"cross_sym.whales[].identity_narrative\",\n \"cross_sym.whales[].risk_assessment_narrative\",\n \"cross_sym.summary_narrative\",\n \"wash_infrastructure.setups[].investigation_narrative\",\n \"wash_infrastructure.summary_narrative\"\n ]\n },\n \"meta\": {\n \"symbol\": \"jellyjelly\",\n \"name\": \"jelly-my-jelly\",\n \"contract_address\": \"FeR8VBqNRSUD5NtXAj2n3j1dAHkZHfyDktKuLXD4pump\",\n \"chain\": \"Solana\",\n \"chain_id\": \"CT_501\",\n \"alpha_listing_date_utc\": \"2025-02-26\",\n \"total_supply\": 999999099,\n \"circulating_supply\": 999999099,\n \"circ_ratio\": 1.0,\n \"alpha_vol_24h_usd\": 687608.7246531503,\n \"alpha_price_usd\": 0.059903269014008095,\n \"alpha_percent_change_24h\": 0.29,\n \"alpha_market_cap_usd\": 59903215.04116271,\n \"alpha_fdv_usd\": 59903215.04116271,\n \"alpha_liquidity_usd\": 4451278.686836986,\n \"alpha_price_high_24h\": 0.06346618607411839,\n \"alpha_price_low_24h\": 0.05668663648770892,\n \"alpha_count_24h\": 4433.0,\n \"alpha_holders\": 33690.0,\n \"token_type_initial\": \"MEME_LIKELY\",\n \"single_chain\": true,\n \"primary_chain\": \"solana\",\n \"primary_chain_derivation\": \"holder_snapshot_uses_active_chain\",\n \"coingecko_platforms\": {\n \"solana\": \"FeR8VBqNRSUD5NtXAj2n3j1dAHkZHfyDktKuLXD4pump\"\n },\n \"chain_lp_realtime\": {\n \"solana\": {\n \"lp_tokens\": null,\n \"lp_usd\": null,\n \"n_dex_pools\": 0,\n \"top_pool_addr\": null,\n \"top_pool_balance\": null,\n \"surf_supported\": true,\n \"ca_on_chain\": \"FeR8VBqNRSUD5NtXAj2n3j1dAHkZHfyDktKuLXD4pump\",\n \"surf_chain\": \"solana\"\n }\n },\n \"realtime_token_info\": {\n \"price_usd\": 0.060711,\n \"volume_24h_usd\": 4479403,\n \"market_cap_usd\": 60925968,\n \"fdv_usd\": 60925968,\n \"price_change_24h_pct\": 1.24375,\n \"surf_project_id\": \"ee09c244-69dd-4537-84bf-92e500026663\",\n \"surf_chains\": [\n \"Solana\"\n ],\n \"surf_match_reason\": \"ca_in_contracts\",\n \"fetch_ok\": true\n },\n \"data_freshness\": {\n \"latest_surf_block_time\": null,\n \"latest_surf_date_utc\": null,\n \"lag_hours\": null,\n \"warning\": null\n },\n \"report_lang_locked\": \"zh\",\n \"_holder_snapshot_mode\": true,\n \"_sql_supported\": false\n },\n \"tier_classification\": {\n \"tier\": \"S1\",\n \"s1_date\": \"2025-02-26\",\n \"s2_date\": null,\n \"s3_date\": null\n },\n \"evidence_graph\": {},\n \"lineage\": {\n \"_pipeline_source\": \"rule_11_backward_trace\",\n \"deployer_addr\": null,\n \"mint_evt_ref\": null,\n \"m6\": {\n \"rows\": [],\n \"n_quiet\": 0,\n \"n_partial_dumper\": 0,\n \"n_full_dumper\": 0\n },\n \"m4_notes\": [\n \"\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n ],\n \"summary_locked_numbers\": {},\n \"dumper_destinations_summary\": {},\n \"flowchart_nodes\": [],\n \"flowchart_edges\": []\n },\n \"anomaly\": {\n \"waves\": [],\n \"rhythm\": {\n \"title\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"waves\": []\n },\n \"verdict_impact\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"detector_summary\": [\n {\n \"emoji\": \"⚪\",\n \"label\": \"上线前内幕分发\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"已分完内幕钱包\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"潜伏钱包 (未分发)\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n {\n \"emoji\": \"⚪\",\n \"label\": \"近 72h 异常大单\",\n \"count\": 0,\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n }\n ]\n },\n \"monitoring_wallets\": [],\n \"decision_summary\": {\n \"_pipeline_source\": \"forensic_pipeline.decision_summary\",\n \"primary_chain\": \"solana\",\n \"primary_chain_derivation\": \"lp_zero_fallback_alpha_chain\",\n \"primary_chain_lp_usd\": null,\n \"realtime_24h_vol_usd\": 4479403,\n \"realtime_price_usd\": 0.060711,\n \"verdict_enum\": \"ADVISORY\",\n \"verdict_cn_label\": \"观察\",\n \"entry_size_cap_usd\": 358,\n \"short_term_catalysts\": [],\n \"blindspots\": [\n \"主战场 solana 的链上 SQL 不在 surf 覆盖, forensic SQL 段全部跳过, 仅保留 Alpha 实时 + token-holders 快照\"\n ],\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"verdict\": {\n \"enum\": \"ADVISORY\",\n \"cn_label\": \"观察\",\n \"baseline\": \"ADVISORY\",\n \"downgrade_applied\": 0,\n \"next_tier_enum\": \"ADVISORY\",\n \"next_tier_cn\": \"观察\",\n \"one_liner\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"decision_action_block\": {\n \"immediate_action\": {\n \"action_enum\": \"wait\",\n \"venue_enum\": \"alpha\",\n \"tranches_n\": 1,\n \"tranche_max_usd\": 1000,\n \"horizon_hours\": 0,\n \"slippage_pct_cap\": 3,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"stop_loss\": {\n \"trigger_price_usd\": 0.051604,\n \"current_price_usd\": 0.060711,\n \"delta_pct\": -15.0,\n \"rationale\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"re_entry_conditions\": [\n {\n \"condition_type\": \"alpha_anomaly_72h_event_count\",\n \"threshold\": 0,\n \"current_value\": 0,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n }\n ]\n },\n \"cross_sym\": {\n \"_pipeline_source\": \"section_cross_sym\",\n \"whales\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_scope\": null,\n \"_registry_age_secs\": null,\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\"\n },\n \"wash_infrastructure\": {\n \"_pipeline_source\": \"section_wash_infra\",\n \"setups\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\",\n \"_truncated\": false,\n \"_truncation_meta\": null,\n \"_n_candidates_scanned\": 0\n },\n \"flow_operators\": {\n \"_pipeline_source\": \"section_flow_operators\",\n \"operators\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": \"surf_no_sql_solana\",\n \"_n_candidates_scanned\": 0\n },\n \"holdings_distribution\": {\n \"_pipeline_source\": \"section_f_holders + section_l_distribution\",\n \"role_rows\": [\n {\n \"role\": \"DEX_POOL\",\n \"role_label\": \"DEX 主池\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"DEPLOYER\",\n \"role_label\": \"项目方钱包\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"RULE11_QUIET\",\n \"role_label\": \"潜伏钱包 (内幕未分发)\",\n \"n_wallets\": 0,\n \"total_balance\": 0,\n \"pct_of_total\": 0.0,\n \"usd_value\": 0.0,\n \"top_addr_short\": null,\n \"top_addr_full\": null\n },\n {\n \"role\": \"OTHER\",\n \"role_label\": \"其他 (散户+未分类)\",\n \"n_wallets\": 40,\n \"total_balance\": 962007318461153.0,\n \"pct_of_total\": 96200818.52305278,\n \"usd_value\": 58404426311095.06,\n \"top_addr_short\": \"u6PJ8DtQuP…\",\n \"top_addr_full\": \"u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w\"\n }\n ],\n \"progress_bars\": [\n {\n \"role\": \"DEX_POOL\",\n \"label\": \"DEX 主池\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"DEPLOYER\",\n \"label\": \"项目方钱包\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"RULE11_QUIET\",\n \"label\": \"潜伏钱包 (内幕未分发)\",\n \"pct\": 0.0,\n \"bar\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"bar_absolute\": \"░░░░░░░░░░░░░░░░░░░░\",\n \"value_text\": \"0 tokens (0.00%)\"\n },\n {\n \"role\": \"OTHER\",\n \"label\": \"其他 (散户+未分类)\",\n \"pct\": 96200818.52305278,\n \"bar\": \"████████████████████\",\n \"bar_absolute\": \"████████████████████\",\n \"value_text\": \"962,007,318,461,153 tokens (96200818.52% / $58,404,426,311,095)\"\n }\n ],\n \"n_top_holders_classified\": 40,\n \"key_takeaways\": [\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n ]\n },\n \"monitoring_footer\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"multi_chain\": {\n \"rows\": [\n {\n \"item\": \"**主战场链**\",\n \"value\": \"**solana** (单链, 无 BSC 镜像; Binance Alpha 以 SPL token 直接挂)\"\n },\n {\n \"item\": \"铸造链 (supply_chain)\",\n \"value\": \"solana, totalSupply 999,999,099\"\n },\n {\n \"item\": \"交易链 (trading_venue_chain)\",\n \"value\": \"solana (Alpha + DEX 主池)\"\n },\n {\n \"item\": \"跨链分布\",\n \"value\": \"**单链 solana** — Alpha 直接挂 SPL token, 没有 BSC / EVM 镜像桥; 本报告仅 holder snapshot + Alpha 实时, **不**包含 forensic SQL 段\"\n },\n {\n \"item\": \"报告覆盖范围\",\n \"value\": \"**HOLDER_SNAPSHOT (主链 solana)** — surf 不覆盖 solana 的 `onchain-sql` (无 `agent.solana_transfers` 表), 全部 forensic SQL 段已跳过; 保留 Alpha 实时行情 + surf token-holders REST + monitoring 导出\"\n }\n ],\n \"gate_note\": \"⚠️ 主链 solana 走 HOLDER_SNAPSHOT 模式 — surf onchain-sql 不覆盖该链, forensic SQL 段全部跳过. 实时行情 / holders / mcap / FDV 等 REST 数据正常拉取. 内幕谱系 / wash / 流水大户 / 跨币 4 类 SQL 检测**无数据**, 顶部 banner 已说明; 用 Solscan / Helius / Shyft / Vybe 手工核对\",\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"tge\": {\n \"rows\": [\n {\n \"label\": \"LP 创建首笔 (DEX 启动)\",\n \"time\": \"—\",\n \"price\": \"—\",\n \"vs_current\": \"—\"\n },\n {\n \"label\": \"Alpha 上线首笔\",\n \"time\": \"2025-02-26 09:20 UTC\",\n \"price\": \"—\",\n \"vs_current\": \"—\"\n },\n {\n \"label\": \"**当前价**\",\n \"time\": \"2026-06-02\",\n \"price\": \"**$0.0607**\",\n \"vs_current\": \"1.00×\"\n }\n ],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"alloc\": {\n \"rows\": [\n {\n \"item\": \"Alpha 配额比例 (官方披露)\",\n \"value\": \"**未披露**\",\n \"source\": \"Binance Alpha 官方接口当前未给出该字段\"\n },\n {\n \"item\": \"项目方钱包 `—` 当前余额\",\n \"value\": \"未知\",\n \"source\": \"项目方分发追溯\"\n },\n {\n \"item\": \"上线前内幕接收方 0 个累计余额\",\n \"value\": \"0 tokens (= 0.00% 总供应)\",\n \"source\": \"内幕钱包累计\"\n },\n {\n \"item\": \"潜伏钱包 0 个持仓 (核心未来风险)\",\n \"value\": \"**0 tokens (= 0.00% 总供应 / $0)**\",\n \"source\": \"未分发内幕钱包\"\n },\n {\n \"item\": \"已分完内幕钱包 0 个 (分发完成)\",\n \"value\": \"已 100% 分完, 剩余 0\",\n \"source\": \"已转出 ≥95% 的内幕钱包\"\n }\n ],\n \"n_quiet\": 0,\n \"quiet_balance_pct_total\": 0.0,\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"dump_tracking\": {\n \"_pipeline_source\": \"dump_tracker\",\n \"insider_n_wallets\": 0,\n \"confirmed_cex_tokens\": 0,\n \"confirmed_cex_pct\": null,\n \"confirmed_cex_labels\": [],\n \"confirmed_dex_tokens\": 0,\n \"confirmed_dex_pct\": null,\n \"confirmed_dex_swaps\": 0,\n \"confirmed_total_tokens\": 0,\n \"confirmed_total_pct\": null,\n \"confirmed_est_profit_usd\": null,\n \"confirmed_capped\": false,\n \"tree_holds_tokens\": 0,\n \"tree_holds_pct_supply\": null,\n \"pure_insider_holds_tokens\": 0,\n \"pure_insider_holds_pct_supply\": null,\n \"insider_holds_tokens\": 0,\n \"insider_holds_pct_supply\": null,\n \"median_price_usd\": null,\n \"wash_dominated\": false,\n \"n_dex_sellers\": 0,\n \"total_dex_swaps\": 0,\n \"top_seller_swaps\": 0,\n \"buckets_complete\": false,\n \"interpretation\": \"未检测到 insider 向 CEX/DEX 的确认卖出 (链上无法确认的转账不计入).\"\n },\n \"cex_trace\": {\n \"rows\": [\n {\n \"exchange\": \"Binance\",\n \"status\": \"未上线\",\n \"ts\": \"—\",\n \"since\": \"—\"\n },\n {\n \"exchange\": \"Aster\",\n \"status\": \"未核实\",\n \"ts\": \"—\",\n \"since\": \"—\"\n },\n {\n \"exchange\": \"Bitget\",\n \"status\": \"未核实\",\n \"ts\": \"—\",\n \"since\": \"—\"\n }\n ],\n \"new_catalyst\": \"无 14 天内新催化剂\",\n \"tier_explanation\": \"当前 S1 (Alpha only)\",\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"liq\": {\n \"rows\": [\n {\n \"anchor\": \"**不超 5% 滑点的最大单笔买入 (估算)**\",\n \"value\": \"$358\",\n \"note\": \"由 Alpha 24h vol 推导 (vol_24h / 96 × 0.05 启发式估算)\"\n },\n {\n \"anchor\": \"DEX 主池流动性\",\n \"value\": \"—\",\n \"note\": \"无主池\"\n },\n {\n \"anchor\": \"DEX 主池 24h 交易量\",\n \"value\": \"$4,479,403\",\n \"note\": \"surf project-detail (跨链 CEX+DEX 实时聚合)\"\n },\n {\n \"anchor\": \"LP 24h 净流\",\n \"value\": \"—\",\n \"note\": \"surf 不覆盖 (chain SQL 表不存在), 见顶部 Solana banner\"\n },\n {\n \"anchor\": \"DEX 主池地址\",\n \"value\": \"—\",\n \"note\": \"FDV $60,925,968\"\n }\n ],\n \"current_price_usd\": 0.060711,\n \"dex_pool_addr\": null,\n \"dex_pool_liquidity_usd\": null,\n \"alpha_5pct_depth_usd_est\": 358,\n \"lp_24h_flow\": {},\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n },\n \"decision_anchors\": [\n {\n \"anchor\": \"Alpha 5% 撤单上限\",\n \"value\": \"$358\",\n \"status\": \"🔴 超薄\"\n },\n {\n \"anchor\": \"DEX 主池 USD 流动性\",\n \"value\": \"—\",\n \"status\": \"🔴\"\n },\n {\n \"anchor\": \"LP 24h 净流\",\n \"value\": \"—\",\n \"status\": \"🟢\"\n }\n ],\n \"_pipeline_timings\": {\n \"section_a\": 19.10585558391176,\n \"rule_11\": 4.5541091822087765e-05,\n \"pool_discovery\": 4.375004209578037e-05,\n \"section_anomaly_72h\": 5.250005051493645e-05,\n \"section_cex_trace\": 0.36819320905487984,\n \"section_liq\": 0.00011833303142338991,\n \"section_tge_alloc_multi_chain\": 1.4443007910158485,\n \"dump_tracker\": 1.4958903193473816e-05,\n \"section_f_l_distribution\": 0.00010966707486659288,\n \"section_cross_sym\": 2.6540947146713734e-05,\n \"section_wash_infra\": 6.749993190169334e-06,\n \"section_flow_operators\": 1.4159595593810081e-06\n },\n \"_pipeline_total_seconds\": 20.933041292009875\n}","content_type":"application/json; charset=utf-8","language":"json","size":17038,"content_sha256":"6f8a9ccbf1faa5ddd4e110f7d6e4adbd4d8ce8064fb55800e232d36ef6820419"},{"filename":"alpha/v06/forensic_pipeline.py","content":"#!/usr/bin/env python3\n\"\"\"forensic_pipeline.py — v0.6 main orchestrator.\n\nReads a CA, runs all forensic Section helpers in 4 dependency-aware rounds,\npopulates an EvidenceGraph, and outputs `report_data_skeleton.json` with:\n - Locked fields (data + structure) fully populated.\n - Writable fields (narrative) as \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" stubs.\n\nLLM then fills the placeholders. validate_report_data.py + render_report.py\ntake it from there.\n\n## Usage (CLI)\n\n```bash\npython3 forensic_pipeline.py 0x595deaad1eb5476ff1e649fdb7efc36f1e4679cc\n# -> writes report_data_skeleton.json to ./\n```\n\nOr with explicit output:\n```bash\npython3 forensic_pipeline.py --ca 0x... --out /tmp/skeleton.json\n```\n\n## v0.6 phase 1 scope (this commit)\n\nImplements Section A (scope) + Rule 11 (backward trace + evidence graph +\nwaves_proposal). Remaining sections (F/L/M/anomaly/liq/cex/tge/alloc/etc)\nwill be wired in subsequent commits — pipeline is designed to grow without\nre-architecting.\n\nv0.6 (2026-05-24)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent / \"helpers\"))\n\nfrom _version import __version__ as SKILL_VERSION\nfrom evidence_graph import EvidenceGraph\nfrom section_a_scope import run as section_a_run\nfrom chain_router import ( # v0.7.20 / v0.7.21.7 / v0.7.21.8\n set_active_chain,\n UnsupportedChainError,\n get_active_chain,\n sql_supported as _chain_sql_supported,\n requires_holder_snapshot as _chain_requires_holder_snapshot,\n)\n\n\n# v0.7.21.8: surf onchain-sql skip-reason. Used by sections whose SQL\n# would target a non-existent `agent.{chain}_*` table (currently only\n# Solana). render_report.py and i18n surface this verbatim so the\n# user can tell \"0 hits because forensic detector didn't find anything\"\n# apart from \"section never ran because surf has no SQL coverage on\n# this chain\".\n_SOLANA_SQL_SKIP_REASON = \"surf_no_sql_solana\"\n\n\ndef _holder_snapshot_empty_rule11() -> dict:\n \"\"\"v0.7.21.8: empty rule_11 result that survives every downstream\n consumer (alloc, distribution, monitoring, render). Distinct from\n v0.7.1's mint-outside-90d degradation via `_skip_reason` so the\n report banner can tell \"deployer trace failed\" apart from \"Solana\n has no surf SQL\".\"\"\"\n return {\n \"deployer\": None,\n \"mint_evt_ref\": None,\n \"mint_ts\": None,\n \"pre_launch_receivers\": [],\n \"quiet_wallets\": [],\n \"dumper_destinations\": {},\n \"waves_proposal\": [],\n \"summary_text\": \"\",\n \"_trace_unavailable\": True,\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n }\n\n\ndef _holder_snapshot_empty_anomaly() -> dict:\n return {\n \"n_recent_events\": 0,\n \"wave3_proposal\": None,\n \"actors\": [],\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n }\n\n\ndef _holder_snapshot_empty_dump_tracking() -> dict:\n return {\n \"insider_n_wallets\": 0,\n \"confirmed_cex_tokens\": 0, \"confirmed_dex_tokens\": 0,\n \"confirmed_total_tokens\": 0, \"confirmed_total_pct\": None,\n \"confirmed_est_profit_usd\": None, \"confirmed_capped\": False,\n # v0.7.21.10: Net Sell Out fields — None on holder-snapshot chains\n # (no surf SQL coverage means no DEX swap aggregation possible).\n \"confirmed_net_sellout_usd\": None,\n \"confirmed_dex_real_usd\": None,\n \"confirmed_cex_estimated_usd\": None,\n \"apparatus_dex_twap_usd_per_token\": None,\n \"net_above_gross_pct\": None,\n \"tree_holds_tokens\": 0, \"tree_holds_pct_supply\": None,\n \"pure_insider_holds_tokens\": 0, \"pure_insider_holds_pct_supply\": None,\n \"insider_holds_tokens\": 0, \"insider_holds_pct_supply\": None,\n \"buckets_complete\": False,\n \"top_seller_addrs\": [],\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n }\n\n\ndef _holder_snapshot_empty_cross_sym() -> dict:\n return {\n \"_pipeline_source\": \"section_cross_sym\",\n \"whales\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_scope\": None,\n \"_registry_age_secs\": None,\n \"_credits_used\": 0,\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n }\n\n\ndef _holder_snapshot_empty_wash_infra() -> dict:\n return {\n \"_pipeline_source\": \"section_wash_infra\",\n \"setups\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n \"_truncated\": False,\n \"_truncation_meta\": None,\n # v0.7.21.8: render template uses `_n_candidates_scanned` in its\n # \"0 hits\" banner; supply 0 so the StrictUndefined check passes\n # before the template's skip-reason branch swaps the message text.\n \"_n_candidates_scanned\": 0,\n }\n\n\ndef _holder_snapshot_empty_flow_operators() -> dict:\n return {\n \"_pipeline_source\": \"section_flow_operators\",\n \"operators\": [],\n \"summary_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"_credits_used\": 0,\n \"_skip_reason\": _SOLANA_SQL_SKIP_REASON,\n # v0.7.21.8: render template reads this field on the 0-hit branch.\n # Provide 0 so the StrictUndefined check passes.\n \"_n_candidates_scanned\": 0,\n }\n\n\ndef _norm_addr(a) -> str:\n \"\"\"v0.7.21.7: chain-aware address normalization for set membership.\n\n EVM lowercases (case-insensitive). Solana base58 preserves case\n (System Program & SPL token addresses are case-sensitive). Returns\n \"\" for falsy / non-string inputs so caller dedupe logic still works.\n \"\"\"\n if not isinstance(a, str) or not a:\n return \"\"\n return a if get_active_chain() == \"solana\" else a.lower()\nfrom rule_11_backward_trace import run_backward_trace\nfrom dump_tracker import run as dump_tracker_run\nfrom section_anomaly_72h import run as section_anomaly_run\nfrom section_cex_trace import run as section_cex_trace_run\nfrom section_liq import run as section_liq_run, discover_main_pool\nfrom section_tge import run as section_tge_run\nfrom section_alloc import run as section_alloc_run\nfrom section_multi_chain import run as section_multi_chain_run\nfrom section_f_holders import run as section_f_holders_run\nfrom section_l_distribution import run as section_l_distribution_run\nfrom section_cross_sym import run as section_cross_sym_run # v0.7\nfrom section_wash_infra import run as section_wash_infra_run # v0.7.7\nfrom section_flow_operators import (\n run as section_flow_operators_run,\n fetch_alpha_token_cas as _fetch_alpha_token_cas,\n fetch_alpha_token_meta as _fetch_alpha_token_meta,\n) # v0.7.21 / .2\nfrom funding_source_attribution import (\n attribute_funding as _attribute_funding,\n query_mining_fed_outflows as _query_mining_fed_outflows, # v0.7.23.2\n discover_mint_authorities as _discover_mint_authorities, # v0.7.24a\n discover_high_throughput_dumpers as _discover_high_throughput_dumpers, # v0.7.24b\n discover_cex_fanout_hubs as _discover_cex_fanout_hubs, # v0.7.24e\n) # v0.7.23.1\nimport monitoring_export\nsys.path.insert(0, str(Path(__file__).parent / \"helpers\"))\nfrom i18n import t as _i18n_t, get_lang as _i18n_get_lang # v0.6.2 i18n\n\n\ndef _days_since(date_str: str | None) -> int:\n \"\"\"Days since YYYY-MM-DD until now. Returns 99999 if date is None.\"\"\"\n if not date_str:\n return 99999\n try:\n from datetime import date, datetime, timezone\n d = date.fromisoformat(date_str)\n now = datetime.now(tz=timezone.utc).date()\n return (now - d).days\n except (ValueError, TypeError):\n return 99999\n\n\ndef _derive_verdict_enum(rule11: dict, anomaly72: dict) -> tuple[str, str, str]:\n \"\"\"Pipeline-derive verdict.enum from raw signals (no LLM, no freelancing).\n\n Returns (enum, cn_label, baseline).\n\n Heuristic (intentionally simple — v0.7 may refine):\n - If Rule 11 found quiet wallets holding >= 5M tokens: EXIT_IF_HOLDING\n - If any pre_launch_receiver has dumped_pct >= 95% with non-trivial size:\n EXIT_IF_HOLDING (active distribution detected)\n - If anomaly72 has >= 10 events: EXIT_IF_HOLDING\n - If anomaly72 has >= 3 events: WAIT\n - Otherwise: ADVISORY\n \"\"\"\n # v0.7.10.3: exclude project-side / infra lockups (vesting / multisig /\n # treasury / DEX-infra / CEX-custody, per Arkham) — they are not insider\n # hoarding. Previously COLLECT's 79% vesting contract and the Binance\n # omnibus custody wallet flipped these signals True.\n quiet_with_size = any(\n r[\"dumped_pct\"] == 0\n and r[\"received_from_deployer\"] >= 5_000_000\n and not r.get(\"is_protocol_lockup\", False)\n for r in rule11.get(\"pre_launch_receivers\", [])\n )\n active_full_dumper = any(\n r.get(\"dumped_pct\") is not None and r[\"dumped_pct\"] >= 95\n and r[\"received_from_deployer\"] >= 5_000_000\n and not r.get(\"is_protocol_lockup\", False)\n for r in rule11.get(\"pre_launch_receivers\", [])\n )\n recent_n = anomaly72.get(\"n_recent_events\", 0)\n\n # v0.6.2: cn_label looked up via i18n (verdict.cn_label.\u003cENUM>).\n if quiet_with_size or active_full_dumper or recent_n >= 10:\n enum = \"EXIT_IF_HOLDING\"\n return (enum, _i18n_t(f\"verdict.cn_label.{enum}\"), \"AVOID\")\n if recent_n >= 3:\n enum = \"WAIT\"\n return (enum, _i18n_t(f\"verdict.cn_label.{enum}\"), \"WAIT\")\n enum = \"ADVISORY\"\n return (enum, _i18n_t(f\"verdict.cn_label.{enum}\"), \"ADVISORY\")\n\n\ndef _derive_action_enum(verdict_enum: str) -> str:\n \"\"\"Map verdict.enum → decision_action_block.immediate_action.action_enum.\"\"\"\n return {\n \"ENTER\": \"buy\",\n \"HOLD\": \"hold\",\n \"WAIT\": \"wait\",\n \"AVOID\": \"wait\",\n \"EXIT_IF_HOLDING\": \"sell\",\n \"ADVISORY\": \"wait\",\n \"INSUFFICIENT_DATA\": \"wait\",\n }.get(verdict_enum, \"wait\")\n\n\ndef _receiver_infra_kind(r: dict) -> str | None:\n \"\"\"'cex' / 'dex' if a rule_11 receiver is NEUTRAL infra (exchange custody / DEX\n pool — not a project insider), else None. Used everywhere a rule_11 receiver\n feeds an insider/operator set (monitoring roles, pre-launch / dumper detector\n counts, canonical-chain operator set, primary-sale seed) so listing-channel +\n liquidity wallets stay out. Vesting / multisig / treasury are NOT neutral —\n they are operator-controlled.\n\n Trusts the upstream is_cex_custody / is_dex_infra flags (now reliable: the source\n classifier `classify_protocol_lockup` excludes VC arms like \"Binance Labs\" from\n CEX and requires a yield/DEX protocol for a bare \"Vault\", so a VC investor /\n operator vault is NOT dropped), then re-derives strictly from the Arkham label\n for receivers whose enrich path left the flags unset.\"\"\"\n if r.get(\"is_cex_custody\"):\n return \"cex\"\n if r.get(\"is_dex_infra\"):\n return \"dex\"\n lbl = r.get(\"arkham_label\")\n if lbl:\n try:\n from surf_labels_probe import neutral_infra_kind\n return neutral_infra_kind(lbl, lbl, None)\n except Exception:\n return None\n return None\n\n\ndef _build_monitoring_wallets(rule11: dict, anomaly72: dict, eg,\n distribution: dict | None = None,\n cross_sym: dict | None = None,\n flow_operators: dict | None = None) -> list[dict]:\n \"\"\"Pipeline emits the monitoring_wallets list. LLM only fills .alert narrative.\n\n Priority list:\n 1. Deployer (always #1)\n 2. Each m6.row (Rule 11 quiet, partial dumper, full dumper)\n 3. Cross-sym mega-wallets (v0.7.3 fix — top-100 holders that also hold\n ≥N other Alpha tokens; deterministic forward-detection of\n cross-token operators / KOL managers / desk wallets)\n 4. OPERATOR_RELAY wallets from distribution (dumper-destinations holding\n ≥1% supply)\n 5. Top 5 distinct actors from 72h anomaly events (deduped against above)\n 6. v0.7.21.1: ALL flow_operators (60+ for PLAY) — every detected\n single-operator / cross-Alpha / async-wash wallet enters the\n monitoring list so monitoring_paste.json carries them. The\n report shows top 10 inline with narrative; the rest live in the\n paste file so the user never has to read skeleton.json.\n \"\"\"\n wallets = []\n n = 1\n\n # 1. Deployer (balance assumed 0 — pipeline doesn't query deployer balance;\n # if downstream filter needs proof, it has status_emoji + role text.)\n deployer = rule11.get(\"deployer\")\n if deployer:\n wallets.append({\n \"n\": n,\n \"addr_short\": deployer[:10],\n \"addr_full\": deployer,\n \"role\": _i18n_t(\"monitoring.role_deployer_full\"),\n \"status_emoji\": \"🟡\",\n # beta.12: balance_tokens added for monitoring_export filter\n # (drop wallets where balance==0 AND no recent activity).\n # Deployer balance is typically 0 in v0.6 pipeline output.\n \"balance_tokens\": 0,\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n n += 1\n\n # 2. Each m6 row\n seen_addrs = {_norm_addr(deployer)} if deployer else set()\n for r in rule11.get(\"pre_launch_receivers\", []):\n addr = _norm_addr(r[\"addr\"])\n if addr in seen_addrs:\n continue\n seen_addrs.add(addr)\n # Identity-aware relabel: a deployer-receiver that is actually CEX custody\n # (e.g. \"Binance Wallet\" — the Alpha listing channel) or a DEX pool (the\n # deployer seeding liquidity) is NOT a project insider and must not be shown\n # as \"项目方内幕分发\". Trust the upstream infra flags, but defensively\n # re-derive from the Arkham label too — enrich paths differ in which flags\n # they set, e.g. a bare \"V3 Pool\" can arrive with is_dex_infra unset. Setting\n # arkham_classification / entity makes monitoring_ranker canonicalize the\n # wallet to dex_pool / public_cex_hot_wallet instead of deployer/insider.\n _lbl = r.get(\"arkham_label\")\n _infra = _receiver_infra_kind(r) # 'cex' / 'dex' / None (NOT vesting/treasury)\n _w = {\n \"n\": n,\n \"addr_short\": r[\"addr\"][:10],\n \"addr_full\": r[\"addr\"],\n \"balance_tokens\": float(r.get(\"current_balance\") or 0),\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n }\n if _infra == \"cex\":\n _w[\"role\"] = _i18n_t(\"monitoring.role_cex_custody\", label=(_lbl or \"CEX\"))\n _w[\"status_emoji\"] = \"⚪\"\n _w[\"arkham_classification\"] = \"CEX_HOT_WALLET\"\n _w[\"arkham_entity_name\"] = _lbl or \"\"\n elif _infra == \"dex\":\n _w[\"role\"] = _i18n_t(\"monitoring.role_dex_pool\", label=(_lbl or \"DEX\"))\n _w[\"status_emoji\"] = \"⚪\"\n _w[\"arkham_classification\"] = \"DEX_POOL\"\n _w[\"arkham_entity_name\"] = _lbl or \"\"\n else:\n # `or 0`: unknown (unconfirmed-backfill) dumped_pct → 0 so the comparisons\n # + `dp=` kwargs can't crash on None (v0.7.13 defense-in-depth).\n dp = r.get(\"dumped_pct\") or 0\n if dp == 0.0:\n _w[\"role\"] = _i18n_t(\"monitoring.role_quiet_full_with_balance\",\n balance=r[\"current_balance\"])\n _w[\"status_emoji\"] = \"🔴\"\n elif dp >= 95.0:\n _w[\"role\"] = _i18n_t(\"monitoring.role_full_dumper_with_pct\", dp=dp)\n _w[\"status_emoji\"] = \"🟢\" # green: largely past\n else:\n _w[\"role\"] = _i18n_t(\"monitoring.role_partial_with_pct\", dp=dp)\n _w[\"status_emoji\"] = \"🟠\"\n wallets.append(_w)\n n += 1\n\n # 3. Cross-sym mega-wallets (v0.7.3) — top-100 holders that also hold\n # significant positions in other Alpha tokens. These are the highest-\n # value forward-tracking targets: cross-token operators / KOL\n # managers / desk wallets. Surface all of them (typically 0-5 per\n # token) so the user can monitor exit moves across the cohort.\n cross_sym_count = 0\n if cross_sym:\n for whale in (cross_sym.get(\"whales\") or []):\n addr = _norm_addr(whale.get(\"address\") or \"\")\n if not addr or addr in seen_addrs:\n continue\n seen_addrs.add(addr)\n pct = whale.get(\"this_token_pct\") or 0\n n_cross = whale.get(\"cross_sym_count\") or 0\n cls = whale.get(\"identity_classification_enum\") or \"UNKNOWN_WHALE\"\n wallets.append({\n \"n\": n,\n \"addr_short\": addr[:10],\n \"addr_full\": addr,\n \"role\": _i18n_t(\n \"monitoring.role_cross_sym_whale\",\n pct=pct, n_cross=n_cross, cls=cls,\n ),\n \"status_emoji\": \"🟣\", # purple — cross-token mega\n \"balance_tokens\": float(whale.get(\"this_token_balance\") or 0),\n \"cross_sym_count\": n_cross,\n \"identity_classification_enum\": cls,\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n n += 1\n cross_sym_count += 1\n\n # 4. OPERATOR_RELAY wallets from distribution (beta.3 fix: dumper\n # destinations holding ≥1% supply are insider concentration points,\n # NOT retail fan-out. Surface ALL of them, not just the top one.)\n relay_count = 0\n if distribution:\n for member in distribution.get(\"operator_relay_members\", []):\n addr = _norm_addr(member.get(\"addr\") or \"\")\n if not addr or addr in seen_addrs:\n continue\n seen_addrs.add(addr)\n pct = member.get(\"pct_of_total\") or 0\n wallets.append({\n \"n\": n,\n \"addr_short\": addr[:10],\n \"addr_full\": addr,\n \"role\": _i18n_t(\"monitoring.role_operator_relay_with_pct\", pct=pct),\n \"status_emoji\": \"🔴\",\n \"balance_tokens\": float(member.get(\"balance\") or 0),\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n n += 1\n relay_count += 1\n\n # 5. Top 5 distinct actors from 72h anomaly\n for actor_addr in (anomaly72.get(\"actors\") or [])[:50]:\n if actor_addr in seen_addrs:\n continue\n # Skip zero address (mint events) and obvious system addresses\n if actor_addr in (\"0x0000000000000000000000000000000000000000\", \"0x\"):\n continue\n seen_addrs.add(actor_addr)\n wallets.append({\n \"n\": n,\n \"addr_short\": actor_addr[:10],\n \"addr_full\": actor_addr,\n \"role\": _i18n_t(\"monitoring.role_anomaly_72h\"),\n \"status_emoji\": \"🟡\",\n # 72h actors: balance unknown but recent activity is the signal,\n # not balance. Filter keeps these regardless of balance.\n \"balance_tokens\": None,\n \"recent_activity_72h\": True,\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n n += 1\n # v0.7.3: include cross_sym_count in the cap so cross-sym whales\n # don't push out 72h actors.\n if n > 1 + len(rule11.get(\"pre_launch_receivers\", [])) + cross_sym_count + relay_count + 5:\n break\n\n # 6. v0.7.21.1: all flow_operators enter monitoring. The summary table\n # in the report shows top 10 with narrative; the rest (PLAY case ~52)\n # live here so monitoring_paste.json includes them. Without this step\n # the user would have to read skeleton.json to track the long tail,\n # which the design explicitly rules out.\n if flow_operators:\n for op in (flow_operators.get(\"operators\") or []):\n addr = _norm_addr(op.get(\"addr\") or \"\")\n if not addr or addr in seen_addrs:\n continue\n seen_addrs.add(addr)\n sub = op.get(\"sub_class\") or \"UNCLASSIFIED_SINGLE_OPERATOR\"\n n_tx = op.get(\"n_tx_this_token\") or 0\n n_alpha = op.get(\"cross_alpha_token_count\") or 0\n wallets.append({\n \"n\": n,\n \"addr_short\": addr[:10],\n \"addr_full\": addr,\n \"role\": _i18n_t(\n \"monitoring.role_flow_operator\",\n sub=sub, n_tx=n_tx, n_alpha=n_alpha,\n ),\n \"status_emoji\": \"🤖\",\n # Flow operators have low net inventory by definition —\n # the signal is FLOW not balance, so balance is OK to\n # carry as-is; monitoring_export's balance filter is\n # bypassed via the recent_activity flag below.\n \"balance_tokens\": float(op.get(\"net_balance_this_token\") or 0),\n \"recent_activity_72h\": True,\n \"sub_class\": sub,\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n n += 1\n\n return wallets\n\n\ndef _fetch_current_price_usd(pair_address: str | None) -> float | None:\n \"\"\"Best-effort current price via DexScreener pair lookup. Returns None\n on any failure — caller's pipeline shouldn't break on price-fetch errors.\n \"\"\"\n if not pair_address:\n return None\n import subprocess\n try:\n proc = subprocess.run(\n [\"curl\", \"-sS\", \"--max-time\", \"5\",\n f\"https://api.dexscreener.com/latest/dex/pairs/bsc/{pair_address}\"],\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\", check=False,\n )\n if proc.returncode != 0:\n return None\n doc = json.loads(proc.stdout)\n price = (doc.get(\"pair\") or {}).get(\"priceUsd\")\n return float(price) if price else None\n except Exception:\n return None\n\n\ndef build_skeleton(\n ca: str,\n alpha_listing_date: str | None = None,\n checkpoint_path: Path | None = None,\n) -> dict:\n \"\"\"Run full pipeline + return report_data_skeleton dict.\n\n `alpha_listing_date` optional — pipeline derives it from Section A's\n Alpha API result if not provided.\n\n v0.7.17: `checkpoint_path` — if given, the pipeline writes a partial\n skeleton dump to this path after every section completes. A long tail\n section (wash_infra_detector, cross_sym) being interrupted no longer\n leaves the user with zero artifact — they still have the partial.\n \"\"\"\n t_start = time.perf_counter()\n timings: dict[str, float] = {}\n raw_sections: dict[str, \"object\"] = {}\n\n # v0.9.4: snapshot surf credit cumulative state BEFORE any sections\n # run. Pipeline finalization (return path) subtracts to get this CA's\n # credit usage. Long-lived daemons that build_skeleton multiple times\n # need this delta — module-global counter would otherwise add prior\n # runs into the current CA's number.\n try:\n # NOTE: use bare `section_a_scope` import (NOT `helpers.section_a_scope`).\n # Python caches modules by import name — `helpers.section_a_scope` and\n # `section_a_scope` resolve to TWO module instances even though backed\n # by the same .py file. parallel_surf's lazy hook writes to the bare\n # name; pipeline must read the same instance.\n from section_a_scope import _surf_credit_snapshot as _credit_snap\n _credits_before_snapshot = _credit_snap()\n except Exception:\n _credits_before_snapshot = {\n \"credits\": 0.0, \"calls\": 0, \"seconds\": 0.0, \"attempts\": 0,\n }\n\n # v0.7.20 adversarial review Round 2 HIGH fix: reset the chain router to a known\n # default at the START of every build_skeleton call. In a single-shot\n # CLI this is a no-op (router defaults to \"bsc\"), but if a long-lived\n # process (daemon / batch wrapper) reuses the module between runs, a\n # prior call's chain leaks into this run. Resetting up front means\n # every build_skeleton starts from the same baseline regardless of\n # prior state, then routes to the Alpha-API chain after Section A.\n set_active_chain(\"bsc\")\n\n def _ck(name: str, value):\n \"\"\"Stash a section's raw output + (best-effort) write the partial\n skeleton to disk. Errors are logged but never block the pipeline.\n\n v0.7.17 audit MEDIUM #1: atomic write (temp + rename) so a\n SIGKILL during the write never leaves a torn JSON file that a\n downstream inspector would parse as gibberish.\n\n v0.7.23: emit a one-line per-section timing breakdown to stderr\n so live monitoring sees per-section cost without parsing JSON.\n Format: `[timing] section_x: 12.3s (total 45.6s)`.\n \"\"\"\n raw_sections[name] = value\n section_t = timings.get(name)\n if section_t is not None:\n total_t = time.perf_counter() - t_start\n print(\n f\"[timing] {name}: {section_t:.1f}s \"\n f\"(total {total_t:.1f}s)\",\n file=sys.stderr, flush=True,\n )\n if checkpoint_path is None:\n return\n try:\n partial = {\n \"_status\": \"in_progress\",\n \"_last_section_completed\": name,\n \"_pipeline_timings\": dict(timings),\n \"_raw_sections\": raw_sections,\n }\n data = (\n json.dumps(partial, ensure_ascii=False, indent=2, default=str)\n .encode(\"utf-8\")\n )\n tmp = checkpoint_path.with_suffix(checkpoint_path.suffix + \".tmp\")\n tmp.write_bytes(data)\n os.replace(tmp, checkpoint_path) # atomic on POSIX + Windows\n except Exception as e:\n print(f\"[checkpoint] write failed ({name}): {e}\",\n file=sys.stderr, flush=True)\n\n # Initialize evidence graph (single instance shared across all sections)\n eg = EvidenceGraph()\n\n # ---------- Round 0: Section A (scope confirmation + Spot graduation) ----------\n t = time.perf_counter()\n scope = section_a_run(ca)\n timings[\"section_a\"] = time.perf_counter() - t\n _ck(\"section_a\", scope)\n\n if not scope.get(\"scope_ok\"):\n return {\n \"_status\": \"abort\",\n \"_reason\": scope.get(\"reason\"),\n \"_detail\": scope.get(\"detail\"),\n \"_timings\": timings,\n }\n\n # v0.7.20: route subsequent SQL helpers to the chain Alpha API reported\n # for this token. Pre-v0.7.20 every helper hardcoded `agent.bsc_transfers`,\n # so a Base / Ethereum / Arbitrum / Polygon / Optimism / Solana token\n # silently fell through to BSC tables and produced an empty forensic\n # report. The PLAY (chainId 8453, Base) rerun is what surfaced this gap;\n # see [[binance-alpha-v0720]] for the full incident write-up. Failure\n # to map the chain_id is fail-loud (raises UnsupportedChainError) — a\n # silent BSC fallback would re-create the exact bug we're fixing.\n #\n # adversarial review Round 2 MEDIUM fix: chain_id MUST be present here. If Section A\n # returned `scope_ok=True` but no chain_id, that's an Alpha API contract\n # violation we cannot route around. Treat as abort, not as \"use whatever\n # the router was set to last run\" (which would silently route to BSC\n # default in a fresh process, or to the prior run's chain in a long-\n # lived process — exactly the leak adversarial review round 2 flagged).\n chain_id = scope.get(\"chain_id\")\n if not chain_id:\n return {\n \"_status\": \"abort\",\n \"_reason\": \"missing_chain_id\",\n \"_detail\": (\n \"Section A returned scope_ok but no chain_id — Alpha API \"\n \"contract violation. Cannot route SQL safely.\"\n ),\n \"_timings\": timings,\n }\n try:\n active_chain = set_active_chain(chain_id)\n except UnsupportedChainError as e:\n return {\n \"_status\": \"abort\",\n \"_reason\": \"unsupported_chain\",\n \"_detail\": str(e),\n \"_timings\": timings,\n }\n scope[\"_active_chain\"] = active_chain\n # v0.7.21.8: Solana has no surf onchain-sql coverage (no\n # `agent.solana_*` tables; verified 2026-06-02). Pipeline switches to\n # HOLDER_SNAPSHOT mode — SQL-based detectors (rule_11 / dump_tracker /\n # wash_infra / flow_operators / cross_sym / anomaly_72h / liq SQL /\n # tge LP-first-trade / l_distribution dumper-destinations) short-\n # circuit with `_skip_reason=\"surf_no_sql_solana\"`. The REST-based\n # sections (section_a Alpha API, section_f_holders token-holders,\n # section_alloc, monitoring_export) still run normally.\n holder_snapshot_mode = _chain_requires_holder_snapshot()\n scope[\"_holder_snapshot_mode\"] = holder_snapshot_mode\n scope[\"_sql_supported\"] = _chain_sql_supported()\n\n # If caller didn't supply alpha_listing_date, use Alpha API's\n if alpha_listing_date is None:\n alpha_listing_date = scope.get(\"alpha_listing_date_utc\")\n\n # ---------- Round 1: Rule 11 backward trace ----------\n # v0.7.21.8: skip entirely on chains without surf onchain-sql (Solana).\n # Pre-v0.7.21.8 the SQL hit UNKNOWN_TABLE error and rule_11 fell into\n # its `error` branch, which then degraded to the same empty payload —\n # but spent ~30s waiting for surf to return the error first. Short-\n # circuiting saves that round-trip and tags the skip with a distinct\n # reason so the report can explain it accurately.\n t = time.perf_counter()\n if holder_snapshot_mode:\n rule11 = _holder_snapshot_empty_rule11()\n else:\n rule11 = run_backward_trace(\n ca=ca,\n alpha_listing_date=alpha_listing_date,\n evidence_graph=eg, # shared instance\n )\n timings[\"rule_11\"] = time.perf_counter() - t\n _ck(\"rule_11\", rule11)\n\n # v0.7.1: deployer-trace failure (mint event outside 90d window — common\n # for tokens listed >3 months ago) used to abort the entire pipeline.\n # Now we degrade gracefully: replace with empty trace, let downstream\n # sections (anomaly_72h, liq, cross_sym, holdings, ...) continue.\n # Cross-sym whale detection in particular does NOT need deployer trace.\n if \"error\" in rule11:\n rule11 = {\n \"deployer\": None,\n \"mint_evt_ref\": None,\n \"mint_ts\": None,\n \"pre_launch_receivers\": [],\n \"quiet_wallets\": [],\n \"dumper_destinations\": {},\n \"waves_proposal\": [],\n \"summary_text\": \"\",\n \"_trace_unavailable\": True, # internal flag for downstream\n }\n\n # ---------- Round 2a: discover main pool + price (cheap, feeds Section ANOMALY usd_value) ----------\n t = time.perf_counter()\n pool_info = discover_main_pool(ca)\n price_usd = pool_info.get(\"price_usd\")\n timings[\"pool_discovery\"] = time.perf_counter() - t\n _ck(\"pool_discovery\", pool_info)\n\n # ---------- Round 2b: Section ANOMALY (recent 24-72h, events get usd_value) ----------\n # v0.7.21.8: SQL-only — skip on Solana.\n t = time.perf_counter()\n if holder_snapshot_mode:\n anomaly72 = _holder_snapshot_empty_anomaly()\n else:\n anomaly72 = section_anomaly_run(\n ca=ca,\n evidence_graph=eg,\n threshold_token_amount=100_000,\n price_usd=price_usd,\n )\n timings[\"section_anomaly_72h\"] = time.perf_counter() - t\n _ck(\"section_anomaly_72h\", anomaly72)\n\n # ---------- Round 3: Section CEX-TRACE (Binance/Aster/Bitget perp probe) ----------\n # Refines tier_classification from S1 stub → real S1/S2/S3\n t = time.perf_counter()\n cex_trace = section_cex_trace_run(\n symbol=scope[\"symbol\"],\n alpha_listing_date=alpha_listing_date,\n )\n timings[\"section_cex_trace\"] = time.perf_counter() - t\n _ck(\"section_cex_trace\", cex_trace)\n\n # ---------- Round 4: Section LIQ (Alpha 5% depth + DEX pool + LP 24h flow) ----------\n t = time.perf_counter()\n liq = section_liq_run(\n ca=ca,\n symbol=scope[\"symbol\"],\n alpha_vol_24h_usd=scope.get(\"alpha_vol_24h_usd\"),\n # v0.7.16: Alpha price fallback when surf project-detail is unavailable.\n alpha_price_usd=scope.get(\"alpha_price_usd\"),\n # v0.7.10: pass scope's surf-realtime LP / token_info so section_liq\n # reads pool / price / vol from already-fetched data instead of\n # calling DexScreener.\n scope_chain_lp=scope.get(\"chain_lp_realtime\"),\n scope_realtime_token_info=scope.get(\"realtime_token_info\"),\n primary_chain=scope.get(\"primary_chain\"),\n )\n timings[\"section_liq\"] = time.perf_counter() - t\n _ck(\"section_liq\", liq)\n\n # ---------- Round 5: section_tge + section_multi_chain + section_f_holders + section_alloc ----------\n # v0.7.20.2: section_f_holders moved EARLIER (was in Round 6) so its\n # burn_balance / burn_pct_of_supply can feed into section_alloc as\n # a dedicated \"已销毁\" row. Pre-v0.7.20.2 the burn tokens (PLAY: 1B\n # = 20% of supply at 0xdead) were silently dropped from top_holders\n # and the user could only spot the 20% sitting at 0xdead by reading\n # the cross_sym whales section — where they wrongly appeared as a\n # \"candidate operator\", because 0xdead matches across most Alpha\n # tokens (every burn-and-deploy template uses the same address).\n t = time.perf_counter()\n tge = section_tge_run(\n ca=ca,\n alpha_listing_ts_ms=scope.get(\"alpha_listing_ts_ms\"),\n pool_addr=liq.get(\"dex_pool_addr\"),\n current_price_usd=liq.get(\"current_price_usd\"),\n )\n holders = section_f_holders_run(\n ca=ca,\n listing_date=scope.get(\"alpha_listing_date_utc\"),\n # v0.7.21.9: on holder-snapshot chains prefer the on-chain\n # `chain_total_supply_ui` (Solana RPC `getTokenSupply`) over the\n # Alpha API snapshot. They drift on inflationary mints — JELLYJELLY\n # ships 999,966,701 on-chain vs 999,999,099 Alpha — and using the\n # chain value keeps the % math consistent with the normalised\n # balance below.\n total_supply=(\n scope.get(\"chain_total_supply_ui\")\n if scope.get(\"chain_total_supply_ui\") is not None\n else scope.get(\"total_supply\")\n ),\n limit=50,\n # v0.7.21.9: SPL decimals so the helper can divide raw lamport\n # balances surf token-holders returns. None on EVM (already\n # normalised) and on Solana-RPC fallback (then we surface raw\n # lamports with no divisor — same as v0.7.21.8 behaviour).\n chain_decimals=scope.get(\"chain_decimals\"),\n )\n alloc = section_alloc_run(\n total_supply=scope.get(\"total_supply\"),\n circulating_supply=scope.get(\"circulating_supply\"),\n rule11=rule11,\n current_price_usd=liq.get(\"current_price_usd\"),\n burn_balance=holders.get(\"burn_balance\"),\n burn_pct_of_supply=holders.get(\"burn_pct_of_supply\"),\n )\n multi_chain = section_multi_chain_run(\n chain_label=scope[\"chain_label\"],\n total_supply=scope.get(\"total_supply\"),\n # v0.7.14 (issue #1): align MULTI-CHAIN with the banner's primary_chain\n # so a cross-chain mirror (ZEST → stacks) isn't reported as BSC single-chain.\n # v0.7.21.8: prefer the active SQL-routing chain on holder-snapshot\n # chains so we don't report a Base wrapper for a Solana token.\n primary_chain=(\n get_active_chain() if holder_snapshot_mode\n else scope.get(\"primary_chain\")\n ),\n holder_snapshot_mode=holder_snapshot_mode,\n )\n timings[\"section_tge_alloc_multi_chain\"] = time.perf_counter() - t\n _ck(\"section_tge_alloc_multi_chain\", {\"tge\": tge, \"alloc\": alloc, \"multi_chain\": multi_chain, \"holders\": holders})\n\n # ---------- Phase 2: entity-level sell tracking (真实派发 Sell-to-CEX/DEX) ----------\n # Post-processes rule_11's already-fetched graph (0 new transfer SQL):\n # classifies dumper destinations via Arkham, sums flow into CEX/DEX sell\n # terminals, values via one market-price series query. Distinct from\n # wallet-level dumped_pct (which counts any transfer as \"distributed\").\n t = time.perf_counter()\n if holder_snapshot_mode:\n # v0.7.21.8: dump_tracker is entirely SQL-based — skip on Solana.\n dump_tracking = _holder_snapshot_empty_dump_tracking()\n else:\n try:\n dump_tracking = dump_tracker_run(\n rule11=rule11,\n ca=ca,\n symbol=scope.get(\"symbol\"),\n listing_ts_ms=scope.get(\"alpha_listing_ts_ms\"),\n listing_date=scope.get(\"alpha_listing_date_utc\"),\n circulating_supply=scope.get(\"circulating_supply\"),\n total_supply=scope.get(\"total_supply\"),\n # v1.2.5: reliable spot price to sanity-bound the DEX-derived\n # per-token price (guards non-18-decimal USD overflow, e.g. SLX).\n spot_price_usd=liq.get(\"current_price_usd\"),\n )\n except Exception as e:\n import sys as _sys\n print(f\"[dump_tracker] failed (non-fatal): {e}\", file=_sys.stderr)\n # v0.7.12: fallback must use the CURRENT Opt1 keys (not legacy\n # clusters/total_sold_*) and carry _error so a hard failure surfaces as\n # \"data unavailable\", not a silent \"0 confirmed sold\" (adversarial review LOW #6).\n dump_tracking = {\n \"insider_n_wallets\": 0,\n \"confirmed_cex_tokens\": 0, \"confirmed_dex_tokens\": 0,\n \"confirmed_total_tokens\": 0, \"confirmed_total_pct\": None,\n \"confirmed_est_profit_usd\": None, \"confirmed_capped\": False,\n # v0.7.21.10: Net Sell Out fields — None on dump_tracker failure\n # so render template `is not none` checks gracefully skip rows.\n \"confirmed_net_sellout_usd\": None,\n \"confirmed_dex_real_usd\": None,\n \"confirmed_cex_estimated_usd\": None,\n \"apparatus_dex_twap_usd_per_token\": None,\n \"net_above_gross_pct\": None,\n # v0.7.19.4: include the new split fields in the fallback\n # so render template can rely on them being present.\n \"tree_holds_tokens\": 0, \"tree_holds_pct_supply\": None,\n \"pure_insider_holds_tokens\": 0, \"pure_insider_holds_pct_supply\": None,\n \"insider_holds_tokens\": 0, \"insider_holds_pct_supply\": None,\n \"buckets_complete\": False, \"_error\": str(e)[:200],\n }\n timings[\"dump_tracker\"] = time.perf_counter() - t\n _ck(\"dump_tracker\", dump_tracking)\n\n # ---------- Round 6: section_l_distribution (Phase B.4) ----------\n # v0.7.20.2: section_f_holders moved into Round 5 so its burn_balance\n # can feed section_alloc. Round 6 keeps section_l_distribution which\n # consumes the same top_holders.\n t = time.perf_counter()\n distribution = section_l_distribution_run(\n top_holders=holders[\"top_holders\"],\n rule11=rule11,\n dex_pool_addr=liq.get(\"dex_pool_addr\"),\n # v0.7.21.9: same chain_total_supply preference as section_f_holders\n # so the % math stays consistent (both helpers sum normalised\n # balances now).\n total_supply=(\n scope.get(\"chain_total_supply_ui\")\n if scope.get(\"chain_total_supply_ui\") is not None\n else scope.get(\"total_supply\")\n ),\n current_price_usd=liq.get(\"current_price_usd\"),\n eg=eg,\n )\n timings[\"section_f_l_distribution\"] = time.perf_counter() - t\n _ck(\"section_f_l_distribution\", {\"holders\": holders, \"distribution\": distribution})\n\n # ---------- Round 7: section_cross_sym (Phase v0.7) ----------\n # Forward-detect cross-sym whale candidates (e.g. 0xb5893a55 type:\n # single wallet that's a top-100 holder across many Alpha tokens).\n # Side-effect: also appends the current report's m6_rows to the\n # local pre_launch_insider index for future cross-sym lookups.\n t = time.perf_counter()\n # Collect counterparty class hints used by identity_classifier signature SQL\n cex_hot_addrs = set()\n for h in holders.get(\"top_holders\", []):\n if h.get(\"entity_type\") == \"cex\":\n addr = _norm_addr(h.get(\"address\") or \"\")\n if addr:\n cex_hot_addrs.add(addr)\n dex_pool_addrs = set()\n if liq.get(\"dex_pool_addr\"):\n dex_pool_addrs.add(_norm_addr(liq[\"dex_pool_addr\"]))\n deployer_addrs = set()\n if rule11.get(\"deployer\"):\n deployer_addrs.add(_norm_addr(rule11[\"deployer\"]))\n op_relay_addrs = [\n m.get(\"addr\", \"\") for m in (distribution.get(\"operator_relay_members\") or [])\n if m.get(\"addr\")\n ]\n # v0.8.4.9: cross_sym 整段 skip (render 已删, registry refresh 也 disabled).\n # 抓到的 wallet 多是 router / MEV bot, 真\"跨币种庄家\"罕见 (\u003c 5%).\n # 节省 ~30 credits/run + cold-start 190 credits (registry build).\n cross_sym = _holder_snapshot_empty_cross_sym()\n timings[\"section_cross_sym\"] = time.perf_counter() - t\n _ck(\"section_cross_sym\", cross_sym)\n\n # ---------- Round 7b: section_wash_infra (v0.7.7) ----------\n # 5-step pure on-chain wash infrastructure detection (no Arkham label\n # dependency). Replaces v0.7.5's narrow wash_distributor_probe (which\n # was empirically misclassifying DEX pools as operator EOAs). Scans\n # top-100 holders minus benign infra (CEX hot wallets, DEX pools,\n # deployer) for the X / P / Q wash triplet signature.\n # v0.8.4.9.9: section_wash_infra 整段 skip (render 已删).\n # 用户 review: Alpha 类 token 用户默认 expect wash trade, \"对敲占 X%\"\n # 数字对决策无价值. 庄家筹码状况 / 内幕变现 / mint_authority 持仓\n # 都跟 wash_infra detector 无关. 节省 ~350 credits/token (~70%\n # wash budget) ≈ $1.75/token. 跟 v0.8.4.9 删 flow_operators +\n # cross_sym 同 pattern.\n t = time.perf_counter()\n wash_infrastructure = _holder_snapshot_empty_wash_infra()\n timings[\"section_wash_infra\"] = time.perf_counter() - t\n _ck(\"section_wash_infra\", wash_infrastructure)\n\n # ---------- Round 7c: section_flow_operators (v0.7.21) ----------\n # Flow-based wallet detector — catches cross-Alpha trading operators\n # and single-operator asynchronous wash (P→X→Q across separate txs).\n # Both are invisible to v0.7.20.2: cross_sym is stock-based, wash_infra\n # requires atomic_pair_ratio ≥ 0.85. See v0721_DESIGN.md.\n #\n # Candidate union = top-50 holders (already fetched) + dump_tracker's\n # top-200 DEX sellers (already fetched). Three batch SQL queries on\n # the union (no per-candidate calls) + one Arkham label batch.\n # Excluded set mirrors cross_sym (m6 / op-relay / burn / cex_hot).\n # v0.8.4.9: section_flow_operators 整段 skip (render 已删).\n # 用户 review: 实际抓的 wallet 多是 PancakeSwap router MEV/sandwich bot,\n # 不是项目方操盘, 对决策无价值. 节省 ~10 credits + 1.5-2s wall time.\n t = time.perf_counter()\n flow_operators_block = _holder_snapshot_empty_flow_operators()\n timings[\"section_flow_operators\"] = time.perf_counter() - t\n _ck(\"section_flow_operators\", flow_operators_block)\n\n # ---------- Round 8: Funding source attribution (v0.7.23.1) ----------\n # Reverse-direction classifier for the high-value addresses surfaced by\n # earlier rounds (wash_infra P/Q/X, flow_operators, rule_11 receivers,\n # top_holders). One batch SQL: classifies each address's INCOMING token\n # by source — mint (= 0x0, e.g. bridge / staking / airdrop mint contract,\n # i.e. \"矿币\" distribution), dex_buy (= known DEX pair pool), or p2p\n # (everything else, including CEX withdraws that we cannot label).\n #\n # The output `mint_pct` tells retail whether a wallet's balance came from\n # mining the token (high mint% = operator who mined an airdrop / sockpuppet\n # cluster) or from real DEX buy pressure (high dex_buy% = retail PnL-taker).\n # This is independent of how the token's distribution mechanism is\n # implemented — works the same for standard deployer-anchored tokens\n # (COLLECT, LAB), bridge mint authority tokens (Humanity / H), and\n # mining-airdrop tokens (Aethir-class). Replaces the abandoned forward\n # cluster-hub trace (see v0.7.23.1 design doc).\n t = time.perf_counter()\n high_value_addrs: list[str] = []\n seen_hv: set[str] = set()\n\n def _add_hv(addr_raw):\n if not addr_raw:\n return\n a = _norm_addr(addr_raw)\n if not a or a in seen_hv:\n return\n seen_hv.add(a)\n high_value_addrs.append(a)\n\n # wash_infra setup actors (P/Q + executor X)\n for _setup in (wash_infrastructure.get(\"setups\") or []):\n for _k in (\"maker_buy_P\", \"maker_sell_Q\", \"executor_X\"):\n _add_hv(_setup.get(_k))\n # flow_operators full operator set\n for _op in (flow_operators_block.get(\"operators\") or []):\n _add_hv(_op.get(\"addr\"))\n # rule_11 m6 receivers (pre-launch insiders)\n for _r in (rule11.get(\"pre_launch_receivers\") or []):\n _add_hv(_r.get(\"addr\"))\n # dump_tracker top-seller addrs (real dumpers surfaced by dex_sell_profile —\n # may not overlap with m6 receivers when the token uses non-standard\n # distribution; audit M2 fix).\n for _seller in (dump_tracking.get(\"top_seller_addrs\") or []):\n _add_hv(_seller)\n # top holders top-30 (real-time concentration)\n for _h in (holders.get(\"top_holders\") or [])[:30]:\n _add_hv(_h.get(\"addr\"))\n\n # Pre-cap snapshot for truncation visibility (audit M1 fix).\n _hv_pre_cap = len(high_value_addrs)\n\n # DEX pair pool addresses from realtime LP detector (section_a)\n dex_pair_addrs: list[str] = []\n _chain_lp = scope.get(\"chain_lp_realtime\") or {}\n if isinstance(_chain_lp, dict):\n for _chain_data in _chain_lp.values():\n if isinstance(_chain_data, dict):\n _top = _chain_data.get(\"top_pool_addr\")\n if _top:\n dex_pair_addrs.append(_top)\n\n # date_floor: listing - 365d. Hardcoded window covers the vast majority of\n # pre/post-launch insider activity without bloating the SQL beyond surf's\n # 30s budget. Floor 2020-01-01 only if no listing date available.\n _listing_iso = scope.get(\"alpha_listing_date_utc\")\n if _listing_iso:\n try:\n from datetime import date as _date, timedelta as _td\n _ld = _date.fromisoformat(_listing_iso[:10])\n _date_floor = (_ld - _td(days=365)).isoformat()\n except (ValueError, TypeError):\n _date_floor = \"2020-01-01\"\n else:\n _date_floor = \"2020-01-01\"\n\n try:\n if holder_snapshot_mode:\n # Solana: SQL detector skipped (no bsc_transfers analog).\n funding_attribution = {\n \"attributions\": {}, \"summary\": {\n \"n_addrs_queried\": 0, \"n_addrs_with_data\": 0,\n \"n_mining_fed\": 0, \"n_dex_fed\": 0,\n \"n_p2p_fed\": 0, \"n_cex_fed\": 0,\n },\n \"_skipped\": _SOLANA_SQL_SKIP_REASON,\n }\n elif not high_value_addrs:\n funding_attribution = {\n \"attributions\": {}, \"summary\": {\n \"n_addrs_queried\": 0, \"n_addrs_with_data\": 0,\n \"n_mining_fed\": 0, \"n_dex_fed\": 0,\n \"n_p2p_fed\": 0, \"n_cex_fed\": 0,\n },\n \"_skipped\": \"no_high_value_addrs_surfaced\",\n }\n else:\n funding_attribution = _attribute_funding(\n ca=scope[\"contract_address\"],\n high_value_addrs=high_value_addrs,\n dex_pair_addrs=dex_pair_addrs,\n cex_addrs=None, # v0.7.23.1: CEX label set TBD in v0.7.24 — for\n # now CEX withdraws fall under p2p (the report\n # surfaces this caveat in the funding section).\n date_floor=_date_floor,\n max_addrs=200,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] failed (non-fatal): {_e}\", file=_sys.stderr)\n funding_attribution = {\n \"attributions\": {}, \"summary\": {\n \"n_addrs_queried\": len(high_value_addrs),\n \"n_addrs_with_data\": 0, \"n_mining_fed\": 0,\n \"n_dex_fed\": 0, \"n_p2p_fed\": 0, \"n_cex_fed\": 0,\n },\n \"_error\": str(_e)[:200],\n }\n\n # v0.7.23.2: follow-up — for each mining-fed address surfaced by\n # attribute_funding, query its DEX dump activity + top outflow destinations.\n # This recovers the (a)(b) confirmed-sell signal that dump_tracker misses\n # when rule_11's m6 trace is empty (mining/bridge token case: H, Aethir-\n # class, Polyhedra-class). The data is attached to funding_attribution\n # so the render template can show \"operator X dumped Y tokens for $Z USD\"\n # alongside the mint% column.\n if isinstance(funding_attribution, dict) and funding_attribution.get(\"attributions\"):\n _mining_fed_addrs = [\n a for a, v in funding_attribution[\"attributions\"].items()\n if v.get(\"is_mining_fed\")\n ]\n if _mining_fed_addrs:\n try:\n _mfo = _query_mining_fed_outflows(\n ca=scope[\"contract_address\"],\n mining_fed_addrs=_mining_fed_addrs,\n date_floor=_date_floor,\n max_addrs=30,\n )\n funding_attribution[\"mining_fed_outflows\"] = _mfo\n import sys as _sys\n _mfo_sum = _mfo.get(\"summary\", {})\n print(\n f\"[funding_attribution] mining_fed_outflows: \"\n f\"{len(_mining_fed_addrs)} addrs queried, \"\n f\"{_mfo_sum.get('n_addrs_with_dex_sells', 0)} with DEX sells, \"\n f\"total ${_mfo_sum.get('total_dex_sold_usd', 0):,.0f} dumped\",\n file=_sys.stderr,\n )\n except Exception as _e:\n import sys as _sys\n print(\n f\"[funding_attribution] mining_fed_outflows failed (non-fatal): {_e}\",\n file=_sys.stderr,\n )\n funding_attribution[\"mining_fed_outflows\"] = {\n \"per_addr\": {}, \"summary\": {}, \"_error\": str(_e)[:200],\n }\n\n # v0.7.24a: mint authority discovery + their self-DEX-dump.\n # mint_authorities are bridge/staking/airdrop contracts that physically\n # receive 0x0 transfers (issue new supply). They typically don't appear\n # in funding_attribution.high_value_addrs (which surveys detector-\n # surfaced wallets), so the v0.7.23.x mining_fed_outflows path misses\n # them entirely. H run uncovered 0x6aa22cb8 (bridge) minted 132.5B\n # over 30d and self-DEX-dumped 19.8B (≈ $3M) — invisible until now.\n #\n # Exclusion logic: skip addrs already covered by mining_fed_outflows\n # (those got rolled into the (b) row) + skip deployer (rule_11 path).\n # Remaining authorities go through the same query_mining_fed_outflows\n # helper to fetch DEX sells + outflows + current balance.\n if isinstance(funding_attribution, dict) and scope.get(\"contract_address\") and not holder_snapshot_mode:\n _mining_fed_addrs_set = {\n a for a, v in (funding_attribution.get(\"attributions\") or {}).items()\n if v.get(\"is_mining_fed\")\n }\n _deployer_addr = (rule11.get(\"deployer\") or \"\")\n _exclude_for_auth = sorted(_mining_fed_addrs_set | ({_deployer_addr} if _deployer_addr else set()))\n try:\n _auth_disco = _discover_mint_authorities(\n ca=scope[\"contract_address\"],\n date_floor=_date_floor,\n exclude_addrs=_exclude_for_auth,\n top_n=10,\n min_pct_supply=0.001, # drop authorities \u003c 0.1% nominal supply\n total_supply=scope.get(\"total_supply\"),\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] mint_authority discovery failed (non-fatal): {_e}\",\n file=_sys.stderr)\n _auth_disco = {\"authorities\": [], \"summary\": {}, \"_error\": str(_e)[:200]}\n\n funding_attribution[\"mint_authorities\"] = _auth_disco\n\n # v0.7.24a.1: Arkham label resolution for mint authorities. Most are\n # v0.7.24e.2 (A3): mint authority labels deferred to single\n # consolidated batch at end of Round 8. Skip inline call — flagged\n # for post-hoc apply.\n\n # Step 2: for the non-excluded authorities, fetch their dump activity\n # using the same query_mining_fed_outflows helper (the per-addr +\n # summary + current_balance schema is identical).\n _auth_addrs_for_dump = [\n a[\"addr\"] for a in (_auth_disco.get(\"authorities\") or [])\n if not a.get(\"is_excluded\")\n ]\n if _auth_addrs_for_dump:\n try:\n _auth_dump = _query_mining_fed_outflows(\n ca=scope[\"contract_address\"],\n mining_fed_addrs=_auth_addrs_for_dump,\n date_floor=_date_floor,\n max_addrs=30,\n )\n # Stitch the disco mint_pct_supply data into per_addr so\n # render template can show \"minted N% supply, dumped X\".\n _disco_by_addr = {a[\"addr\"]: a for a in (_auth_disco.get(\"authorities\") or [])}\n for _addr, _v in (_auth_dump.get(\"per_addr\") or {}).items():\n _d = _disco_by_addr.get(_addr) or {}\n _v[\"total_minted\"] = _d.get(\"total_minted\") or 0.0\n _v[\"mint_pct_supply\"] = _d.get(\"mint_pct_supply\")\n _v[\"n_mints\"] = _d.get(\"n_mints\") or 0\n funding_attribution[\"mint_authority_dumps\"] = _auth_dump\n import sys as _sys\n _mad_sum = _auth_dump.get(\"summary\", {})\n print(\n f\"[funding_attribution] mint_authority_dumps: \"\n f\"{len(_auth_addrs_for_dump)} authorities, \"\n f\"{_mad_sum.get('n_addrs_with_dex_sells', 0)} with DEX sells, \"\n f\"total ${_mad_sum.get('total_dex_sold_usd', 0):,.0f} self-dumped\",\n file=_sys.stderr,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] mint_authority_dumps failed (non-fatal): {_e}\",\n file=_sys.stderr)\n funding_attribution[\"mint_authority_dumps\"] = {\n \"per_addr\": {}, \"summary\": {}, \"_error\": str(_e)[:200],\n }\n\n # v0.7.24b: high-throughput dump trace — find operator wallets that\n # received a meaningful allocation (1M-5% nominal supply) and dumped\n # ~all of it (balance ≈ 0) via many txs (n_tx ≥ 1000). Catches the\n # sss_crypto Twitter thread profile (0x47a6e4e1: 30M tokens / 79k tx\n # cleared out) which all v0.7.23.x detectors missed. Excludes addrs\n # already covered by mining_fed + mint_authority + deployer.\n if isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n _ht_exclude = set(_exclude_for_auth) | {a[\"addr\"] for a in (_auth_disco.get(\"authorities\") or []) if not a.get(\"is_excluded\")}\n try:\n _ht_disco = _discover_high_throughput_dumpers(\n ca=scope[\"contract_address\"],\n date_floor=_date_floor,\n exclude_addrs=sorted(_ht_exclude),\n min_throughput=1_000_000.0,\n max_balance_frac=0.05,\n min_n_tx=1000,\n top_n=100,\n total_supply=scope.get(\"total_supply\"),\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] high_throughput discovery failed (non-fatal): {_e}\",\n file=_sys.stderr)\n _ht_disco = {\"dumpers\": [], \"summary\": {}, \"_error\": str(_e)[:200]}\n\n # v0.7.24e.2 (A3): high_throughput dumper labels deferred to\n # single consolidated batch at end of Round 8. is_infra + summary\n # recompute also deferred.\n _operator_dumpers = [\n d for d in (_ht_disco.get(\"dumpers\") or [])\n if not d.get(\"is_excluded\")\n ]\n _ht_disco[\"summary\"] = {\n \"n_dumpers\": len(_operator_dumpers),\n \"total_throughput\": sum(d.get(\"total_in\", 0) for d in _operator_dumpers),\n \"n_dumpers_pre_filter\": _ht_disco.get(\"summary\", {}).get(\"n_dumpers\", 0),\n }\n funding_attribution[\"high_throughput_dumpers\"] = _ht_disco\n import sys as _sys\n print(\n f\"[funding_attribution] high_throughput_dumpers: \"\n f\"{len(_operator_dumpers)} operator dumpers \"\n f\"(pre-infra-filter {len((_ht_disco.get('dumpers') or []))}), \"\n f\"total throughput {sum(d.get('total_in', 0) for d in _operator_dumpers):,.0f} tokens\",\n file=_sys.stderr,\n )\n\n # v0.7.24a.1: resolve Arkham labels for top destinations of mining-fed\n # v0.7.24e.2 (A3): destination labels deferred to consolidated batch at\n # end of Round 8. Original logic was a separate resolve_labels call.\n # Now folded into the mega-batch + apply step below.\n if False and isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n try:\n from surf_labels_probe import resolve_labels as _resolve_labels\n _dest_addrs: set[str] = set()\n for _mfo_key in (\"mining_fed_outflows\", \"mint_authority_dumps\"):\n _mfo_dict = funding_attribution.get(_mfo_key) or {}\n for _wallet, _wallet_data in (_mfo_dict.get(\"per_addr\") or {}).items():\n for _d in (_wallet_data.get(\"top_destinations\") or []):\n _dest = (_d.get(\"dest\") or \"\").lower()\n if _dest:\n _dest_addrs.add(_dest)\n if _dest_addrs:\n _dest_labels = _resolve_labels(sorted(_dest_addrs))\n pass # deferred (A3)\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] destination label resolve failed (non-fatal): {_e}\",\n file=_sys.stderr)\n\n # v0.7.24e: CEX deposit fan-out detection. sss_crypto Twitter thread\n # documented BEAT pattern (Gate.io hot wallet → 1 hub → 10 sub-wallets\n # each holding 0.1%+ supply). Operators pull allocation from CEX, route\n # through a hub, fan out to dispense the top100 concentration signal.\n # No existing detector catches this — rule_11 (deployer mint), wash_infra\n # (P/Q atomic), flow_operators (cross-Alpha 5+), mining_fed (mint primary),\n # mint_authority (from=0x0), high_throughput (n_tx >= 1000) all miss.\n # SSS_crypto BEAT verification: 2 confirmed hubs (KuCoin Deposit / MEXC\n # Deposit), 29 fan-out recipients, 1.88M tokens from CEX.\n if isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n try:\n _fanout_disco = _discover_cex_fanout_hubs(\n ca=scope[\"contract_address\"],\n date_floor=_date_floor,\n min_recipients=5, max_recipients=50,\n min_per_recipient=100_000.0,\n min_total_out=1_000_000.0,\n top_n_hubs=30,\n max_recipients_per_hub_display=20,\n )\n funding_attribution[\"cex_fanout_hubs\"] = _fanout_disco\n import sys as _sys\n _fo_sum = _fanout_disco.get(\"summary\", {})\n print(\n f\"[funding_attribution] cex_fanout_hubs: \"\n f\"{_fo_sum.get('n_confirmed_hubs', 0)} confirmed CEX-source hubs \"\n f\"(from {_fo_sum.get('n_candidate_hubs', 0)} candidates), \"\n f\"{_fo_sum.get('total_fanout_recipients', 0)} fan-out recipients, \"\n f\"{_fo_sum.get('total_cex_inflow_tokens', 0):,.0f} tokens from CEX\",\n file=_sys.stderr,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] cex_fanout_hubs failed (non-fatal): {_e}\",\n file=_sys.stderr)\n funding_attribution[\"cex_fanout_hubs\"] = {\n \"hubs\": [], \"summary\": {}, \"_error\": str(_e)[:200],\n }\n\n # v0.7.24c: Multi-chain dump trace. CoinGecko platforms data tells us\n # which chains have this token deployed. For chains other than primary\n # (= where scope.contract_address lives), run mint authority + high-\n # throughput dumper detection. Catches Eleve's H finding: 0x9e995952\n # on Ethereum dumped 140M H ($22M USD), invisible to BSC pipeline.\n if isinstance(funding_attribution, dict) and scope.get(\"coingecko_platforms\") and not holder_snapshot_mode:\n try:\n from chain_router import (\n chain_lock as _chain_lock,\n sql_supported as _sql_supported,\n get_active_chain as _get_active_chain,\n )\n # CoinGecko platform slug → chain_router prefix\n _CG_TO_CHAIN = {\n \"ethereum\": \"ethereum\",\n \"binance-smart-chain\": \"bsc\",\n \"arbitrum-one\": \"arbitrum\",\n \"base\": \"base\",\n \"polygon-pos\": \"polygon\",\n \"optimistic-ethereum\": \"optimism\",\n }\n _primary_chain = _get_active_chain()\n _cross_chain_results: dict[str, Any] = {}\n # v0.9.7 fix (FOLKS 9-chain 2026-06-16): gate cross-chain\n # forensic by LP threshold. FOLKS empirical: 7 non-primary\n # chains × full mint/throughput scan = +7 min, ALL return 0\n # because FOLKS only has meaningful activity on BSC + ETH +\n # Avalanche. Pre-v0.9.7 skill ran forensic on every CG-mapped\n # chain blindly. Now: skip chains with LP \u003c $10K (no real\n # activity → no operator to find).\n _MULTI_CHAIN_MIN_LP_USD = float(\n os.environ.get(\"BINANCE_ALPHA_MULTI_CHAIN_MIN_LP_USD\", \"10000\")\n )\n _chain_lp_realtime = scope.get(\"chain_lp_realtime\") or {}\n for _cg_platform, _ca_on_chain in (scope.get(\"coingecko_platforms\") or {}).items():\n _chain_short = _CG_TO_CHAIN.get(_cg_platform)\n if not _chain_short or _chain_short == _primary_chain:\n continue\n if not _ca_on_chain:\n continue\n # LP gate: skip chains with KNOWN-low liquidity. adversarial review\n # v0.9.7 Finding 5 (HIGH): distinguish \"LP probe failed\n # (unknown)\" from \"LP genuinely low\". A failed surf probe\n # leaves lp_usd=None — that is NOT evidence the chain is\n # dead, so we must NOT skip on it (would silently miss an\n # active chain whose probe flaked). Only skip when LP is\n # KNOWN and below threshold. Unknown LP → scan anyway\n # (correctness > speed for the unknown case).\n _chain_lp_entry = _chain_lp_realtime.get(_chain_short) or {}\n _lp_usd = _chain_lp_entry.get(\"lp_usd\")\n _lp_probe_failed = bool(_chain_lp_entry.get(\"_error\")) or (\n _chain_lp_entry.get(\"surf_supported\") and _lp_usd is None\n and _chain_lp_entry.get(\"n_dex_pools\") is None\n )\n if _lp_usd is not None and _lp_usd \u003c _MULTI_CHAIN_MIN_LP_USD:\n _cross_chain_results[_chain_short] = {\n \"ca\": _ca_on_chain, \"cg_platform\": _cg_platform,\n \"_skipped\": (\n f\"LP ${_lp_usd:.0f} \u003c ${_MULTI_CHAIN_MIN_LP_USD:.0f} threshold \"\n f\"— chain has no meaningful activity to forensic\"\n ),\n }\n continue\n if _lp_usd is None and not _lp_probe_failed:\n # LP probe ran cleanly + found 0 pools = genuinely no\n # DEX market on this chain → skip (same as low LP).\n if (_chain_lp_entry.get(\"n_dex_pools\") or 0) == 0:\n _cross_chain_results[_chain_short] = {\n \"ca\": _ca_on_chain, \"cg_platform\": _cg_platform,\n \"_skipped\": \"0 DEX pools on this chain — no market to forensic\",\n }\n continue\n # Else: lp_usd known >= threshold, OR lp_usd unknown due to\n # probe failure → scan (don't silently skip an active chain).\n try:\n with _chain_lock(_chain_short):\n if not _sql_supported():\n _cross_chain_results[_chain_short] = {\n \"ca\": _ca_on_chain, \"cg_platform\": _cg_platform,\n \"_skipped\": f\"surf has no agent.{_chain_short}_transfers table\",\n }\n continue\n # v0.9.7 adversarial review Finding 4 (MEDIUM): set the SECONDARY\n # chain's token decimals before its SQL runs. The\n # bridged/mirror deployment may have different decimals\n # than the primary (e.g. 18 on BSC, 6 on a bridge\n # mint). Without this, the secondary chain's\n # mint/throughput thresholds use the primary divisor\n # and mis-scale. Save + restore around the scan so the\n # primary chain's decimals aren't leaked back.\n from chain_router import (\n get_token_decimals as _get_tok_dec,\n set_token_decimals as _set_tok_dec,\n )\n from section_a_scope import _fetch_evm_token_decimals\n # Capture primary decimals, then do EVERYTHING\n # (fetch + set + scan) inside try so the finally\n # unconditionally restores — even if the secondary\n # decimals RPC or set throws. Pre-bulletproof version\n # had fetch/set outside the try; a throw there would\n # leak secondary decimals into the rest of the\n # pipeline (every later section uses the wrong divisor).\n _saved_decimals = _get_tok_dec()\n _chain_id_for_rpc = {\n \"ethereum\": \"1\", \"bsc\": \"56\", \"polygon\": \"137\",\n \"base\": \"8453\", \"arbitrum\": \"42161\",\n \"optimism\": \"10\", \"avalanche\": \"43114\",\n }.get(_chain_short)\n try:\n _sec_dec = _fetch_evm_token_decimals(\n _ca_on_chain, _chain_id_for_rpc,\n )\n _set_tok_dec(_sec_dec) # None → fallback 18 internally\n _mc_auth = _discover_mint_authorities(\n ca=_ca_on_chain, date_floor=_date_floor,\n top_n=10, min_pct_supply=0.001,\n total_supply=scope.get(\"total_supply\"),\n )\n _mc_ht = _discover_high_throughput_dumpers(\n ca=_ca_on_chain, date_floor=_date_floor,\n min_throughput=1_000_000.0, max_balance_frac=0.25,\n min_n_tx=500, top_n=50,\n total_supply=scope.get(\"total_supply\"),\n )\n finally:\n _set_tok_dec(_saved_decimals) # restore primary, always\n # v0.7.24e.2 (A3): multi_chain mint_auth + ht labels\n # deferred to consolidated batch at end of Round 8.\n\n _cross_chain_results[_chain_short] = {\n \"ca\": _ca_on_chain, \"cg_platform\": _cg_platform,\n \"mint_authorities\": _mc_auth,\n \"high_throughput_dumpers\": _mc_ht,\n }\n import sys as _sys\n print(\n f\"[multi_chain] {_chain_short} ({_ca_on_chain}): \"\n f\"{_mc_auth.get('summary', {}).get('n_authorities', 0)} mint authorities, \"\n f\"{_mc_ht.get('summary', {}).get('n_dumpers', 0)} ht dumpers\",\n file=_sys.stderr,\n )\n except Exception as _ce:\n import sys as _sys\n print(f\"[multi_chain] {_chain_short} failed: {_ce}\", file=_sys.stderr)\n _cross_chain_results[_chain_short] = {\n \"ca\": _ca_on_chain, \"cg_platform\": _cg_platform,\n \"_error\": str(_ce)[:200],\n }\n if _cross_chain_results:\n funding_attribution[\"multi_chain\"] = _cross_chain_results\n except Exception as _e:\n import sys as _sys\n print(f\"[multi_chain] outer failed (non-fatal): {_e}\", file=_sys.stderr)\n\n # v0.7.24e.2 (A3): consolidated label batch. Earlier each detector\n # invoked surf wallet-labels-batch separately (mint_auth + destinations +\n # ht + multi_chain × N chains × 2 detectors = 4-7 calls × 3-5s = 15-25s\n # cumulative). Now all addrs needing labels are collected into a single\n # set and resolved in 1 batch, then applied back to each detector dict.\n # Saves ~12-20s + ~25-35 credits per pipeline run. The cex_fanout helper\n # keeps its internal label call because it uses the result to filter\n # candidates (gate-decision), not post-hoc enrichment.\n if isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n try:\n _label_addrs_union: set[str] = set()\n # 1. Mint authority addrs (primary chain)\n for _a in (funding_attribution.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n if _a.get(\"addr\"):\n _label_addrs_union.add(_a[\"addr\"].lower())\n # 2. mining_fed_outflows + mint_authority_dumps top destinations\n for _key in (\"mining_fed_outflows\", \"mint_authority_dumps\"):\n _block = funding_attribution.get(_key) or {}\n for _wallet, _wallet_data in (_block.get(\"per_addr\") or {}).items():\n for _d in (_wallet_data.get(\"top_destinations\") or []):\n _dest = (_d.get(\"dest\") or \"\").lower()\n if _dest:\n _label_addrs_union.add(_dest)\n # 3. high_throughput dumpers (primary chain)\n for _d in (funding_attribution.get(\"high_throughput_dumpers\", {}).get(\"dumpers\") or []):\n if _d.get(\"addr\") and not _d.get(\"is_excluded\"):\n _label_addrs_union.add(_d[\"addr\"].lower())\n # 4. Multi-chain mint authorities + ht dumpers\n for _chain_name, _mc_data in (funding_attribution.get(\"multi_chain\") or {}).items():\n for _a in (_mc_data.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n if _a.get(\"addr\"):\n _label_addrs_union.add(_a[\"addr\"].lower())\n for _d in (_mc_data.get(\"high_throughput_dumpers\", {}).get(\"dumpers\") or []):\n if _d.get(\"addr\") and not _d.get(\"is_excluded\"):\n _label_addrs_union.add(_d[\"addr\"].lower())\n\n _consolidated_labels: dict[str, dict] = {}\n # v0.7.24e.3 (A4): cap label batch at 500 unique addrs to avoid\n # surf wallet-labels-batch timing out on PLAY-class tokens that\n # surface 1000+ high-throughput operators. For those, label the\n # first 500 by detection order — others render as UNLABELED but\n # the report still surfaces them.\n _LABEL_BATCH_CAP = 500\n if _label_addrs_union:\n _sorted = sorted(_label_addrs_union)\n _capped = _sorted[:_LABEL_BATCH_CAP]\n _dropped = len(_sorted) - len(_capped)\n try:\n from surf_labels_probe import resolve_labels as _resolve_labels\n _consolidated_labels = _resolve_labels(_capped)\n import sys as _sys\n print(\n f\"[funding_attribution] A3 consolidated label batch: \"\n f\"{len(_capped)} unique addrs in 1 surf call\"\n + (f\" (A4 cap: dropped {_dropped} above {_LABEL_BATCH_CAP})\" if _dropped else \"\")\n + \" (saved 3-5 separate batches)\",\n file=_sys.stderr,\n )\n except Exception as _le:\n import sys as _sys\n print(f\"[funding_attribution] A3 consolidated label batch failed (non-fatal): {_le}\",\n file=_sys.stderr)\n\n # Post-hoc apply: walk each detector dict, apply labels +\n # recompute filters (is_infra) where needed.\n _INFRA_CLS = {\"DEX_POOL\", \"CEX_DEPOSIT\", \"CEX_HOT_WALLET\"}\n\n def _apply_label(rec):\n _lab = _consolidated_labels.get((rec.get(\"addr\") or \"\").lower()) or {}\n rec[\"arkham_label\"] = _lab.get(\"label\")\n rec[\"arkham_entity_name\"] = _lab.get(\"entity_name\")\n rec[\"arkham_entity_type\"] = _lab.get(\"entity_type\")\n rec[\"arkham_classification\"] = _lab.get(\"classification\") or \"UNLABELED\"\n\n # Apply to mint authorities (primary)\n for _a in (funding_attribution.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n _apply_label(_a)\n\n # Apply to top destinations + recompute destination_label_summary\n _cex_classes = {\"CEX_DEPOSIT\", \"CEX_HOT_WALLET\"}\n _cex_total = 0.0\n _dex_pool_total = 0.0\n for _key in (\"mining_fed_outflows\", \"mint_authority_dumps\"):\n _block = funding_attribution.get(_key) or {}\n for _wallet, _wallet_data in (_block.get(\"per_addr\") or {}).items():\n for _d in (_wallet_data.get(\"top_destinations\") or []):\n _dest = (_d.get(\"dest\") or \"\").lower()\n _lab = _consolidated_labels.get(_dest) or {}\n _d[\"arkham_label\"] = _lab.get(\"label\")\n _d[\"arkham_entity_name\"] = _lab.get(\"entity_name\")\n _d[\"arkham_classification\"] = _lab.get(\"classification\") or \"UNLABELED\"\n _amt = float(_d.get(\"amt\") or 0)\n _cls = _d[\"arkham_classification\"]\n if _cls in _cex_classes:\n _cex_total += _amt\n elif _cls == \"DEX_POOL\":\n _dex_pool_total += _amt\n funding_attribution[\"destination_label_summary\"] = {\n \"cex_deposit_tokens\": _cex_total,\n \"dex_pool_tokens\": _dex_pool_total,\n \"n_destinations_labeled\": sum(\n 1 for v in _consolidated_labels.values() if v.get(\"classification\")\n ),\n }\n\n # Apply to primary high_throughput + recompute is_infra + summary\n _ht_block = funding_attribution.get(\"high_throughput_dumpers\") or {}\n for _d in (_ht_block.get(\"dumpers\") or []):\n _apply_label(_d)\n _d[\"is_infra\"] = _d.get(\"arkham_classification\") in _INFRA_CLS\n _ht_op_dumpers = [\n d for d in (_ht_block.get(\"dumpers\") or [])\n if not d.get(\"is_excluded\") and not d.get(\"is_infra\")\n ]\n if _ht_block.get(\"dumpers\"):\n _ht_block[\"summary\"] = {\n \"n_dumpers\": len(_ht_op_dumpers),\n \"total_throughput\": sum(d.get(\"total_in\", 0) for d in _ht_op_dumpers),\n \"n_dumpers_pre_filter\": len(_ht_block.get(\"dumpers\") or []),\n }\n\n # Apply to multi_chain — same pattern per chain\n for _chain_name, _mc_data in (funding_attribution.get(\"multi_chain\") or {}).items():\n for _a in (_mc_data.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n _apply_label(_a)\n _mc_ht_block = _mc_data.get(\"high_throughput_dumpers\") or {}\n for _d in (_mc_ht_block.get(\"dumpers\") or []):\n _apply_label(_d)\n _d[\"is_infra\"] = _d.get(\"arkham_classification\") in _INFRA_CLS\n _mc_ht_op = [\n d for d in (_mc_ht_block.get(\"dumpers\") or [])\n if not d.get(\"is_excluded\") and not d.get(\"is_infra\")\n ]\n if _mc_ht_block.get(\"dumpers\"):\n _mc_ht_block[\"summary\"] = {\n \"n_dumpers\": len(_mc_ht_op),\n \"total_throughput\": sum(d.get(\"total_in\", 0) for d in _mc_ht_op),\n \"n_dumpers_pre_filter\": len(_mc_ht_block.get(\"dumpers\") or []),\n }\n except Exception as _e:\n import sys as _sys\n print(f\"[funding_attribution] A3 post-hoc apply failed (non-fatal): {_e}\",\n file=_sys.stderr)\n\n # audit M1 fix: surface input-side truncation in pipeline log + the\n # report (via _debug.pipeline_truncated_n). silent loss of high-value addrs\n # past max_addrs=200 is the same class of bug as v0.7.23's dump_tracker\n # silent zero — visible-fail is the convention.\n _hv_dropped_input = max(0, _hv_pre_cap - 200)\n if _hv_dropped_input > 0:\n import sys as _sys\n print(\n f\"[funding_attribution] high_value_addrs cap 200 — \"\n f\"truncated {_hv_dropped_input} from {_hv_pre_cap} total \"\n f\"(wash → flow → m6 → dump-sellers → top-30 holders priority)\",\n file=_sys.stderr,\n )\n # Inject into _debug so the render template surfaces it. attribute_funding\n # already returns sql_truncated_addr_n from internal cap, but here we\n # report the upstream pipeline cap separately.\n if isinstance(funding_attribution, dict):\n _dbg = funding_attribution.setdefault(\"_debug\", {})\n _dbg[\"pipeline_truncated_n\"] = _hv_dropped_input\n _dbg[\"pipeline_input_total\"] = _hv_pre_cap\n # v0.8.6.5.0: Build _master_cluster_addrs — union of all detector cluster\n # wallet sets. Single source of truth for \"项目方控钱包\" candidates, used\n # by wallet_cluster_graph (next section) + render to avoid dedup logic\n # across multiple detector outputs. Python union only — 0 extra surf calls.\n if isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n _master = set()\n # 1. mint_authorities (主 mint 权限)\n for _a in (funding_attribution.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n _addr = (_a.get(\"addr\") or \"\").lower()\n if _addr:\n _master.add(_addr)\n # 2. mint_authority_dumps destinations (mint → cluster)\n _mad = funding_attribution.get(\"mint_authority_dumps\", {}).get(\"per_addr\") or {}\n for _auth, _data in _mad.items():\n for _d in (_data.get(\"top_destinations\") or []):\n _addr = (_d.get(\"dest\") or \"\").lower()\n if _addr:\n _master.add(_addr)\n # 3. mining_fed_outflows targets\n _mfo = funding_attribution.get(\"mining_fed_outflows\", {}).get(\"per_addr\") or {}\n for _src, _data in _mfo.items():\n for _d in (_data.get(\"top_destinations\") or []):\n _addr = (_d.get(\"dest\") or \"\").lower()\n if _addr:\n _master.add(_addr)\n # 4. high_throughput_dumpers\n for _d in (funding_attribution.get(\"high_throughput_dumpers\", {}).get(\"dumpers\") or []):\n _addr = (_d.get(\"addr\") or \"\").lower()\n if _addr:\n _master.add(_addr)\n # 5. cex_fanout_hubs (hubs + recipients)\n _cfh = funding_attribution.get(\"cex_fanout_hubs\", {})\n for _h in (_cfh.get(\"hubs\") or []):\n _hub_addr = (_h.get(\"addr\") or \"\").lower()\n if _hub_addr:\n _master.add(_hub_addr)\n for _r in (_h.get(\"_net_structured_recipient_addrs_raw\") or []):\n if _r:\n _master.add(_r.lower())\n # 6. deployer (m6 root)\n if rule11 and isinstance(rule11, dict):\n _dpl = (rule11.get(\"deployer\") or \"\").lower()\n if _dpl:\n _master.add(_dpl)\n # 6b. m6 谱系 dumpers (rule_11 抓的 d1-d4)\n for _d_block in (rule11.get(\"dumpers\") or {}).values() if isinstance(rule11.get(\"dumpers\"), dict) else []:\n for _d in (_d_block or []):\n _addr = (_d.get(\"addr\") if isinstance(_d, dict) else _d) or \"\"\n if isinstance(_addr, str) and _addr:\n _master.add(_addr.lower())\n funding_attribution[\"_master_cluster_addrs\"] = sorted(_master)\n import sys as _sys\n print(f\"[funding_attribution] _master_cluster_addrs union: {len(_master)} wallets \"\n f\"(across mint_auth + cluster + ht + fanout + m6)\", file=_sys.stderr)\n timings[\"funding_attribution\"] = time.perf_counter() - t\n _ck(\"funding_attribution\", funding_attribution)\n\n # ---------- Round 7d: wallet_cluster_graph (v0.8.6.5.0) ----------\n # Bidirectional IN-filter SQL to find wallet-to-wallet edges among\n # candidate set (master_cluster + top_holders ≥ 0.1% + cex_fanout\n # recipients). Catches Bubblemaps-style \"looks-like-retail wallets\n # actually transferring to each other\" clusters that single-direction\n # detectors miss. Surf-compliant: chunked sequential SQL, max_rows 9000.\n t = time.perf_counter()\n wallet_cluster_graph: dict = {\"clusters\": [], \"summary\": {}, \"_pipeline_source\": \"wallet_cluster_graph\"}\n if isinstance(funding_attribution, dict) and not holder_snapshot_mode:\n try:\n from wallet_cluster_graph_detector import discover_wallet_cluster_graph\n\n # Build candidate list: master_cluster + top_holders ≥ 0.1% supply\n _master_set = set(funding_attribution.get(\"_master_cluster_addrs\") or [])\n _candidates = set(_master_set)\n _total_supply = scope.get(\"total_supply\") or 0\n _min_top_holder = _total_supply * 0.001 if _total_supply else 0 # 0.1% supply\n _top_holder_only = set()\n for _h in (holders.get(\"top_holders\") or []):\n _bal = _h.get(\"balance_tokens\") if isinstance(_h, dict) else 0\n _addr = _h.get(\"address\") if isinstance(_h, dict) else None\n if _addr and (not _min_top_holder or (_bal or 0) >= _min_top_holder):\n _candidates.add(_addr.lower())\n if _addr.lower() not in _master_set:\n _top_holder_only.add(_addr.lower())\n # v0.8.6.5.2 adversarial review M5: source categorization for n_new metric\n _source_cat: dict[str, str] = {}\n for _a in _master_set:\n _source_cat[_a] = \"master_cluster\"\n for _a in _top_holder_only:\n _source_cat[_a] = \"top_holder_only\"\n\n # Reuse Arkham labels from cex_fanout (saves label resolve call)\n _arkham_labels: dict = {}\n _cfh_block = funding_attribution.get(\"cex_fanout_hubs\", {}) or {}\n for _h in (_cfh_block.get(\"hubs\") or []):\n _src = (_h.get(\"cex_source\") or \"\").lower()\n if _src:\n _arkham_labels[_src] = {\n \"classification\": _h.get(\"cex_source_classification\"),\n \"label\": _h.get(\"cex_source_label\"),\n \"entity_name\": _h.get(\"cex_source_entity\"),\n }\n\n wallet_cluster_graph = discover_wallet_cluster_graph(\n ca=scope[\"contract_address\"],\n candidates=sorted(_candidates),\n total_supply=_total_supply,\n date_floor=_date_floor,\n arkham_labels=_arkham_labels,\n resolve_candidate_labels=True, # adversarial review M1 fix\n source_categorization=_source_cat, # adversarial review M5 fix\n )\n import sys as _sys\n _wcg_sum = wallet_cluster_graph.get(\"summary\", {})\n print(\n f\"[wallet_cluster_graph] {_wcg_sum.get('n_clusters', 0)} clusters / \"\n f\"{_wcg_sum.get('n_cluster_addrs_total', 0)} cluster addrs / \"\n f\"{_wcg_sum.get('n_candidates_input', 0)} candidates → \"\n f\"{_wcg_sum.get('n_candidates_post_l1', 0)} post-L1 / \"\n f\"{_wcg_sum.get('n_edges_total', 0)} edges / \"\n f\"{_wcg_sum.get('n_chunks_run', 0)} chunks\",\n file=_sys.stderr,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[wallet_cluster_graph] failed (non-fatal): {_e}\", file=_sys.stderr)\n wallet_cluster_graph = {\"clusters\": [], \"summary\": {}, \"_error\": str(_e)[:200]}\n timings[\"wallet_cluster_graph\"] = time.perf_counter() - t\n _ck(\"wallet_cluster_graph\", wallet_cluster_graph)\n\n # ---------- Round 7e: dump_tracker mining-mode fallback (v0.8.6.6) ----------\n # Build extra_receivers from funding_attribution + wcg cluster, rerun\n # dump_tracker with mining cluster as insider list. Unlocks confirmed\n # net sellout for cross-chain bridge / mining tokens (JCT/AOP/BTX/BEAT)\n # where rule_11 m6 lineage is empty/sparse.\n t = time.perf_counter()\n dump_tracking_mining: dict = {}\n if (isinstance(funding_attribution, dict) and not holder_snapshot_mode\n and isinstance(dump_tracking, dict)\n and (not dump_tracking.get(\"confirmed_net_sellout_usd\")\n or dump_tracking.get(\"confirmed_total_pct\", 0) \u003c 1.0)):\n try:\n mining_addrs: set[str] = set()\n for _a in (funding_attribution.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n _addr = (_a.get(\"addr\") or \"\").lower()\n if _addr:\n mining_addrs.add(_addr)\n for _auth, _data in (funding_attribution.get(\"mint_authority_dumps\", {}).get(\"per_addr\") or {}).items():\n for _d in (_data.get(\"top_destinations\") or []):\n _addr = (_d.get(\"dest\") or \"\").lower()\n if _addr:\n mining_addrs.add(_addr)\n for _src, _data in (funding_attribution.get(\"mining_fed_outflows\", {}).get(\"per_addr\") or {}).items():\n for _d in (_data.get(\"top_destinations\") or []):\n _addr = (_d.get(\"dest\") or \"\").lower()\n if _addr:\n mining_addrs.add(_addr)\n for _d in (funding_attribution.get(\"high_throughput_dumpers\", {}).get(\"dumpers\") or []):\n _addr = (_d.get(\"addr\") or \"\").lower()\n if _addr:\n mining_addrs.add(_addr)\n for _h in (funding_attribution.get(\"cex_fanout_hubs\", {}).get(\"hubs\") or []):\n _hub = (_h.get(\"addr\") or \"\").lower()\n if _hub:\n mining_addrs.add(_hub)\n for _r in (_h.get(\"_net_structured_recipient_addrs_raw\") or []):\n if _r:\n mining_addrs.add(_r.lower())\n for _cluster in (wallet_cluster_graph.get(\"clusters\") or []):\n for _a in (_cluster.get(\"addrs\") or []):\n mining_addrs.add(_a.lower())\n mining_addrs.discard(\"\")\n extra_receivers = [{\"addr\": a, \"is_cex_custody\": False, \"is_dex_infra\": False}\n for a in sorted(mining_addrs)]\n if len(extra_receivers) >= 3: # bother only if enough mining cluster\n dump_tracking_mining = dump_tracker_run(\n rule11=rule11,\n ca=ca,\n symbol=scope.get(\"symbol\"),\n listing_ts_ms=scope.get(\"alpha_listing_ts_ms\"),\n listing_date=scope.get(\"alpha_listing_date_utc\"),\n circulating_supply=scope.get(\"circulating_supply\"),\n total_supply=scope.get(\"total_supply\"),\n extra_receivers=extra_receivers,\n # v1.2.5: same spot-price guard as the main call (codex HIGH)\n # — without it non-18-decimal mining tokens still overflow.\n spot_price_usd=liq.get(\"current_price_usd\"),\n )\n import sys as _sys\n _dtm = dump_tracking_mining\n print(f\"[dump_tracker_mining] cluster={len(extra_receivers)} wallets, \"\n f\"confirmed_total={_dtm.get('confirmed_total_tokens', 0):,.0f} tokens \"\n f\"({(_dtm.get('confirmed_total_pct') or 0):.2f}% circ) \"\n f\"net_sellout_usd={(_dtm.get('confirmed_net_sellout_usd') or 0):,.0f}\",\n file=_sys.stderr)\n except Exception as _e:\n import sys as _sys\n print(f\"[dump_tracker_mining] failed (non-fatal): {_e}\", file=_sys.stderr)\n dump_tracking_mining = {\"_error\": str(_e)[:200]}\n timings[\"dump_tracker_mining\"] = time.perf_counter() - t\n _ck(\"dump_tracker_mining\", dump_tracking_mining)\n\n # ---------- Round 7f: multi-chain supply-overhang (R3/R5/R6, CAP) ----------\n # A token whose Alpha listing is a small MIRROR (CAP: BSC 1.4%) while the\n # bulk of supply + real operator control lives on another surf-SQL chain\n # (CAP: Ethereum 99.6%) is measured by the single-chain chip on the wrong\n # slice → \"🟢 分散 22.9%\" when the truth is \"~90% 控盘 + 84.5% 锁仓 overhang\".\n # detect_multichain_split (surf-only, 0 extra credit — reuses the per-chain\n # holders section_a already fetched) gates this; on a split we recompute the\n # 真实派发 buckets on the canonical supply chain by REUSING _compute_chip_3way\n # (operator union from mint-authority lineage + wallet cluster graph), with\n # the genesis reserve excluded as a locked overhang. SQL runs under\n # chain_lock(supply_chain) via the proven _run_surf_with_retry path (NOT\n # rule_11/run_parallel). BSC-native / single-chain tokens never split → 0\n # added cost. Solana is excluded (HOLDER_SNAPSHOT, no agent.solana_* tables).\n t = time.perf_counter()\n supply_chain_overhang: dict = {}\n if not holder_snapshot_mode:\n try:\n from helpers.section_a_scope import detect_multichain_split\n from helpers.supply_chain_overhang import compute_supply_chain_overhang\n _split_info = detect_multichain_split(\n chain_lp_realtime=scope.get(\"chain_lp_realtime\") or {},\n coingecko_platforms=scope.get(\"coingecko_platforms\") or {},\n alpha_chain_id=scope.get(\"chain_id\"),\n )\n if _split_info.get(\"split\"):\n import sys as _sys\n print(\n f\"[supply_chain_overhang] split detected: Alpha={_split_info.get('alpha_prefix')} \"\n f\"→ supply chain {_split_info.get('supply_prefix')} \"\n f\"({_split_info.get('supply_pct_of_total')}% of cross-chain supply); \"\n f\"recomputing 真实派发 on {_split_info.get('supply_ca')}\",\n file=_sys.stderr,\n )\n supply_chain_overhang = compute_supply_chain_overhang(\n split=_split_info,\n total_supply=scope.get(\"total_supply\"),\n circulating_supply=scope.get(\"circulating_supply\"),\n date_floor=_date_floor,\n alpha_listing_date=scope.get(\"alpha_listing_date_utc\"),\n deployer_addr=(rule11.get(\"deployer\") if isinstance(rule11, dict) else None),\n symbol=scope.get(\"symbol\"),\n )\n print(\n f\"[supply_chain_overhang] operator={supply_chain_overhang.get('operator_pct')}% \"\n f\"relay={supply_chain_overhang.get('relay_pct')}% \"\n f\"non_op={supply_chain_overhang.get('non_operator_pct')}% / \"\n f\"locked overhang={supply_chain_overhang.get('locked_pct_of_total')}% of total \"\n f\"({supply_chain_overhang.get('n_genesis_funded')} lineage-funded, \"\n f\"{supply_chain_overhang.get('n_clusters')} clusters)\"\n + (f\" ERROR={supply_chain_overhang.get('_error')}\"\n if supply_chain_overhang.get('_error') else \"\"),\n file=_sys.stderr,\n )\n else:\n supply_chain_overhang = {\"split\": False}\n except Exception as _e:\n import sys as _sys\n print(f\"[supply_chain_overhang] failed (non-fatal): {_e}\", file=_sys.stderr)\n supply_chain_overhang = {\"split\": False, \"_error\": str(_e)[:200]}\n timings[\"supply_chain_overhang\"] = time.perf_counter() - t\n _ck(\"supply_chain_overhang\", supply_chain_overhang)\n\n # v1.2.16 (product spec 2026-07-01, UB): section_multi_chain ran in Round 5 BEFORE the\n # supply-split was known, so it reported \"单链 完整覆盖\" even when\n # detect_multichain_split found the bulk of supply on another chain (UB: 87.3%\n # on a verified Ethereum contract). Re-run it now with the split result so the\n # 多链 rows agree with the chip's supply-chain-overhang framing.\n if isinstance(supply_chain_overhang, dict) and supply_chain_overhang.get(\"split\") \\\n and not supply_chain_overhang.get(\"_error\") and not holder_snapshot_mode:\n try:\n multi_chain = section_multi_chain_run(\n chain_label=scope[\"chain_label\"],\n total_supply=scope.get(\"total_supply\"),\n primary_chain=scope.get(\"primary_chain\"),\n holder_snapshot_mode=holder_snapshot_mode,\n supply_split=supply_chain_overhang,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[multi_chain re-run] failed (non-fatal): {_e}\", file=_sys.stderr)\n\n # ---------- Round 7g: primary-sale (CCA / IDO) attribution — UNIVERSAL ----------\n # Self-check EVERY token for project public-distribution pools (CCA / IDO /\n # launchpad / airdrop redemption) and attribute insider/KOL capture. Multi-\n # chain split tokens reuse the supply-chain detection already computed inside\n # compute_supply_chain_overhang; single-chain tokens detect on the primary\n # chain using the deployer / pre-launch / mint-authority operator seed. Gated:\n # no operator-funded distribution pool → empty dict → render omits the section.\n t = time.perf_counter()\n primary_sales: dict = {}\n if not holder_snapshot_mode and scope.get(\"contract_address\"):\n try:\n _sco_ps = (supply_chain_overhang.get(\"primary_sales\")\n if isinstance(supply_chain_overhang, dict) else None)\n _is_split = bool(isinstance(supply_chain_overhang, dict)\n and supply_chain_overhang.get(\"split\"))\n if isinstance(_sco_ps, dict) and _sco_ps.get(\"pools\"):\n primary_sales = _sco_ps # multi-chain: detected on the supply chain\n elif not _is_split:\n # single-chain ONLY. codex HIGH: never fall back to the Alpha listing\n # chain for a split/mirror token — its public sale lives on the\n # canonical supply chain (handled above); detecting on the mirror chain\n # would catch bridge activity with the wrong seed/denominator.\n from chain_router import set_active_chain as _psa_set_chain\n from helpers.supply_chain_overhang import _is_contract as _psa_is_contract\n from helpers.primary_sale_attribution import (\n detect_primary_sale_pools as _psa_detect,\n enrich_with_social as _psa_social,\n )\n _psa_prefix = (_psa_set_chain(chain_id) if chain_id else None)\n if _psa_prefix and _psa_prefix != \"solana\":\n # FUNDING seed = treasury sources only (deployer / pre-launch /\n # mint authorities) — narrow, for the operator-funded gate.\n _psa_seed: set = set()\n if isinstance(rule11, dict) and rule11.get(\"deployer\"):\n _psa_seed.add(_norm_addr(rule11[\"deployer\"]))\n _r11_recv = (rule11.get(\"pre_launch_receivers\") or []) if isinstance(rule11, dict) else []\n for _r in _r11_recv:\n # parity with monitoring / counts / supply_chain: a CEX\n # listing-channel or DEX pool receiver is not an operator and\n # must not seed the primary-sale funding set (codex HIGH).\n if _receiver_infra_kind(_r):\n continue\n _a = _norm_addr(_r.get(\"address\") or _r.get(\"addr\") or \"\")\n if _a:\n _psa_seed.add(_a)\n if isinstance(funding_attribution, dict):\n for _a in (funding_attribution.get(\"mint_authorities\", {}).get(\"authorities\") or []):\n _ad = _norm_addr(_a.get(\"address\") or _a.get(\"addr\") or \"\")\n if _ad:\n _psa_seed.add(_ad)\n # operator relay members are project-controlled distribution hubs\n # (Safe/multisig-like) — add to BOTH the funding seed (so a\n # Safe-funded single-chain CCA is not missed, codex HIGH) and the\n # overlap set. Still far narrower than the full operator_set.\n _relay = (distribution.get(\"operator_relay_members\") or []) if isinstance(distribution, dict) else []\n for _m in _relay:\n _a = _norm_addr(_m.get(\"addr\") or _m.get(\"address\") or \"\")\n if _a:\n _psa_seed.add(_a)\n _psa_overlap: set = set(_psa_seed)\n _psa_pools = _psa_detect(\n prefix=_psa_prefix, supply_ca=scope[\"contract_address\"],\n operator_seed=_psa_seed, operator_set=_psa_overlap,\n total_supply=float(scope.get(\"total_supply\") or 0),\n circulating_supply=float(scope.get(\"circulating_supply\") or 0),\n date_floor=_date_floor, is_contract_fn=_psa_is_contract,\n code_cache={}, listing_date=scope.get(\"alpha_listing_date_utc\"),\n )\n if _psa_pools:\n primary_sales = {\"pools\": _psa_pools, \"chain\": _psa_prefix}\n if scope.get(\"symbol\"):\n primary_sales[\"social\"] = _psa_social(scope[\"symbol\"], _psa_pools)\n except Exception as _e:\n import sys as _sys\n print(f\"[primary_sale] universal attribution failed (non-fatal): {_e}\",\n file=_sys.stderr)\n primary_sales = {\"_error\": str(_e)[:200]}\n timings[\"primary_sales\"] = time.perf_counter() - t\n _ck(\"primary_sales\", primary_sales)\n\n # Insider-only pre-launch receivers: exclude NEUTRAL infra (CEX custody / DEX\n # pool) so the \"上线前内幕分发\" / \"已分完\" detector counts don't include the\n # exchange listing-channel wallet or the liquidity pool as insider distribution.\n # (Vesting/multisig/treasury stay — they are project-controlled allocations.)\n _pre_recv = (rule11.get(\"pre_launch_receivers\") or []) if isinstance(rule11, dict) else []\n _insider_recv = [r for r in _pre_recv if not _receiver_infra_kind(r)]\n\n # ---------- Build the skeleton report_data ----------\n # Locked sections (pipeline-populated)\n skeleton = {\n \"_schema_version\": SKILL_VERSION,\n \"_pipeline_status\": \"ok\",\n \"_field_authority\": {\n \"locked\": [\"meta\", \"tier_classification\", \"evidence_graph\",\n \"anomaly.waves[].events[].evt_ref\",\n \"anomaly.waves[].events[].ts\",\n \"anomaly.waves[].events[].amount\",\n \"anomaly.waves[].events[].from_to\",\n \"lineage.m6.rows\",\n # v0.7: cross-sym whale fields\n \"cross_sym.whales[].address\",\n \"cross_sym.whales[].this_token_pct\",\n \"cross_sym.whales[].this_token_balance\",\n \"cross_sym.whales[].cross_sym_count\",\n \"cross_sym.whales[].cross_sym_tokens\",\n \"cross_sym.whales[].top_cross_sym_token\",\n \"cross_sym.whales[].arkham_label\",\n \"cross_sym.whales[].pre_launch_insider_count\",\n \"cross_sym.whales[].pre_launch_insider_tokens\",\n \"cross_sym.whales[].behavior_signature\"],\n \"derived_locked\": [\"cross_sym.whales[].identity_classification_enum\",\n \"cross_sym.whales[].confidence_score\",\n \"cross_sym.whales[].evidence_required_fields\",\n # v0.7.7 role classifier output (6-step on-chain)\n \"cross_sym.whales[].role_classification\",\n # v0.7.7 wash-infra detector output (5-step pure\n # on-chain X/P/Q signature)\n \"wash_infrastructure.setups[].executor_X\",\n \"wash_infrastructure.setups[].maker_buy_P\",\n \"wash_infrastructure.setups[].maker_sell_Q\",\n \"wash_infrastructure.setups[].atomic_pair_ratio\",\n \"wash_infrastructure.setups[].p_drift_pct\",\n \"wash_infrastructure.setups[].q_drift_pct\",\n \"wash_infrastructure.setups[].p_tok_in\",\n \"wash_infrastructure.setups[].q_tok_in\",\n \"wash_infrastructure.setups[].tx_from_diversity\",\n \"wash_infrastructure.setups[].classification\"],\n \"writable\": [\"verdict.one_liner\", \"anomaly.waves[].events[].nature\",\n \"anomaly.detector_summary[].detail\",\n \"anomaly.verdict_impact\",\n \"*.interpretation\",\n \"holdings_distribution.key_takeaways\",\n \"lineage.m4_notes\",\n \"monitoring_wallets[].alert\",\n \"monitoring_footer\",\n # v0.7 narrative slots\n \"cross_sym.whales[].identity_narrative\",\n \"cross_sym.whales[].risk_assessment_narrative\",\n \"cross_sym.summary_narrative\",\n # v0.7.7 wash-infra narrative slots\n \"wash_infrastructure.setups[].investigation_narrative\",\n \"wash_infrastructure.summary_narrative\"],\n },\n\n \"meta\": {\n \"symbol\": scope[\"symbol\"],\n \"name\": scope[\"name\"],\n \"contract_address\": scope[\"contract_address\"],\n \"chain\": scope[\"chain_label\"],\n \"chain_id\": scope[\"chain_id\"],\n \"alpha_listing_date_utc\": scope[\"alpha_listing_date_utc\"],\n \"total_supply\": scope[\"total_supply\"],\n \"circulating_supply\": scope[\"circulating_supply\"],\n \"circ_ratio\": scope[\"circ_ratio\"],\n \"alpha_vol_24h_usd\": scope[\"alpha_vol_24h_usd\"],\n # v0.7.16: Alpha API token-list price + LP fields. Used by the\n # top-of-report token-info table as a fallback when surf\n # project-detail.token_info is NOT_FOUND (e.g. GUA — listed 3\n # months and still un-indexed by surf), so the header table is\n # never blank on a real Alpha-listed token.\n \"alpha_price_usd\": scope.get(\"alpha_price_usd\"),\n \"alpha_percent_change_24h\": scope.get(\"alpha_percent_change_24h\"),\n \"alpha_market_cap_usd\": scope.get(\"alpha_market_cap_usd\"),\n \"alpha_fdv_usd\": scope.get(\"alpha_fdv_usd\"),\n \"alpha_liquidity_usd\": scope.get(\"alpha_liquidity_usd\"),\n \"alpha_price_high_24h\": scope.get(\"alpha_price_high_24h\"),\n \"alpha_price_low_24h\": scope.get(\"alpha_price_low_24h\"),\n \"alpha_count_24h\": scope.get(\"alpha_count_24h\"),\n \"alpha_holders\": scope.get(\"alpha_holders\"),\n \"token_type_initial\": scope[\"token_type_initial\"],\n # v0.7.10: cross-chain truth via CoinGecko platforms + surf realtime\n # token-holders DEX-label LP comparison (no longer 24h-lagged\n # bsc_dex_trades aggregate). single_chain = True iff CoinGecko shows\n # only one deployment chain.\n \"single_chain\": (\n len(scope.get(\"coingecko_platforms\") or {}) \u003c= 1\n ),\n # v0.7.21.8: on holder-snapshot chains (Solana) the LP-based\n # derive_primary_chain can pick a wrapped-deployment chain\n # (FARTCOIN has a Base wrapper at 0x2f6c…, LP-only fallback\n # picked \"base\" even though the canonical chain is solana). Force\n # primary_chain to match the active SQL routing chain so the\n # report banners + blindspots + monitoring.chain are consistent.\n \"primary_chain\": (\n get_active_chain() if holder_snapshot_mode\n else scope.get(\"primary_chain\")\n ),\n \"primary_chain_derivation\": (\n \"holder_snapshot_uses_active_chain\"\n if holder_snapshot_mode\n else scope.get(\"primary_chain_derivation\")\n ),\n \"coingecko_platforms\": scope.get(\"coingecko_platforms\") or {},\n \"chain_lp_realtime\": scope.get(\"chain_lp_realtime\") or {},\n \"realtime_token_info\": scope.get(\"realtime_token_info\") or {},\n # v0.7.9: surf BSC data availability snapshot (drives banner) —\n # historical SQL freshness only; not used to decide primary_chain\n # or LP/vol in v0.7.10.\n \"data_freshness\": scope.get(\"data_freshness\") or {},\n # v0.6.2 audit HIGH #2: lock lang into skeleton.\n # render_report.py validates this matches its own --lang. Mismatch\n # = visible-fail (refuse render) instead of silent leak.\n \"report_lang_locked\": _i18n_get_lang(),\n # v0.7.21.8: surf onchain-sql coverage flag. True when the active\n # chain has no `agent.{chain}_*` tables (Solana). Render template\n # surfaces a top-of-report banner explaining why the SQL-only\n # forensic sections are absent.\n \"_holder_snapshot_mode\": holder_snapshot_mode,\n \"_sql_supported\": _chain_sql_supported(),\n # v0.7.21.9: SPL mint metadata (Solana). Lets the report header\n # show the on-chain `decimals` + show the chain-truth supply\n # next to the Alpha API snapshot when they differ. Both None on\n # EVM and on Solana RPC fallback.\n \"chain_decimals\": scope.get(\"chain_decimals\"),\n \"chain_total_supply_ui\": scope.get(\"chain_total_supply_ui\"),\n },\n\n # Tier — refined by section_cex_trace (no longer stub)\n \"tier_classification\": {\n \"tier\": cex_trace[\"tier\"],\n \"s1_date\": cex_trace[\"s1_date\"],\n \"s2_date\": cex_trace[\"s2_date\"],\n \"s3_date\": cex_trace[\"s3_date\"],\n },\n\n # Evidence graph (the centerpiece of v0.6)\n \"evidence_graph\": eg.to_dict(),\n\n # Lineage section: pipeline-built m6 + flowchart from Rule 11\n \"lineage\": {\n \"_pipeline_source\": \"rule_11_backward_trace\",\n \"deployer_addr\": rule11[\"deployer\"],\n \"mint_evt_ref\": rule11[\"mint_evt_ref\"],\n \"m6\": {\n \"rows\": [\n {\n \"m6_ref\": r[\"m6_ref\"],\n \"addr_short\": r[\"addr\"][:10],\n \"addr_full\": r[\"addr\"],\n \"received_from_deployer\": r[\"received_from_deployer\"],\n \"current_balance\": r[\"current_balance\"],\n \"dumped_pct\": r[\"dumped_pct\"],\n # v0.7.10.3: carry Arkham lockup classification so the\n # m6 table can flag vesting / multisig / CEX-custody\n # rows instead of presenting them as insiders.\n \"is_protocol_lockup\": r.get(\"is_protocol_lockup\", False),\n \"is_vesting\": r.get(\"is_vesting\", False),\n \"is_multisig\": r.get(\"is_multisig\", False),\n \"is_cex_custody\": r.get(\"is_cex_custody\", False),\n # v1.2.2: also carry DEX-infra / treasury so the m6 table can\n # flag a liquidity pool or treasury row, not just CEX/vesting.\n \"is_dex_infra\": r.get(\"is_dex_infra\", False),\n \"is_treasury\": r.get(\"is_treasury\", False),\n \"arkham_label\": r.get(\"arkham_label\"),\n # LLM writes the interpretation\n \"identity_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"status_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n }\n for r in rule11[\"pre_launch_receivers\"]\n ],\n \"n_quiet\": len(rule11[\"quiet_wallets\"]),\n # consistency with the detector_summary counts: exclude neutral infra\n # (CEX/DEX) so the lineage dumper tally doesn't present a listing /\n # liquidity wallet as an insider partial/full dumper.\n \"n_partial_dumper\": sum(\n 1 for r in _insider_recv\n if r.get(\"dumped_pct\") is not None and 0 \u003c r[\"dumped_pct\"] \u003c 95\n ),\n \"n_full_dumper\": sum(\n 1 for r in _insider_recv\n if r.get(\"dumped_pct\") is not None and r[\"dumped_pct\"] >= 95\n ),\n },\n \"m4_notes\": [\n rule11[\"summary_text\"], # auto-derived locked summary\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\", # LLM adds interpretation bullets\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n ],\n # v0.7.14 (adversarial review HIGH #1): every number cited in m4_notes[0]'s\n # pipeline summary is exposed here as a locked numeric field so the\n # narrative-hallucination check's pool includes them. Lets us run\n # the check on ALL m4_notes indices (incl. 0) without false-positives,\n # closing the m4_notes[0] exemption bypass on writable content.\n \"summary_locked_numbers\": rule11.get(\"summary_locked_numbers\", {}),\n \"dumper_destinations_summary\": {\n dumper_addr: {\n \"n_destinations\": len(dests),\n \"top_destination_addr\": dests[0][\"to\"] if dests else None,\n \"top_destination_evt_ref\": dests[0][\"evt_ref\"] if dests else None,\n }\n for dumper_addr, dests in rule11[\"dumper_destinations\"].items()\n },\n # Phase B.4 lineage flowchart (node_NNN into evidence_graph)\n \"flowchart_nodes\": distribution[\"flowchart_nodes\"],\n \"flowchart_edges\": distribution[\"flowchart_edges\"],\n # v0.9.6 Fix #3 (adversarial review Windows EVAA 2026-06-15 feedback):\n # step4 dumpers dropped due to chunk errors. Surface in\n # render Data Gap so the user knows about the trace incompleteness.\n \"step4_skipped_dumpers\": rule11.get(\"step4_skipped_dumpers\", []),\n \"n_step4_skipped_dumpers\": rule11.get(\"n_step4_skipped_dumpers\", 0),\n # v0.9.7 adversarial review R1 Finding 6 (HIGH): step4 budget-cap truncation.\n # When recursion_truncated, the verdict is a LOWER BOUND — N\n # sub-dumpers holding ~X tokens were not recursively expanded.\n # Surfaced so render Data Gap can disclose the under-count.\n \"recursion_truncated\": rule11.get(\"recursion_truncated\", False),\n \"n_sub_dumpers_skipped\": rule11.get(\"n_sub_dumpers_skipped\", 0),\n \"n_sub_dumpers_skipped_tokens\": rule11.get(\"n_sub_dumpers_skipped_tokens\", 0.0),\n },\n\n # Anomaly waves: pipeline-built from Rule 11 waves_proposal + Section ANOMALY 72h\n \"anomaly\": {\n \"waves\": (\n rule11[\"waves_proposal\"]\n + ([anomaly72[\"wave3_proposal\"]] if anomaly72.get(\"wave3_proposal\") else [])\n ),\n \"rhythm\": {\n \"title\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"waves\": [\n {\n \"name\": w[\"title\"],\n \"ts_text\": w[\"ts_range\"],\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n }\n for w in (\n rule11[\"waves_proposal\"]\n + ([anomaly72[\"wave3_proposal\"]] if anomaly72.get(\"wave3_proposal\") else [])\n )\n ],\n },\n \"verdict_impact\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"detector_summary\": [\n # Mandatory categories (R10 enforces presence). v0.6.2: labels via i18n.\n {\n \"emoji\": \"🔴\" if len(_insider_recv) > 0 else \"⚪\",\n \"label\": _i18n_t(\"anomaly.detector_label.pre_launch_distribution\"),\n \"count\": len(_insider_recv), # excludes CEX/DEX neutral infra\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n {\n \"emoji\": \"🔴\" if sum(\n 1 for r in _insider_recv\n if r.get(\"dumped_pct\") is not None and r[\"dumped_pct\"] >= 95\n ) > 0 else \"⚪\",\n \"label\": _i18n_t(\"anomaly.detector_label.full_dumper_wallets\"),\n \"count\": sum(\n 1 for r in _insider_recv\n if r.get(\"dumped_pct\") is not None and r[\"dumped_pct\"] >= 95\n ),\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n {\n \"emoji\": \"🟠\" if len(rule11[\"quiet_wallets\"]) > 0 else \"⚪\",\n \"label\": _i18n_t(\"anomaly.detector_label.quiet_wallets\"),\n \"count\": len(rule11[\"quiet_wallets\"]),\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n {\n \"emoji\": (\n \"🔴\" if anomaly72[\"n_recent_events\"] >= 10\n else \"🟠\" if anomaly72[\"n_recent_events\"] >= 3\n else \"🟡\" if anomaly72[\"n_recent_events\"] > 0\n else \"⚪\"\n ),\n \"label\": (\n _i18n_t(\"anomaly.detector_label.anomaly_72h_truncated\",\n limit=anomaly72.get(\"limit\", 100))\n if anomaly72.get(\"was_truncated\")\n else _i18n_t(\"anomaly.detector_label.anomaly_72h\")\n ),\n \"count\": anomaly72[\"n_recent_events\"],\n \"detail\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n # Phase 1.3+ categories (庄家归集 / LP 24h / Alpha 5% depth)\n # NOT emitted in alpha.3 — when pipeline can't produce data,\n # don't fabricate a detector entry. Validator R10 requires\n # >=3 detectors which we already satisfy (Rule 11 + 72h = 4).\n # Pipeline adds these back in phase B once section_liq +\n # section_cex_trace + hub detection are wired.\n ],\n },\n\n # Monitoring wallets: pipeline-emitted (audit regression restored)\n # Includes: deployer + every m6 row + top 5 distinct actors from 72h\n \"monitoring_wallets\": _build_monitoring_wallets(rule11, anomaly72, eg, distribution=distribution, cross_sym=cross_sym, flow_operators=flow_operators_block),\n\n # v0.7.9: 决策摘要 section. Top-of-report 5-line user-decision-centric\n # summary. Pipeline derives all numeric fields; LLM writes 1 line narrative.\n # User reads decision_summary FIRST, then drills into forensic detail.\n \"decision_summary\": {\n \"_pipeline_source\": \"forensic_pipeline.decision_summary\",\n # 主战场链 + 实时 LP (该链) + aggregate 24h vol (跨链 CEX+DEX)\n \"primary_chain\": scope.get(\"primary_chain\"),\n \"primary_chain_derivation\": scope.get(\"primary_chain_derivation\"),\n \"primary_chain_lp_usd\": (\n (scope.get(\"chain_lp_realtime\") or {}).get(scope.get(\"primary_chain\") or \"\", {}) or {}\n ).get(\"lp_usd\"),\n \"realtime_24h_vol_usd\": (\n (scope.get(\"realtime_token_info\") or {}).get(\"volume_24h_usd\")\n ),\n \"realtime_price_usd\": (\n (scope.get(\"realtime_token_info\") or {}).get(\"price_usd\")\n ),\n # 建议动作 (从 verdict 镜像)\n \"verdict_enum\": _build_verdict_block(rule11, anomaly72).get(\"enum\"),\n \"verdict_cn_label\": _build_verdict_block(rule11, anomaly72).get(\"cn_label\"),\n # 进场上限 (从 section_liq 镜像)\n \"entry_size_cap_usd\": liq.get(\"alpha_5pct_depth_usd_est\"),\n # 短期催化 (CEX listing date / unlock 上面已有部分数据)\n \"short_term_catalysts\": [\n row.get(\"status\") + \": \" + (row.get(\"exchange\") or \"\") + \" \" + (row.get(\"ts\") or \"\")\n for row in (cex_trace.get(\"rows\") or [])\n if row.get(\"status\") and \"已上线\" in (row.get(\"status\") or \"\")\n ],\n # 盲区清单. v0.7.10: pipeline 历史 forensic (m6/transfers/wash) 仍只\n # 跑 BSC partition, 所以主战场 ≠ BSC 时, 实时 LP/vol 跨链能拿到, 但\n # 历史血统/异动 trace 只能在 BSC 镜像端 (常常浅或不存在). 这是真盲区,\n # 跟实时数据 EVM 覆盖广没冲突.\n # v0.7.21.8: prefer the active chain (matches what SQL got routed\n # to) over scope.primary_chain — for Solana the LP-based derivation\n # can pick a wrapper chain (FARTCOIN Base wrapper) that misleads\n # the blindspot label.\n \"blindspots\": [\n b for b in [\n ((scope.get(\"data_freshness\") or {}).get(\"warning\")),\n (\n f\"主战场 {get_active_chain()} 的链上 SQL 不在 surf 覆盖, forensic SQL 段全部跳过, 仅保留 Alpha 实时 + token-holders 快照\"\n if holder_snapshot_mode\n else (\n f\"主战场 {scope.get('primary_chain')} 的历史血统/分发追溯不在 pipeline 覆盖范围 (仅 BSC 镜像端可查)\"\n if scope.get(\"primary_chain\") and scope.get(\"primary_chain\") != \"binance-smart-chain\"\n else None\n )\n ),\n ] if b\n ],\n # writable: LLM 写一段中文叙述串起来\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n\n # Verdict — derived_locked, pipeline writes real values now (fix adversarial review\n # alpha.2 deadlock: previously placeholder caused R11/locked conflict)\n \"verdict\": _build_verdict_block(rule11, anomaly72),\n\n # decision_action_block — derived_locked numeric slots, writable narrative\n \"decision_action_block\": _build_decision_action_block(rule11, anomaly72, liq=liq),\n\n # cross_sym — v0.7 forward-detection of cross-sym whale candidates.\n # Each whale entry has:\n # - locked: address, this_token_pct, cross_sym_count, etc.\n # - derived_locked: identity_classification_enum, confidence_score,\n # role_classification (v0.7.7 6-step on-chain)\n # - writable: identity_narrative, risk_assessment_narrative\n # Empty whales list is normal — most fresh CAs have no cross-sym whales.\n \"cross_sym\": cross_sym,\n\n # wash_infrastructure — v0.7.7 5-step pure on-chain wash detection.\n # Each setup entry exposes the X / P / Q triplet (executor + maker\n # buy-side + maker sell-side) plus the structural metrics that\n # confirm the wash signature (atomic-pair ratio, P+Q drift,\n # tx_from diversity). Replaces v0.7.5 wash_distributor_probe which\n # was empirically mis-fitting (DEX pool ↔ MM bot pattern was being\n # branded as \"operator EOA ↔ controlled contract wash\").\n \"wash_infrastructure\": wash_infrastructure,\n\n # v0.7.21: flow-based wallet detector — cross-Alpha trading\n # operators + single-operator async wash. Complementary to\n # wash_infrastructure (atomic-pair wash) — see v0721_DESIGN.md.\n \"flow_operators\": flow_operators_block,\n\n # v0.7.23.1: reverse-direction funding source classification for the\n # high-value addresses surfaced by all earlier detectors. Each addr\n # gets {mint, dex_buy, p2p} share so retail can read \"this wallet\n # is mining-fed\" vs \"this wallet is real DEX buyer\" without us having\n # to decode the token's specific distribution mechanism (bridge /\n # vesting / airdrop / standard deployer-anchored). Critical for\n # Aethir / Humanity / Polyhedra-class tokens where rule_11's standard\n # deployer→m6 trace returns empty.\n \"funding_attribution\": funding_attribution,\n\n # v0.8.6.5.0: wallet_cluster_graph — bidirectional IN-filter SQL,\n # connected components, 5-layer false-positive defense. Catches\n # Bubblemaps-style cluster patterns single-direction detectors miss.\n \"wallet_cluster_graph\": wallet_cluster_graph,\n # v1.1.0 (CAP): multi-chain supply-overhang. Present + split=True only\n # when the Alpha listing is a mirror and the canonical supply chain was\n # re-measured (operator/relay/non_operator % of circulating + locked\n # overhang). split=False (or absent) for single-chain tokens. Read by\n # screen_summary._compute_chip_3way override + render 真实派发 section.\n \"supply_chain_overhang\": supply_chain_overhang,\n # v1.2.0: primary-sale (CCA / IDO / launchpad) attribution — universal,\n # top-level so the render reads one place regardless of chain topology.\n \"primary_sales\": primary_sales,\n # v0.8.6.6: mining-mode dump_tracker fallback (extra_receivers =\n # mint_authority + cluster + ht + fanout + wcg cluster). Unlocks\n # confirmed net sellout for cross-chain bridge / mining tokens\n # where rule_11 m6 lineage is empty/sparse.\n \"dump_tracking_mining\": dump_tracking_mining,\n # v0.8.7.0: 6-dimension deterministic TL;DR for report top.\n # ChatGPT review: \"第一屏没有把信息压成 3 个判断\". screen_summary\n # builds 6 dims (阶段 / 筹码 / 成交质量 / 供应风险 / 盘口阶段 /\n # 监控重点) + 1-sentence summary, rendered as `## 0. 一屏结论`.\n # Built after main skeleton so it can read chain_state / dump_tracking\n # / behavior_profile etc. emitted earlier — see render path below.\n \"screen_summary\": None, # filled after build via helpers.screen_summary\n\n # holdings_distribution — Phase B.4 wired with real role_rows + progress_bars\n \"holdings_distribution\": {\n \"_pipeline_source\": \"section_f_holders + section_l_distribution\",\n \"role_rows\": distribution[\"role_rows\"],\n \"progress_bars\": distribution[\"progress_bars\"],\n \"n_top_holders_classified\": distribution[\"n_top_holders_classified\"],\n \"key_takeaways\": [\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n ],\n },\n\n # monitoring_footer — writable single-line footer\n \"monitoring_footer\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n\n # Section data (Phase B.3 real data, no longer stubs):\n \"multi_chain\": {\n \"rows\": multi_chain[\"rows\"],\n \"gate_note\": multi_chain[\"gate_note\"],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n \"tge\": {\n \"rows\": tge[\"rows\"],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n \"alloc\": {\n \"rows\": alloc[\"rows\"],\n \"n_quiet\": alloc[\"n_quiet\"],\n \"quiet_balance_pct_total\": alloc[\"quiet_balance_pct_total\"],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n # Phase 2: 真实派发 (entity-level sell to CEX/DEX). Pipeline-locked\n # data; LLM writes the interpretation only when clusters exist (else\n # pipeline sets a fixed line so the validator sees no placeholder in\n # a section the renderer skips anyway).\n \"dump_tracking\": {\n \"_pipeline_source\": \"dump_tracker\",\n # v0.7.12 (Opt1): CONFIRMED-SOLD LOWER BOUND + current holdings only.\n # The flow-summed \"allocation − balance\" upper bound was dropped — it\n # double-counted on deployer round-trips (R2 → >total-supply garbage).\n \"insider_n_wallets\": dump_tracking.get(\"insider_n_wallets\", 0),\n # Confirmed-sold FLOOR: (a) insider→CEX-deposit + (b) insider DEX swaps.\n \"confirmed_cex_tokens\": dump_tracking.get(\"confirmed_cex_tokens\", 0),\n \"confirmed_cex_pct\": dump_tracking.get(\"confirmed_cex_pct\"),\n \"confirmed_cex_labels\": dump_tracking.get(\"confirmed_cex_labels\", []),\n \"confirmed_dex_tokens\": dump_tracking.get(\"confirmed_dex_tokens\", 0),\n \"confirmed_dex_pct\": dump_tracking.get(\"confirmed_dex_pct\"),\n \"confirmed_dex_swaps\": dump_tracking.get(\"confirmed_dex_swaps\", 0),\n \"confirmed_total_tokens\": dump_tracking.get(\"confirmed_total_tokens\", 0),\n \"confirmed_total_pct\": dump_tracking.get(\"confirmed_total_pct\"),\n \"confirmed_est_profit_usd\": dump_tracking.get(\"confirmed_est_profit_usd\"),\n # v0.7.21.10: Net Sell Out (apparatus' time-weighted estimate).\n \"confirmed_net_sellout_usd\": dump_tracking.get(\"confirmed_net_sellout_usd\"),\n \"confirmed_dex_real_usd\": dump_tracking.get(\"confirmed_dex_real_usd\"),\n \"confirmed_cex_estimated_usd\": dump_tracking.get(\"confirmed_cex_estimated_usd\"),\n # v0.9.2.1: per-window splits (7d / 30d) added in dump_tracker\n # run() — pass through whitelist so the render template + JSON\n # footer get them. Without this whitelist they're silently\n # stripped because v0.6 field authority demands explicit\n # enumeration. (v0.9.2 hotfix — discovered via CLO_v092 run\n # where dump_tracking main path produced the sellout, not the\n # mining fallback that wholesale-emits.)\n \"confirmed_total_tokens_7d\": dump_tracking.get(\"confirmed_total_tokens_7d\"),\n \"confirmed_total_tokens_30d\": dump_tracking.get(\"confirmed_total_tokens_30d\"),\n \"confirmed_total_pct_7d\": dump_tracking.get(\"confirmed_total_pct_7d\"),\n \"confirmed_total_pct_30d\": dump_tracking.get(\"confirmed_total_pct_30d\"),\n \"confirmed_net_sellout_usd_7d\": dump_tracking.get(\"confirmed_net_sellout_usd_7d\"),\n \"confirmed_net_sellout_usd_30d\": dump_tracking.get(\"confirmed_net_sellout_usd_30d\"),\n \"confirmed_cex_tokens_7d\": dump_tracking.get(\"confirmed_cex_tokens_7d\"),\n \"confirmed_cex_tokens_30d\": dump_tracking.get(\"confirmed_cex_tokens_30d\"),\n \"confirmed_dex_tokens_7d\": dump_tracking.get(\"confirmed_dex_tokens_7d\"),\n \"confirmed_dex_tokens_30d\": dump_tracking.get(\"confirmed_dex_tokens_30d\"),\n \"confirmed_dex_real_usd_7d\": dump_tracking.get(\"confirmed_dex_real_usd_7d\"),\n \"confirmed_dex_real_usd_30d\": dump_tracking.get(\"confirmed_dex_real_usd_30d\"),\n \"apparatus_dex_twap_usd_per_token\": dump_tracking.get(\"apparatus_dex_twap_usd_per_token\"),\n \"net_above_gross_pct\": dump_tracking.get(\"net_above_gross_pct\"),\n \"confirmed_capped\": dump_tracking.get(\"confirmed_capped\", False),\n # v0.7.19.4: TREE holdings (with lockup + exit-infra) — conservation\n # anchor; render shows it as \"内幕树当前持有 (含未解锁锁仓)\".\n \"tree_holds_tokens\": dump_tracking.get(\"tree_holds_tokens\", 0),\n \"tree_holds_pct_supply\": dump_tracking.get(\"tree_holds_pct_supply\"),\n # v0.7.19.4: PURE insider holdings (excluding Arkham-confirmed\n # protocol_lockup + cex_custody + dex_infra) — narrative anchor.\n \"pure_insider_holds_tokens\": dump_tracking.get(\"pure_insider_holds_tokens\", 0),\n \"pure_insider_holds_pct_supply\": dump_tracking.get(\"pure_insider_holds_pct_supply\"),\n # Backward-compat alias (old name) — same value as tree_holds.\n # Render template uses the new explicit names; this stays for any\n # external consumer that hasn't read the v0.7.19.4 changelog.\n \"insider_holds_tokens\": dump_tracking.get(\"insider_holds_tokens\", 0),\n \"insider_holds_pct_supply\": dump_tracking.get(\"insider_holds_pct_supply\"),\n \"median_price_usd\": dump_tracking.get(\"median_price_usd\"),\n \"wash_dominated\": dump_tracking.get(\"wash_dominated\", False),\n \"n_dex_sellers\": dump_tracking.get(\"n_dex_sellers\", 0),\n \"total_dex_swaps\": dump_tracking.get(\"total_dex_swaps\", 0),\n \"top_seller_swaps\": dump_tracking.get(\"top_seller_swaps\", 0),\n \"buckets_complete\": dump_tracking.get(\"buckets_complete\", True),\n \"interpretation\": (\n \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n if (dump_tracking.get(\"confirmed_total_tokens\") or 0) > 0\n else \"未检测到 insider 向 CEX/DEX 的确认卖出 (链上无法确认的转账不计入).\"\n ),\n },\n \"cex_trace\": {\n \"rows\": cex_trace[\"cex_trace_rows\"],\n \"new_catalyst\": (\n _i18n_t(\"section.cex_trace.new_catalyst_none_14d\")\n if not cex_trace[\"s2_date\"] or _days_since(cex_trace[\"s2_date\"]) >= 14\n else _i18n_t(\"section.cex_trace.new_catalyst_within_14d\")\n ),\n \"tier_explanation\": _i18n_t(\n \"section.cex_trace.tier_explanation\",\n tier=cex_trace[\"tier\"],\n tier_desc=_i18n_t(\n \"section.cex_trace.tier_desc_S2\" if cex_trace[\"has_binance_perp\"]\n else \"section.cex_trace.tier_desc_S1\"\n ),\n ),\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n \"liq\": {\n \"rows\": liq[\"liq_rows\"],\n \"current_price_usd\": liq[\"current_price_usd\"],\n \"dex_pool_addr\": liq[\"dex_pool_addr\"],\n \"dex_pool_liquidity_usd\": liq[\"dex_pool_liquidity_usd\"],\n \"alpha_5pct_depth_usd_est\": liq[\"alpha_5pct_depth_usd_est\"],\n \"lp_24h_flow\": liq[\"lp_24h_flow\"],\n \"interpretation\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n \"decision_anchors\": liq[\"decision_anchors_partial\"],\n\n \"_pipeline_timings\": timings,\n \"_pipeline_total_seconds\": time.perf_counter() - t_start,\n }\n\n # ---------- Round 9: behavior_classifier (v0.7.26) ----------\n # Deterministic multi-label classifier translates raw detector outputs\n # into 10 forensic-state labels (A1/A2/A3/B1/B2/C1/C2/C3/D1/D2).\n # Calibrated thresholds derived from 8-token backtest (v0725_5_CALIBRATION.md).\n # 0 surf cost — pure Python derive from the already-assembled skeleton.\n # If classifier raises (defensive: corrupt skeleton), inject an empty\n # profile rather than aborting the whole pipeline.\n try:\n from helpers.behavior_classifier import build_profile as _build_behavior_profile\n skeleton[\"behavior_profile\"] = _build_behavior_profile(skeleton)\n except Exception as _e:\n import sys as _sys\n print(f\"[behavior_classifier] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"behavior_profile\"] = {\n \"_schema_version\": \"v0.7.26\",\n \"active_labels\": [],\n \"primary_behavior\": None,\n \"by_label\": {},\n \"_error\": str(_e),\n }\n\n # ---------- Round 9.6: screen_summary (v0.8.7.0) ----------\n # 6-dimension deterministic TL;DR for `## 0. 一屏结论`. Built AFTER\n # behavior_profile so chain_state / risk_score etc. are populated.\n # Derive chain_state into skeleton (render reads same logic but emit\n # only to JSON footer — we need it on skel for screen_summary).\n try:\n from render_report import _derive_chain_state_neutral_5tier as _ds5\n _cs = _ds5(skeleton)\n skeleton[\"chain_state\"] = _cs[\"subtype\"]\n skeleton[\"chain_state_label\"] = _cs[\"label\"]\n skeleton[\"chain_state_risk_score\"] = _cs[\"risk\"]\n except Exception as _e:\n import sys as _sys\n print(f\"[chain_state_derive] failed (non-fatal): {_e}\", file=_sys.stderr)\n # ---------- Round 9.5: hidden_operator_enricher (v0.8.2) ----------\n # Append heuristically-flagged hidden operator wallets to\n # monitoring_wallets BEFORE annotate_monitoring_wallets so they get\n # scored + exported to paste.json. Currently only fake-mining\n # cluster members (from mint_authority_dumps detect → fake cluster).\n # 0 surf cost.\n try:\n from helpers.hidden_operator_enricher import enrich_monitoring_with_hidden_operators\n enrich_monitoring_with_hidden_operators(skeleton)\n except Exception as _e:\n import sys as _sys\n print(f\"[hidden_operator_enricher] failed (non-fatal): {_e}\", file=_sys.stderr)\n\n # ---------- Round 9.6: hidden_operator_activity (v0.8.3) ----------\n # Run (a)/(b) confirmed-sells query against the hidden operator set\n # to surface whether the heuristically-flagged wallets have already\n # started realizing. ~10-30 credits, parallelizable with dump_tracker\n # queries (currently sequential here for simplicity).\n try:\n from helpers.hidden_operator_activity import probe_hidden_operator_activity\n skeleton[\"hidden_operator_activity\"] = probe_hidden_operator_activity(\n ca=skeleton.get(\"meta\", {}).get(\"contract_address\") or \"\",\n skel=skeleton,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[hidden_operator_activity] failed (non-fatal): {_e}\", file=_sys.stderr)\n # v0.8.3.1 audit HIGH #2 fix: return COMPLETE zero shape on\n # exception, not partial {\"_error\": ...} dict. Template uses\n # StrictUndefined; partial dict raises on `.n_hidden_wallets_tracked`\n # access in the elif branch.\n skeleton[\"hidden_operator_activity\"] = {\n \"n_hidden_wallets_tracked\": 0,\n \"confirmed_cex_tokens\": 0.0,\n \"confirmed_dex_tokens\": 0.0,\n \"confirmed_total_tokens\": 0.0,\n \"confirmed_total_pct_circ\": None,\n \"confirmed_est_usd\": None,\n \"confirmed_est_usd_source\": \"none\",\n \"n_distinct_cex_destinations\": 0,\n \"n_dex_swaps\": 0,\n \"cex_destination_brands\": [],\n \"date_floor\": None,\n \"_error\": str(_e)[:120],\n }\n\n # ---------- Round 10: monitoring_ranker (v0.7.27) ----------\n # Deterministic per-wallet score → 4-tier level (CRITICAL / HIGH /\n # NORMAL / NOT_TRACKED). Annotates monitoring_wallets[] in-place.\n # paste.json export (downstream in render) sorts + filters by level.\n # 0 surf cost. Defensive try/except mirrors Round 9 pattern.\n try:\n from helpers.monitoring_ranker import annotate_monitoring_wallets, summary_by_level\n annotate_monitoring_wallets(skeleton)\n skeleton[\"monitoring_summary\"] = {\n \"_schema_version\": \"v0.7.27\",\n \"_calibrated_for\": \"context_B_crypto_operator\",\n \"level_counts\": summary_by_level(skeleton.get(\"monitoring_wallets\") or []),\n }\n except Exception as _e:\n import sys as _sys\n print(f\"[monitoring_ranker] failed (non-fatal): {_e}\", file=_sys.stderr)\n # v0.7.27.1 adversarial review MED fix: include `_calibrated_for` even on\n # failure path so validator (which fail-closes on absent locked\n # scalar paths) doesn't abort the whole render.\n skeleton[\"monitoring_summary\"] = {\n \"_schema_version\": \"v0.7.27\",\n \"_calibrated_for\": \"context_B_crypto_operator\",\n \"level_counts\": {\"CRITICAL\": 0, \"HIGH\": 0, \"NORMAL\": 0, \"NOT_TRACKED\": 0},\n \"_error\": str(_e),\n }\n\n # ---------- Round 10.4: recent (72h) flow actions (v1.2.9) ----------\n # product spec 2026-06-29 (JCT/Janction): is the operator, RIGHT NOW, preparing the next\n # dump — a mint-authority / cluster hub fanning out to fresh sockpuppets, or many\n # EOAs consolidating into a CEX? cex_fanout only catches CEX-SOURCE fan-out and\n # missed JCT's mint-authority → 50-EOA seed. Cheap (2 SQL, 3-day window) on the\n # Alpha chain; surfaced at highest priority in 一屏结论 / 速读.\n try:\n from helpers.recent_flow_actions import detect_recent_flow_actions\n # v1.2.9 (codex MED): only clear operator-SOURCE roles — a fan-out FROM these\n # is a real seeding signal. Excludes fanout_recipient (a recipient, not a\n # source), treasury_vesting (a legit vesting distributor paying 10+ would\n # false-positive), and direct_dumper (already-sold, not a seeder).\n _op_roles = {\"deployer\", \"mint_authority\", \"suspected_operator_reserve\",\n \"fake_mining_cluster_member\", \"cex_fanout_hub\",\n \"high_throughput_operator\"}\n _cex_roles = {\"public_cex_hot_wallet\", \"cex_deposit_destination\"}\n _infra_roles = {\"dex_pool\", \"router_aggregator\"}\n _mw = skeleton.get(\"monitoring_wallets\") or []\n _rf_op = {(_w.get(\"addr_full\") or \"\").lower() for _w in _mw\n if _w.get(\"monitor_role_enum\") in _op_roles}\n _rf_cex = {(_w.get(\"addr_full\") or \"\").lower() for _w in _mw\n if _w.get(\"monitor_role_enum\") in _cex_roles}\n _rf_infra = {(_w.get(\"addr_full\") or \"\").lower() for _w in _mw\n if _w.get(\"monitor_role_enum\") in _infra_roles}\n # mint authorities from funding_attribution (may not all be in monitoring)\n for _a in ((funding_attribution.get(\"mint_authorities\") or {}).get(\"authorities\") or []):\n if _a.get(\"addr\"):\n _rf_op.add(_a[\"addr\"].lower())\n try:\n from chain_router import set_active_chain as _sac\n _sac(scope.get(\"chain_id\"))\n except Exception:\n pass\n skeleton[\"recent_flow_actions\"] = detect_recent_flow_actions(\n ca=ca, operator_addrs=_rf_op, cex_addrs=_rf_cex, infra_addrs=_rf_infra,\n window_days=3, min_counterparties=10,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[recent_flow_actions] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"recent_flow_actions\"] = {\"_error\": str(_e)[:120]}\n\n # ---------- Round 10.45: recent_mint_events (v1.2.14) ----------\n # product spec 2026-07-01 (TAC): mint_authorities carries no timestamp, so a large RECENT\n # mint from 0x0 (operator creating fresh ammo — often the pump source) was invisible\n # in the 一屏结论 / 速读. Cheap (1 SQL, daily GROUP BY) on the Alpha chain; flags the\n # largest significant mint (>= 1% circ) within the recent window with its date.\n try:\n from helpers.recent_mint_events import detect_recent_mint_events\n try:\n from chain_router import set_active_chain as _sac2\n _sac2(scope.get(\"chain_id\"))\n except Exception:\n pass\n _circ = float((skeleton.get(\"meta\") or {}).get(\"circulating_supply\") or 0)\n skeleton[\"recent_mint_events\"] = detect_recent_mint_events(\n ca=ca, circ_supply=_circ, recent_window_days=14, min_pct_circ=1.0,\n )\n except Exception as _e:\n import sys as _sys\n print(f\"[recent_mint_events] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"recent_mint_events\"] = {\"_error\": str(_e)[:120]}\n\n # ---------- Round 10.5: screen_summary (v1.0.4 — moved here) ----------\n # v1.0.4 (O 2026-06-20): build_screen_summary MUST run AFTER\n # annotate_monitoring_wallets (Round 10) + hidden_operator_enricher\n # (Round 9.5). The chip-structure 3-way (screen_summary._compute_chip_3way)\n # builds its operator union from monitoring_wallets[].monitor_role_enum,\n # which is ONLY assigned by annotate_monitoring_wallets. Built earlier (the\n # old Round 9 position), monitor_role_enum was still unset → op_union empty\n # → 一屏「筹码结构」reported operator 0.0% (假\"分散\") while the render-side\n # 真实派发 section — computed at render time, post-annotation — reported the\n # real operator % (e.g. O: 83.4% 高控盘). Same skeleton, contradictory\n # headline vs detail. Building here makes the headline use the identical\n # annotated data the detailed section does. chain_state (its other input)\n # is set in Round 9 above, so it is available.\n # v1.0.5 (ARX 2026-06-22): compute the neutral-infra exclusion set ONCE\n # (DEX/CEX infra that surf/Arkham labelled — must not count as operator).\n # Stored on the skeleton so the Python chip math AND the jinja 真实派发\n # classifier read the identical set → headline == detail.\n try:\n from helpers.screen_summary import compute_neutral_infra_addrs as _cni\n skeleton[\"_neutral_infra_addrs\"] = _cni(skeleton)\n except Exception as _e:\n import sys as _sys\n print(f\"[neutral_infra] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"_neutral_infra_addrs\"] = []\n try:\n from helpers.screen_summary import build_screen_summary as _build_screen_summary\n skeleton[\"screen_summary\"] = _build_screen_summary(skeleton)\n import sys as _sys\n _ss = skeleton[\"screen_summary\"]\n print(f\"[screen_summary] {len(_ss.get('dimensions') or [])} dimensions, \"\n f\"one_sentence='{(_ss.get('one_sentence') or '')[:80]}'\",\n file=_sys.stderr)\n except Exception as _e:\n import sys as _sys\n print(f\"[screen_summary] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"screen_summary\"] = {\"_error\": str(_e)[:200]}\n\n # ---------- Round 11: address_role_resolver (v0.7.28) ----------\n # Build address_role_index — every address that appears across\n # detectors gets a primary_role + all_roles[] + cross-link target.\n # Render template uses this for selective dedupe (high_throughput +\n # fanout recipients sections only) + safety-net JSON dump in\n # machine_readable footer. 0 surf cost.\n try:\n from helpers.address_role_resolver import build_address_role_index\n skeleton[\"address_role_index\"] = build_address_role_index(skeleton)\n except Exception as _e:\n import sys as _sys\n print(f\"[address_role_resolver] failed (non-fatal): {_e}\", file=_sys.stderr)\n skeleton[\"address_role_index\"] = {}\n\n # v0.9.4: surf credit accounting. Pre-v0.9.4 the skeleton's three\n # `_credits_used=0` entries were leftover placeholders for skipped\n # detectors (cross_sym / wash / flow_operators). Now we record the\n # actual cumulative usage at top level so reports + monitoring can\n # show real cost per CA — and surface retry-storm waste (timeout\n # attempts billed for partial scan).\n try:\n # See entry-point comment: bare `section_a_scope` import — must match\n # the module instance parallel_surf writes to.\n from section_a_scope import _surf_credit_snapshot\n _credits_after = _surf_credit_snapshot()\n _b = _credits_before_snapshot\n skeleton[\"_surf_credits\"] = {\n \"credits_used\": round(_credits_after[\"credits\"] - _b[\"credits\"], 2),\n \"successful_calls\": _credits_after[\"calls\"] - _b[\"calls\"],\n \"billed_attempts\": _credits_after[\"attempts\"] - _b[\"attempts\"],\n \"wall_seconds\": round(_credits_after[\"seconds\"] - _b[\"seconds\"], 2),\n }\n except Exception as _e:\n import sys as _sys\n print(f\"[surf_credit_accounting] failed (non-fatal): {_e}\", file=_sys.stderr)\n\n return skeleton\n\n\ndef _build_verdict_block(rule11: dict, anomaly72: dict) -> dict:\n enum, cn_label, baseline = _derive_verdict_enum(rule11, anomaly72)\n # next_tier is the enum the verdict CAN downgrade to (AVOID if currently\n # EXIT_IF_HOLDING, else same). Cross-LLM audit alpha.8 MEDIUM finding:\n # next_tier_cn was previously derived from `enum`, not `next_tier_enum`,\n # producing an (AVOID + 建议卖出) inconsistency for EXIT_IF_HOLDING cases.\n # Both fields must come from `next_tier_enum` to keep enum/label atomic.\n next_tier_enum = \"AVOID\" if enum == \"EXIT_IF_HOLDING\" else enum\n return {\n \"enum\": enum,\n \"cn_label\": cn_label,\n \"baseline\": baseline,\n \"downgrade_applied\": 1 if enum != baseline else 0,\n \"next_tier_enum\": next_tier_enum,\n \"next_tier_cn\": _i18n_t(f\"verdict.cn_label.{next_tier_enum}\"),\n \"one_liner\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n }\n\n\ndef _build_decision_action_block(rule11: dict, anomaly72: dict, liq: dict | None = None) -> dict:\n enum, _, _ = _derive_verdict_enum(rule11, anomaly72)\n action = _derive_action_enum(enum)\n # Pipeline-derived numeric slots (Phase B.1 fills from section_liq)\n liq = liq or {}\n overrides = liq.get(\"decision_action_overrides\", {}) if liq else {}\n current_price = liq.get(\"current_price_usd\")\n stop_trigger = overrides.get(\"stop_loss_trigger_price\")\n\n return {\n \"immediate_action\": {\n \"action_enum\": action,\n \"venue_enum\": \"alpha\",\n \"tranches_n\": 3 if action == \"sell\" else 1,\n \"tranche_max_usd\": overrides.get(\"tranche_max_usd\", 10000),\n \"horizon_hours\": 48 if action == \"sell\" else 0,\n \"slippage_pct_cap\": 3,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n \"stop_loss\": {\n \"trigger_price_usd\": stop_trigger,\n \"current_price_usd\": current_price,\n \"delta_pct\": (\n round((stop_trigger / current_price - 1) * 100, 1)\n if (stop_trigger and current_price) else None\n ),\n \"rationale\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n },\n # Re-entry conditions: pipeline pre-fills with Rule 11 quiet wallet\n # condition + circ-ratio threshold; LLM writes per-condition narrative.\n \"re_entry_conditions\": _build_re_entry_conditions(rule11, anomaly72),\n }\n\n\ndef _build_re_entry_conditions(rule11: dict, anomaly72: dict) -> list[dict]:\n \"\"\"Pipeline-derived re-entry conditions. Each entry has condition_type +\n threshold + current_value (all locked), plus narrative (writable).\n\n v0.7.19.3 adversarial review MEDIUM#1: `quiet` set must exclude protocol_lockup\n wallets — vesting / multisig / treasury are NOT insider quiet\n candidates. Without this filter the \"rule11_quiet_wallets_dumped_pct\"\n re-entry condition would surface vesting contracts as if they were\n quiet insiders waiting to dump (the same vesting-vs-insider conflation\n that the COLLECT report's headline \"潜伏 80% 抛压\" came from).\n \"\"\"\n conds = []\n quiet = [\n r for r in rule11.get(\"pre_launch_receivers\", [])\n if r.get(\"dumped_pct\") == 0\n and not r.get(\"is_protocol_lockup\")\n ]\n if quiet:\n total_quiet = sum((r.get(\"current_balance\") or 0) for r in quiet)\n conds.append({\n \"condition_type\": \"rule11_quiet_wallets_dumped_pct\",\n \"threshold\": 80,\n \"current_value\": 0,\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n conds.append({\n \"condition_type\": \"alpha_anomaly_72h_event_count\",\n \"threshold\": 0,\n \"current_value\": anomaly72.get(\"n_recent_events\", 0),\n \"narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n return conds\n\n\ndef main() -> int:\n # Beta.15: force UTF-8 stdout/stderr (Windows cp1252 chokes on\n # '→' / emoji / 中文 in progress prints). Same fix as render_report.py.\n for stream in (sys.stdout, sys.stderr):\n if hasattr(stream, \"reconfigure\"):\n try:\n stream.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"ca\", nargs=\"?\", help=\"Contract address 0x... (positional or via --ca)\")\n ap.add_argument(\"--ca\", dest=\"ca_opt\", default=None, help=\"Contract address\")\n ap.add_argument(\"--alpha-listing-date\", default=None,\n help=\"YYYY-MM-DD. If omitted, derives from Alpha API.\")\n # v0.6.1: --out-dir is new preferred CLI. It auto-creates the structured\n # layout: \u003cout_dir>/.work/skeleton.json + \u003cout_dir>/monitoring/.\n # --out is kept for backward compat (skeleton at exact path, monitoring\n # next to it). Pick whichever based on which flag user provides.\n ap.add_argument(\"--out-dir\", dest=\"out_dir\", default=None,\n help=\"Output dir. v0.6.1 preferred. Auto-creates \"\n \"\u003cdir>/.work/skeleton.json + \u003cdir>/monitoring/.\")\n ap.add_argument(\"--out\", default=None,\n help=\"Legacy: skeleton.json file path. monitoring/ \"\n \"written alongside. Use --out-dir for v0.6.1 layout.\")\n # v0.6.2: --lang flag for i18n. Default zh (backward compat). When set\n # to \"en\", pipeline helpers emit English labels in skeleton; render\n # must be called with same --lang for consistency.\n ap.add_argument(\"--lang\", default=\"zh\", choices=(\"zh\", \"en\"),\n help=\"Pipeline output language for labels (default: zh).\")\n args = ap.parse_args()\n\n # Set lang BEFORE build_skeleton() so all helpers using t() see correct lang.\n sys.path.insert(0, str(Path(__file__).parent / \"helpers\"))\n from i18n import set_lang\n set_lang(args.lang)\n\n # v0.6.5: silent update-check (24h cache, 3s timeout, non-blocking).\n # Prints i18n-localized one-liner to stderr if a newer version exists.\n # Disable via BINANCE_ALPHA_NO_UPDATE_CHECK=1.\n try:\n from update_check import check_for_update\n _u = check_for_update()\n # v0.9.5: auto-update on detection (audit Finding 1).\n # When update_check successfully ran `npx skills update hertzflow`,\n # the files on disk are now newer than what THIS process loaded.\n # Exit cleanly so the user can re-invoke and pick up the new code.\n # IMPORTANT: only the CLI entry-point may exit here. update_check\n # itself NEVER calls sys.exit (audit Finding 1: imported\n # library users like Discord bot batches must not be killed by\n # an upstream `check_for_update()` call).\n if isinstance(_u, dict) and _u.get(\"auto_updated\"):\n return 0\n except SystemExit:\n raise # never silently swallow SystemExit; preserve exit code\n except Exception:\n pass # update check never blocks main pipeline\n\n ca = args.ca or args.ca_opt\n if not ca:\n ap.error(\"CA required (positional or --ca)\")\n\n # Determine layout: --out-dir takes precedence; --out is legacy fallback;\n # default = current dir with v0.6.1 layout.\n if args.out_dir:\n out_dir = Path(args.out_dir)\n out_dir.mkdir(parents=True, exist_ok=True)\n work_dir = out_dir / \".work\"\n work_dir.mkdir(parents=True, exist_ok=True)\n skeleton_path = work_dir / \"skeleton.json\"\n monitoring_parent = out_dir # monitoring/ goes under out_dir, not under .work\n elif args.out:\n skeleton_path = Path(args.out)\n monitoring_parent = skeleton_path.parent\n else:\n # No flag → default v0.6.1 layout in current dir\n out_dir = Path(\".\")\n work_dir = out_dir / \".work\"\n work_dir.mkdir(parents=True, exist_ok=True)\n skeleton_path = work_dir / \"skeleton.json\"\n monitoring_parent = out_dir\n\n # v0.7.17: banner moved from stderr → stdout. Windows PowerShell 5.1 turns\n # native-command stderr into a NativeCommandError record, which makes the\n # wrapper look like the pipeline crashed even though Python + surf are\n # still alive (Windows adversarial review BEAT rerun handoff 2026-05-28). audit\n # HIGH#1: if the caller chose `--out -` (stdout for the skeleton JSON),\n # writing a banner to stdout corrupts the JSON stream; in that case skip\n # the banner entirely (the file dest is stdout so stderr is the only\n # stream left for diagnostics, but PowerShell never sees `--out -`\n # because there is no `-` pseudo-path in the current CLI — guard anyway).\n _out_is_stdout = args.out is not None and args.out in (\"-\", \"/dev/stdout\")\n if not _out_is_stdout:\n print(f\"forensic_pipeline.py v0.6 — CA={ca}\", file=sys.stdout, flush=True)\n else:\n print(f\"forensic_pipeline.py v0.6 — CA={ca}\", file=sys.stderr, flush=True)\n\n # v0.7.17: incremental skeleton checkpoint. Each section dumps its raw\n # output to .work/skeleton_partial.json the moment it completes, so a long\n # tail section (e.g. wash_infra_detector at ~13 min on Windows-on-ARM\n # surf.exe x86_64 emulation) being interrupted by the user no longer\n # leaves them with zero durable output. After all sections finish, the\n # final assembled skeleton is written to skeleton.json as before.\n partial_path = skeleton_path.parent / \"skeleton_partial.json\"\n # audit HIGH#3: clear any stale checkpoint from a prior interrupted\n # run so the file `_last_section_completed` always reflects the CURRENT\n # invocation, never a previous one that died before its first _ck.\n try:\n if partial_path.exists():\n partial_path.unlink()\n except Exception as e:\n print(f\"[checkpoint] stale cleanup failed: {e}\", file=sys.stderr, flush=True)\n skeleton = build_skeleton(\n ca=ca, alpha_listing_date=args.alpha_listing_date,\n checkpoint_path=partial_path,\n )\n\n payload = json.dumps(skeleton, ensure_ascii=False, indent=2, default=str)\n skeleton_path.write_bytes(payload.encode(\"utf-8\"))\n # audit HIGH#2: delete the checkpoint once the final skeleton is on\n # disk. Without this, a successful run leaves `skeleton_partial.json`\n # next to `skeleton.json` and an operator inspecting the directory cannot\n # tell whether the partial belongs to this run (now harmless) or a NEXT\n # invocation that has not yet started.\n try:\n if partial_path.exists():\n partial_path.unlink()\n except Exception as e:\n print(f\"[checkpoint] final cleanup failed: {e}\",\n file=sys.stderr, flush=True)\n\n status = skeleton.get(\"_status\", \"ok\")\n timings = skeleton.get(\"_pipeline_timings\", {})\n total = skeleton.get(\"_pipeline_total_seconds\", 0)\n print(f\" status: {status}\", file=sys.stderr)\n print(f\" total: {total:.1f}s\", file=sys.stderr)\n for k, v in timings.items():\n print(f\" {k}: {v:.1f}s\", file=sys.stderr)\n print(f\" wrote: {skeleton_path} ({len(payload)} bytes)\", file=sys.stderr)\n\n # v0.6.0-beta.3: monitoring_wallets multi-format export. Original v0.4.8\n # deliverable that v0.6 重写 dropped. v0.6.1 emits 2 files under\n # \u003cout_dir>/monitoring/: paste.json + wallets_full.json.\n if status == \"ok\":\n meta = skeleton.get(\"meta\", {})\n mw_export = monitoring_export.write_all(\n symbol=meta.get(\"symbol\", \"UNKNOWN\"),\n chain=meta.get(\"chain\", \"BSC\"),\n contract_address=meta.get(\"contract_address\", ca),\n monitoring_wallets=skeleton.get(\"monitoring_wallets\", []),\n out_dir=monitoring_parent,\n lang=args.lang,\n )\n print(f\" monitoring: {mw_export['n_wallets']} wallets → {mw_export['dir']}\", file=sys.stderr)\n for fname in mw_export[\"files\"]:\n print(f\" {fname}\", file=sys.stderr)\n\n return 0 if status == \"ok\" else 1\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":158407,"content_sha256":"72f2056cb997303f21b5c8dae6bdace27f8165ab40c11bb501e5ca795a35107e"},{"filename":"alpha/v06/helpers/_version.py","content":"\"\"\"Single source of truth for skill version.\n\nBoth forensic_pipeline.py (writes `_schema_version` into skeleton) and\nupdate_check.py (compares against latest GitHub tag) read from here, so\nrelease bumps only touch one file.\n\"\"\"\n\n__version__ = \"1.2.16\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":252,"content_sha256":"9a840279d556e5b92040d243388059ab16e25d8147eb4fb0818435a4415bbb18"},{"filename":"alpha/v06/helpers/address_role_resolver.py","content":"\"\"\"v0.7.28 — Deterministic address role resolver + cross-link index.\n\nWalks all detector outputs in the skeleton, collects EVERY address that\nappears (whether in mint_authorities / cex_fanout_hubs / high_throughput\n/ monitoring_wallets / etc), groups by addr, picks the highest-priority\nprimary_role per address, emits skeleton.address_role_index keyed by\nlowercased address.\n\nGoal: stop the report from repeating the same wallet's full card\nacross 4 sections. The render template uses the index to:\n - Show full card ONLY in the address's primary_role section.\n - Replace duplicate card with a compact link badge in other sections.\n - Append the index to machine_readable_json so LLMs / AI 二次研究 can\n cross-reference even without following markdown anchors.\n\nSelective dedupe scope (v0.7.28):\n - high_throughput_dumpers list: dedupe (highest duplicate ratio —\n JCT has ~80% of HT operators also in fan-out hub recipients).\n - fan-out hub recipients list: dedupe.\n - mining-fed wallet outflows: KEEP FULL (only 2-5 entries, info\n density too high to summarize).\n - monitoring_wallets: KEEP ALL (retail needs to import), add a\n \"primary: X\" badge column.\n - mint_authorities section: KEEP FULL (only 2-8 entries typically).\n\nThis balances retail size reduction (-25-35% on dense JCT/H reports)\nwith LLM safety net (index always dumped to JSON tail).\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom i18n import t # v0.6.2 i18n\n\n# ----------------------------------------------------------------------\n# Role priority — highest priority FIRST. When an address appears under\n# multiple roles, primary_role = the one earliest in this list.\n# ----------------------------------------------------------------------\nROLE_PRIORITY = [\n \"mint_authority\", # supply source — every move is signal\n \"cex_fanout_hub\", # control point — upstream of recipients\n \"direct_dumper\", # confirmed insider sell action\n \"high_throughput_operator\", # historical sweeper\n \"fanout_recipient\", # downstream sub-wallet\n \"anomaly_participant\", # near-term observable\n \"cross_alpha_whale\", # cross-token presence\n \"deployer\", # static role, info-only without activity\n \"treasury_vesting\", # known time-table actor\n \"unknown_top_holder\", # generic large holder\n \"dex_pool\", # infra (DEX main pool)\n \"router_aggregator\", # infra (DEX router)\n \"public_cex_hot_wallet\", # infra (public CEX hot wallet)\n \"other\",\n]\n\n\n# ----------------------------------------------------------------------\n# Section anchor + zh label for primary role. Render template uses these\n# to assemble the compact \"[→ 详见 X 段]\" link.\n# Anchors are the markdown section IDs auto-generated from the H2\n# headings (kebab-case of the Chinese title). render_report.py keeps\n# these stable via inline anchor tags where helpful.\n# ----------------------------------------------------------------------\n# v0.7.28.1 adversarial review HIGH fix: explicit anchors emit as `\u003ca id=\"X\">\u003c/a>`\n# right BEFORE the corresponding heading (render_report.py injects these\n# at v0.7.28.1+). Don't rely on markdown auto-anchor heuristics which\n# vary across renderers and break on Chinese/emoji/parens.\n# v0.7.28.1 adversarial review HIGH fix: rename labels to avoid banned words\n# (\"派\" in \"真实派发\", \"进场\" in \"进场上限\"). 链上侦测-neutral.\n# v0.6.2 i18n: `label_key` holds an i18n key; the user-facing label is\n# resolved via t() at build time (after the pipeline calls set_lang), so\n# the same SECTION_INFO produces zh or en labels per active language.\nSECTION_INFO = {\n \"mint_authority\": {\n \"anchor\": \"section-bridge-mint\",\n \"label_key\": \"addr_role.section_label.mint_authority\",\n },\n \"cex_fanout_hub\": {\n \"anchor\": \"section-cex-fanout\",\n \"label_key\": \"addr_role.section_label.cex_fanout_hub\",\n },\n \"fanout_recipient\": {\n \"anchor\": \"section-cex-fanout\",\n \"label_key\": \"addr_role.section_label.fanout_recipient\",\n },\n \"direct_dumper\": {\n \"anchor\": \"section-real-distribution\",\n \"label_key\": \"addr_role.section_label.direct_dumper\",\n },\n \"high_throughput_operator\": {\n \"anchor\": \"section-high-throughput\",\n \"label_key\": \"addr_role.section_label.high_throughput_operator\",\n },\n \"anomaly_participant\": {\n \"anchor\": \"section-recent-anomaly\",\n \"label_key\": \"addr_role.section_label.anomaly_participant\",\n },\n \"cross_alpha_whale\": {\n \"anchor\": \"section-cross-sym\",\n \"label_key\": \"addr_role.section_label.cross_alpha_whale\",\n },\n \"deployer\": {\n \"anchor\": \"section-alloc\",\n \"label_key\": \"section.alloc.title\",\n },\n \"treasury_vesting\": {\n \"anchor\": \"section-alloc\",\n \"label_key\": \"section.alloc.title\",\n },\n \"dex_pool\": {\n \"anchor\": \"section-liq\",\n \"label_key\": \"addr_role.section_label.dex_pool\",\n },\n \"router_aggregator\": {\n \"anchor\": \"section-monitoring\",\n \"label_key\": \"addr_role.section_label.monitoring\",\n },\n \"public_cex_hot_wallet\": {\n \"anchor\": \"section-monitoring\",\n \"label_key\": \"addr_role.section_label.monitoring\",\n },\n \"unknown_top_holder\": {\n \"anchor\": \"section-holdings\",\n \"label_key\": \"addr_role.section_label.unknown_top_holder\",\n },\n \"other\": {\n \"anchor\": \"section-monitoring\",\n \"label_key\": \"addr_role.section_label.monitoring\",\n },\n}\n\n\ndef _norm_addr(a: Any) -> str:\n \"\"\"Lowercase + strip. Returns empty string on None/non-str.\"\"\"\n if not isinstance(a, str):\n return \"\"\n return a.strip().lower()\n\n\ndef _collect_addresses_per_role(skel: dict) -> dict[str, set[str]]:\n \"\"\"Walk every detector list and collect addresses observed per role.\n Output: {role_enum: set[addr_lower]}.\n\n Defensive against missing keys / detector skips — every list is\n optional. Solana abort tokens (FARTCOIN, JELLYJELLY) will return\n mostly-empty sets.\n \"\"\"\n out: dict[str, set[str]] = {role: set() for role in ROLE_PRIORITY}\n\n fa = skel.get(\"funding_attribution\") or {}\n\n # mint_authority (only non-excluded)\n for a in ((fa.get(\"mint_authorities\") or {}).get(\"authorities\") or []):\n if a.get(\"is_excluded\"):\n continue\n addr = _norm_addr(a.get(\"addr\"))\n if addr:\n out[\"mint_authority\"].add(addr)\n\n # cex_fanout_hub + fanout_recipient\n for h in ((fa.get(\"cex_fanout_hubs\") or {}).get(\"hubs\") or []):\n haddr = _norm_addr(h.get(\"addr\"))\n if haddr:\n out[\"cex_fanout_hub\"].add(haddr)\n for r in (h.get(\"top_recipients\") or []):\n raddr = _norm_addr(r.get(\"addr\"))\n if raddr:\n out[\"fanout_recipient\"].add(raddr)\n\n # high_throughput_operator (only non-excluded)\n for d in ((fa.get(\"high_throughput_dumpers\") or {}).get(\"dumpers\") or []):\n if d.get(\"is_excluded\"):\n continue\n addr = _norm_addr(d.get(\"addr\"))\n if addr:\n out[\"high_throughput_operator\"].add(addr)\n\n # direct_dumper — m6 ∩ top_seller_addrs (matches behavior_classifier C1\n # semantics: insider-confirmed dump path, not generic top sellers).\n # v0.7.28.1 adversarial review HIGH fix: skeleton m6 rows use `addr_full`, not\n # `address`/`addr`. The previous lookup never matched, so direct_dumper\n # set was always empty on real skeletons.\n m6_addrs: set[str] = set()\n for r in ((skel.get(\"lineage\") or {}).get(\"m6\") or {}).get(\"rows\") or []:\n addr = _norm_addr(\n r.get(\"addr_full\") or r.get(\"address\") or r.get(\"addr\")\n )\n if addr:\n m6_addrs.add(addr)\n dt = skel.get(\"dump_tracking\") or {}\n top_sellers = {_norm_addr(a) for a in (dt.get(\"top_seller_addrs\") or []) if a}\n out[\"direct_dumper\"] = (m6_addrs & top_sellers) if m6_addrs else set()\n\n # cross_alpha_whale\n for w in ((skel.get(\"cross_sym\") or {}).get(\"whales\") or []):\n addr = _norm_addr(w.get(\"address\"))\n if addr:\n out[\"cross_alpha_whale\"].add(addr)\n\n # deployer — only the deployer_addr itself. M6 rows are NOT\n # deployer-class (they're direct_dumper / anomaly_participant);\n # v0.7.28.1 adversarial review MED fix: comment previously said \"flowchart_nodes\n # + m6\" which over-claimed. Now correctly reflects code.\n lineage = skel.get(\"lineage\") or {}\n dep = _norm_addr(lineage.get(\"deployer_addr\"))\n if dep:\n out[\"deployer\"].add(dep)\n # treasury_vesting — from cross_sym.role_classification Arkham hits\n for w in ((skel.get(\"cross_sym\") or {}).get(\"whales\") or []):\n if (w.get(\"arkham_label\") or \"\").lower() in (\"vesting\", \"treasury\", \"multisig\"):\n addr = _norm_addr(w.get(\"address\"))\n if addr:\n out[\"treasury_vesting\"].add(addr)\n\n # anomaly_participant — from anomaly.waves[].events[].from_to addrs.\n # v0.7.28.1 adversarial review MED fix: previous split-on-arrow logic was loose\n # (\"0xabc(label)→0xdef\" → invalid pseudo-address \"0xabc(label)\").\n # Use strict EVM-address regex over the whole from_to string. EVM\n # addresses cannot contain → or parens, so regex is unambiguous.\n import re as _re_local\n _evm_addr_re = _re_local.compile(r\"\\b0x[a-fA-F0-9]{40}\\b\")\n for w in (skel.get(\"anomaly\") or {}).get(\"waves\") or []:\n for ev in w.get(\"events\") or []:\n ft = ev.get(\"from_to\") or \"\"\n for match in _evm_addr_re.findall(ft):\n addr = _norm_addr(match)\n if addr:\n out[\"anomaly_participant\"].add(addr)\n\n # monitoring_wallets — entries already carry role enum from\n # monitoring_ranker (v0.7.27). Cross-feed router/public_cex_hot/\n # dex_pool/unknown_top_holder/other roles into the index.\n for w in (skel.get(\"monitoring_wallets\") or []):\n addr = _norm_addr(w.get(\"addr_full\") or w.get(\"address\"))\n if not addr:\n continue\n rid = w.get(\"monitor_role_enum\") or \"\"\n if rid == \"router_aggregator\":\n out[\"router_aggregator\"].add(addr)\n elif rid == \"public_cex_hot_wallet\":\n out[\"public_cex_hot_wallet\"].add(addr)\n elif rid == \"dex_pool\":\n out[\"dex_pool\"].add(addr)\n elif rid == \"unknown_top_holder\":\n out[\"unknown_top_holder\"].add(addr)\n elif rid == \"other\":\n out[\"other\"].add(addr)\n\n return out\n\n\ndef build_address_role_index(skel: dict) -> dict[str, dict]:\n \"\"\"Top-level entry. Returns the address_role_index keyed by\n lowercased address:\n\n {\n \"0x055a3b37...\": {\n \"primary_role\": \"cex_fanout_hub\",\n \"all_roles\": [\"cex_fanout_hub\", \"fanout_recipient\", \"high_throughput_operator\"],\n \"primary_section_anchor\": \"cex-fanout\",\n \"primary_section_label_zh\": \"🎯 CEX 提币 大规模分发 控筹\",\n \"addr_short\": \"0x055a3b37\"\n },\n ...\n }\n \"\"\"\n per_role = _collect_addresses_per_role(skel)\n all_addrs: set[str] = set()\n for s in per_role.values():\n all_addrs.update(s)\n\n index: dict[str, dict] = {}\n for addr in all_addrs:\n roles = [r for r in ROLE_PRIORITY if addr in per_role[r]]\n primary = roles[0] if roles else \"other\"\n info = SECTION_INFO.get(primary, SECTION_INFO[\"other\"])\n index[addr] = {\n \"primary_role\": primary,\n \"all_roles\": roles,\n \"primary_section_anchor\": info[\"anchor\"],\n \"primary_section_label_zh\": t(info[\"label_key\"]),\n \"addr_short\": addr[:10] if addr.startswith(\"0x\") else addr[:8],\n }\n return index\n\n\ndef is_primary_in_section(\n addr: str,\n section_role: str,\n address_role_index: dict[str, dict],\n) -> bool:\n \"\"\"Render helper: given an address and the section's role enum\n (e.g. 'high_throughput_operator' for the 🌊 high-throughput section),\n return True if THIS section is the address's primary section — i.e.\n show full card. Return False if primary is elsewhere — show compact\n cross-link.\n\n For fan-out section, treat both 'cex_fanout_hub' and 'fanout_recipient'\n as matching since they share the same anchor.\n \"\"\"\n if not addr:\n return True # safe default: full card if no addr to look up\n entry = address_role_index.get(_norm_addr(addr))\n if not entry:\n return True # never seen elsewhere → full card\n pr = entry.get(\"primary_role\")\n if pr == section_role:\n return True\n # fan-out hub + recipient share the same section\n if section_role == \"cex_fanout_hub\" and pr == \"fanout_recipient\":\n return True\n if section_role == \"fanout_recipient\" and pr == \"cex_fanout_hub\":\n return True\n return False\n\n\ndef cross_link_badge_zh(\n addr: str,\n address_role_index: dict[str, dict],\n) -> str:\n \"\"\"Render helper: return 1-line Chinese cross-link badge for compact\n display. e.g. \"[🎯 primary: 🎯 CEX 提币 大规模分发 控筹](#cex-大规模分发)\".\n Falls back to empty string if address is not in index.\n \"\"\"\n entry = address_role_index.get(_norm_addr(addr))\n if not entry:\n return \"\"\n return (\n f\"[🎯 primary: {entry['primary_section_label_zh']}]\"\n f\"(#{entry['primary_section_anchor']})\"\n )\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13467,"content_sha256":"a16b58f932c5d094d5a25df60a35ea65dc90abe13c1e567ea315ff02225d8da7"},{"filename":"alpha/v06/helpers/behavior_classifier.py","content":"\"\"\"v0.7.26 — Deterministic behavior classifier.\n\nTranslates raw 检测器 outputs into 10 multi-label \"庄家行为画像\"\ncategories (4 大类: 筹码准备 / 成交制造 / 出货行为 / 协同结构).\n\nThresholds calibrated via v0.7.25.5 backtest on 8 historical\nskeleton.json files (BSC tokens + 2 Solana abort negatives). See\nv0725_5_CALIBRATION.md for empirical evidence and threshold delta vs\nChatGPT v0.8 spec.\n\nSCOPE / CONTEXT:\n This classifier targets ONLY Context B (per M47 methodology context\n selection): crypto-operator-driven Binance Alpha tokens, where\n insider / operator behavior dominates and frequentist-friendly\n liquid-market thresholds (Context A) do not apply.\n\n Do NOT use these thresholds on Context A assets (BTC / ETH /\n large-cap CEX-listed tokens) without re-calibration — the wash bot\n share floor (0.05) would falsely flag legit HFT activity, and the\n \"insider hold ≥2%\" trigger would falsely flag legit VC / treasury\n positions.\n\nCALIBRATION CAVEATS (small n=8 sample):\n - LOO-sensitivity: each of H/JCT/BEAT does heavy lifting in the\n sample (jackknife removes any → ≥1 threshold's STRONG count drops\n by 1). See scripts/v0725_5_calibration_data.md.\n - D1 STRONG ≥ 4 — derived from sample max=3 (H only). Lowered from\n ChatGPT's ≥5 to enable future fire, but UNVALIDATED.\n - C2 MEDIUM/WEAK never fire in 8-token sample. Kept in code for\n future mid-range hits.\n - Schedule v0.8 expanded calibration (n ≥ 30).\n\nArchitecture (per ChatGPT v3 spec):\n raw_detectors → behavior_classifier → monitoring_ranker (v0.7.27)\n → markdown_renderer\n\nThe classifier is the *only* place that decides \"what is happening\non-chain\". Renderer should never invent labels — it just translates the\nclassifier's output into human-readable narrative.\n\nOutput shape:\n {\n \"active_labels\": [str], # all non-OFF labels, sorted by severity\n \"primary_behavior\": str, # highest-severity single label, or None\n \"by_label\": {\n \"\u003clabel_id>\": {\n \"severity\": str, # STRONG / MEDIUM / WEAK / OFF\n \"category\": str, # A/B/C/D\n \"trigger_metrics\": {...}, # raw inputs that fired the trigger\n \"human_summary_zh\": str # 1-sentence Chinese description\n }\n }\n }\n\nMulti-label by design: A1 + B1 + C3 + D2 can all be active on the same\ntoken. ChatGPT's earlier mutex gates (e.g. A1 trigger required\nanomaly \u003c 10) were dropped after calibration showed they suppress true\npositives.\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom i18n import t # v0.6.2 i18n\n\n# ----------------------------------------------------------------------\n# Severity ranking (used to pick primary_behavior + sort active_labels)\n# ----------------------------------------------------------------------\n_SEVERITY_RANK = {\"STRONG\": 3, \"MEDIUM\": 2, \"WEAK\": 1, \"OFF\": 0}\n\n# Behavior labels grouped by category. Order within each category =\n# priority for primary_behavior tiebreak when multiple labels share\n# the highest severity.\n_BEHAVIOR_ORDER = [\n \"A1\", \"A2\", \"A3\", # 筹码准备\n \"B1\", \"B2\", # 成交制造\n \"C1\", \"C2\", \"C3\", # 出货行为\n \"D1\", \"D2\", # 协同结构\n]\n\n_CATEGORY_OF = {\n \"A1\": \"A\", \"A2\": \"A\", \"A3\": \"A\",\n \"B1\": \"B\", \"B2\": \"B\",\n \"C1\": \"C\", \"C2\": \"C\", \"C3\": \"C\",\n \"D1\": \"D\", \"D2\": \"D\",\n}\n\n# v0.6.2 i18n: built lazily (at build_profile call time) so the active\n# language set by the pipeline is honored. Enum keys (A1/B1/... and\n# A/B/C/D) are stable; only the display values are translated.\ndef _label_names_zh() -> dict:\n return {\n \"A1\": t(\"behavior.label_name.A1\"),\n \"A2\": t(\"behavior.label_name.A2\"),\n \"A3\": t(\"behavior.label_name.A3\"),\n \"B1\": t(\"behavior.label_name.B1\"),\n \"B2\": t(\"behavior.label_name.B2\"),\n \"C1\": t(\"behavior.label_name.C1\"),\n \"C2\": t(\"behavior.label_name.C2\"),\n \"C3\": t(\"behavior.label_name.C3\"),\n \"D1\": t(\"behavior.label_name.D1\"),\n \"D2\": t(\"behavior.label_name.D2\"),\n }\n\n\ndef _category_names_zh() -> dict:\n return {\n \"A\": t(\"behavior.category_name.A\"),\n \"B\": t(\"behavior.category_name.B\"),\n \"C\": t(\"behavior.category_name.C\"),\n \"D\": t(\"behavior.category_name.D\"),\n }\n\n\ndef _g(d: Any, *keys: str, default: Any = None) -> Any:\n \"\"\"Safe nested .get() — returns default if any link in chain missing.\"\"\"\n cur = d\n for k in keys:\n if not isinstance(cur, dict):\n return default\n cur = cur.get(k)\n if cur is None:\n return default\n return cur\n\n\ndef _len(d: Any, *keys: str) -> int:\n v = _g(d, *keys, default=[])\n return len(v) if isinstance(v, (list, dict)) else 0\n\n\ndef derive_metrics(skel: dict) -> dict:\n \"\"\"Compute the raw metric set used by all 10 behavior triggers.\n\n Calibration-tested field paths (v0.7.25.5):\n - funding_attribution.mint_authorities.authorities[] (NOT\n mint_authority_dumps, which is a separate empty-shell key)\n - high_throughput_dumpers.dumpers[].total_in (NOT total_in_tokens)\n - Both honor is_excluded flag to skip infra-mint addresses.\n \"\"\"\n m: dict[str, Any] = {}\n\n # Meta\n m[\"symbol\"] = _g(skel, \"meta\", \"symbol\")\n m[\"total_supply\"] = _g(skel, \"meta\", \"total_supply\") or 0\n m[\"circ_supply\"] = _g(skel, \"meta\", \"circulating_supply\") or 0\n m[\"mcap_usd\"] = _g(skel, \"meta\", \"alpha_market_cap_usd\")\n m[\"vol_24h_usd\"] = _g(skel, \"meta\", \"alpha_vol_24h_usd\")\n m[\"lp_usd\"] = _g(skel, \"meta\", \"alpha_liquidity_usd\")\n\n # Lineage / m6\n m[\"m6_quiet\"] = _g(skel, \"lineage\", \"m6\", \"n_quiet\") or 0\n m[\"m6_full\"] = _g(skel, \"lineage\", \"m6\", \"n_full_dumper\") or 0\n\n # Dump tracker.\n # v0.8.6.7: use dump_tracking_mining net_sellout if larger than m6-only.\n # Mining fallback insider = m6 + mint_authority + cluster + ht + fanout +\n # wcg cluster (100-200 wallets vs m6 0-50). For cluster-heavy / cross-\n # chain / mining tokens, mining fallback catches the real $X — m6 only\n # missed it. Pick max so non-mining tokens (Velvet/COLLECT) unchanged.\n dt = skel.get(\"dump_tracking\") or {}\n dtm = skel.get(\"dump_tracking_mining\") or {}\n base_net = dt.get(\"confirmed_net_sellout_usd\") or 0\n mining_net = dtm.get(\"confirmed_net_sellout_usd\") or 0\n if mining_net > base_net and not dtm.get(\"_error\"):\n dt = dtm # prefer mining fallback for ALL dt-derived signals\n m[\"pure_insider_pct\"] = dt.get(\"pure_insider_holds_pct_supply\") or 0\n m[\"wash_dominated\"] = bool(dt.get(\"wash_dominated\") or False)\n m[\"wash_swap_count\"] = dt.get(\"total_dex_swaps\") or 0\n m[\"wash_top_bot_swaps\"] = dt.get(\"top_seller_swaps\") or 0\n m[\"wash_n_dex_sellers\"] = dt.get(\"n_dex_sellers\") or 0\n m[\"wash_top_bot_share\"] = (\n m[\"wash_top_bot_swaps\"] / m[\"wash_swap_count\"]\n if m[\"wash_swap_count\"] else 0.0\n )\n m[\"confirmed_cex_tokens\"] = dt.get(\"confirmed_cex_tokens\") or 0\n m[\"confirmed_dex_tokens\"] = dt.get(\"confirmed_dex_tokens\") or 0\n m[\"net_sellout_usd\"] = dt.get(\"confirmed_net_sellout_usd\") or 0\n m[\"sell_pct_circ\"] = (\n ((m[\"confirmed_cex_tokens\"] + m[\"confirmed_dex_tokens\"]) / m[\"circ_supply\"] * 100)\n if m[\"circ_supply\"] else 0.0\n )\n\n # 72h anomaly + truncation flag.\n # v0.7.25.5 calibration: detector caps at 100. STRONG must include\n # `count >= 50 OR truncated` so saturated tokens (h/jct = 100) fire\n # STRONG rather than being indistinguishable from a real \"exactly 50\".\n anomaly_summary = _g(skel, \"anomaly\", \"detector_summary\", default=[]) or []\n recent_n = 0\n recent_truncated = False\n for d in anomaly_summary:\n lbl = d.get(\"label\") or \"\"\n if \"72h\" in lbl or \"近期\" in lbl or \"异常\" in lbl.lower():\n cnt = d.get(\"count\") or 0\n if cnt > recent_n:\n recent_n = cnt\n # \"truncated\" flag in label or count exactly hitting cap.\n if \"truncated\" in lbl.lower() or cnt >= 100:\n recent_truncated = True\n m[\"anomaly_72h_count\"] = recent_n\n m[\"anomaly_72h_truncated\"] = recent_truncated\n\n # Cross-sym\n m[\"cross_sym_count\"] = _len(skel, \"cross_sym\", \"whales\")\n\n # Funding attribution — mint authority\n # v0.7.25.5: skeleton lacks `current_balance_tokens`. Closest is\n # `mint_pct_supply` (lifetime mint% of circ supply), capped at 100%\n # to neutralize inflationary mints (h's 0x6aa22cb8 = 1325%).\n auth_obj = _g(skel, \"funding_attribution\", \"mint_authorities\", default={}) or {}\n auth = auth_obj.get(\"authorities\") or []\n auth_active = [a for a in auth if not a.get(\"is_excluded\", False)]\n m[\"mint_authority_count\"] = len(auth_active)\n auth_max_pct = max((a.get(\"mint_pct_supply\") or 0 for a in auth_active), default=0)\n auth_sum_pct = sum((a.get(\"mint_pct_supply\") or 0) for a in auth_active)\n m[\"mint_pct_supply_max\"] = min(auth_max_pct, 100.0)\n m[\"mint_pct_supply_sum\"] = min(auth_sum_pct, 100.0)\n\n # Funding attribution — high throughput\n htd = _g(skel, \"funding_attribution\", \"high_throughput_dumpers\", \"dumpers\", default=[]) or []\n htd_active = [h for h in htd if not h.get(\"is_excluded\", False)]\n m[\"high_throughput_count\"] = len(htd_active)\n htd_total_in = sum((h.get(\"total_in\") or 0) for h in htd_active)\n m[\"high_throughput_total_in_pct_supply\"] = (\n htd_total_in / m[\"total_supply\"] * 100 if m[\"total_supply\"] else 0.0\n )\n\n # Funding attribution — CEX fan-out\n #\n # v0.8.1: use net_structured_fanout (Phase 2 recipient-detail\n # derived, ≥ min_per_recipient gate matching Phase 1 hub-shape\n # semantic) instead of broken total_out_tokens (Phase 1 SQL-layer\n # inconsistent, 13x inflated on VELVET).\n #\n # audit FIXES applied:\n # - HIGH: refuse to fall back to broken total_out_tokens (gross)\n # when net is missing — emit `fanout_metric_stale=True` so the\n # classifier downstream can mark A2 size claim as unavailable.\n # - CRITICAL: respect _phase2_complete flag from\n # discover_cex_fanout_hubs — when False, Phase 2 SQL truncated\n # and the net metric is an undercount.\n # - HIGH: use net_structured_recipients for recipient count (not\n # Phase 1 gross n_recipients which includes loopbacks).\n hubs = _g(skel, \"funding_attribution\", \"cex_fanout_hubs\", \"hubs\", default=[]) or []\n fanout_summary = _g(skel, \"funding_attribution\", \"cex_fanout_hubs\", \"summary\",\n default={}) or {}\n m[\"fanout_hub_count\"] = len(hubs)\n\n # Net-structured tokens preferred. Falls back to net (no threshold)\n # if structured missing. Refuses gross. Marks stale if neither net.\n if \"net_structured_fanout_tokens_total\" in fanout_summary:\n fanout_out_tokens = float(\n fanout_summary.get(\"net_structured_fanout_tokens_total\") or 0.0\n )\n m[\"fanout_metric_stale\"] = False\n m[\"fanout_metric_source\"] = \"net_structured\"\n elif \"net_fanout_tokens_total\" in fanout_summary:\n fanout_out_tokens = float(fanout_summary.get(\"net_fanout_tokens_total\") or 0.0)\n m[\"fanout_metric_stale\"] = False\n m[\"fanout_metric_source\"] = \"net\"\n elif hubs and \"net_structured_fanout_tokens\" in (hubs[0] or {}):\n fanout_out_tokens = sum(\n (h.get(\"net_structured_fanout_tokens\") or 0) for h in hubs\n )\n m[\"fanout_metric_stale\"] = False\n m[\"fanout_metric_source\"] = \"net_structured\"\n elif hubs and \"net_fanout_tokens\" in (hubs[0] or {}):\n fanout_out_tokens = sum((h.get(\"net_fanout_tokens\") or 0) for h in hubs)\n m[\"fanout_metric_stale\"] = False\n m[\"fanout_metric_source\"] = \"net\"\n else:\n # Legacy skeleton (pre-v0.8.1) lacks net metrics.\n # DO NOT silently fall back to total_out_tokens — that is the\n # broken gross that triggered the VELVET incident. Mark stale.\n fanout_out_tokens = 0.0\n m[\"fanout_metric_stale\"] = True\n m[\"fanout_metric_source\"] = \"stale_legacy_skeleton\"\n\n # Truncation guard: even if net metrics exist, if Phase 2 was\n # truncated we have an undercount. Mark stale so A2 can downgrade.\n phase2_complete = fanout_summary.get(\"_phase2_complete\")\n if phase2_complete is False:\n m[\"fanout_metric_stale\"] = True\n m[\"fanout_metric_source\"] = \"phase2_truncated\"\n\n # Recipient count: prefer net-structured-unique > net-unique >\n # Phase 1 gross sum.\n if \"net_structured_unique_recipients\" in fanout_summary:\n fanout_recipients = int(\n fanout_summary.get(\"net_structured_unique_recipients\") or 0\n )\n elif \"net_fanout_unique_recipients\" in fanout_summary:\n fanout_recipients = int(fanout_summary.get(\"net_fanout_unique_recipients\") or 0)\n else:\n fanout_recipients = sum((h.get(\"n_recipients\") or 0) for h in hubs)\n\n m[\"fanout_recipients_total\"] = fanout_recipients\n m[\"fanout_total_pct_supply\"] = (\n fanout_out_tokens / m[\"total_supply\"] * 100 if m[\"total_supply\"] else 0.0\n )\n # v0.8.1.2: also expose 占当前流通 % — A2 narrative uses this as\n # primary because circulating supply is what users actually trade\n # against (locked vesting / treasury stock is not in market).\n m[\"fanout_total_pct_circ\"] = (\n fanout_out_tokens / m[\"circ_supply\"] * 100 if m[\"circ_supply\"] else None\n )\n\n # Multi-chain\n platforms = _g(skel, \"meta\", \"coingecko_platforms\", default={}) or {}\n m[\"coingecko_chain_count\"] = len(platforms)\n mc = _g(skel, \"funding_attribution\", \"multi_chain\", default={}) or {}\n non_primary_chains = [k for k in mc.keys() if not k.startswith(\"_\")]\n m[\"non_primary_chain_count\"] = len(non_primary_chains)\n\n # B2 fake liquidity ratios\n if m[\"vol_24h_usd\"] and m[\"lp_usd\"] and m[\"lp_usd\"] > 0:\n m[\"vol_lp_ratio\"] = m[\"vol_24h_usd\"] / m[\"lp_usd\"]\n else:\n m[\"vol_lp_ratio\"] = None\n if m[\"lp_usd\"] and m[\"mcap_usd\"] and m[\"mcap_usd\"] > 0:\n m[\"lp_mcap_ratio\"] = m[\"lp_usd\"] / m[\"mcap_usd\"]\n else:\n m[\"lp_mcap_ratio\"] = None\n\n return m\n\n\ndef _label(severity: str, category: str, trigger_metrics: dict, human_summary: str) -> dict:\n return {\n \"severity\": severity,\n \"category\": category,\n \"trigger_metrics\": trigger_metrics,\n \"human_summary_zh\": human_summary,\n }\n\n\ndef classify(m: dict) -> dict:\n \"\"\"Apply calibrated triggers. Returns per-label severity + metric\n snapshot + 1-sentence summary.\"\"\"\n out: dict[str, dict] = {}\n\n # ------------------------------------------------------------------\n # A1 ACCUMULATION_IDLE\n # v0.7.25.5 CALIBRATION DELTA: dropped ChatGPT's `anomaly_72h \u003c 10`\n # precondition. Empirical: all wash-dominated hold-tokens also have\n # anomaly ≥ 10 → A1+C3 multi-label co-occurrence is normal, not mutex.\n #\n # v0.7.26.1 adversarial review CRITICAL fix: A1 must use ONLY pure_insider_pct\n # (current balance from dump_tracker), NOT mint_pct_supply (which is\n # lifetime mint — a mint authority that minted 100% supply but then\n # distributed it all is NOT \"still holding\"). Mint authority supply-\n # source semantics belong to A3, not A1.\n # ------------------------------------------------------------------\n key_holder_pct = m[\"pure_insider_pct\"]\n a1_trigger = (\n key_holder_pct >= 2.0\n and m[\"sell_pct_circ\"] \u003c= 1.0\n )\n if a1_trigger and key_holder_pct >= 5.0:\n sev = \"STRONG\"\n elif a1_trigger and key_holder_pct >= 2.0:\n sev = \"MEDIUM\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n # v0.7.26.1 adversarial review HIGH fix: rephrase to avoid banned words\n # (\"持有\"/\"卖出\"). Use 链上侦测-neutral terminology: \"余额\" for\n # current 余额, \"确认流出\" for confirmed sells. Also handles\n # MED finding: A1+C1 co-fire narrative is now consistent\n # (\"集中余额未流出\" describes the state regardless of small C1\n # confirmed dumps).\n out[\"A1\"] = _label(\n sev, \"A\",\n {\"pure_insider_pct\": round(key_holder_pct, 2),\n \"sell_pct_circ\": round(m[\"sell_pct_circ\"], 3)},\n t(\"behavior.summary.A1\", key_holder_pct=key_holder_pct)\n )\n else:\n out[\"A1\"] = _label(\"OFF\", \"A\", {}, \"\")\n\n # ------------------------------------------------------------------\n # A2 FANOUT_CONTROL\n #\n # v0.8.1 audit fix: when fanout_metric_stale=True (legacy\n # skeleton without v0.8.1 net metrics, OR Phase 2 SQL truncated),\n # the size claim is unreliable. Pattern detection (hubs >= 3) is\n # still valid (it doesn't depend on the broken total_out). So we\n # downgrade STRONG → MEDIUM when stale and suppress the % number\n # from the human summary, replacing with a \"未刷新 / 截断\" caveat.\n # ------------------------------------------------------------------\n hubs = m[\"fanout_hub_count\"]\n fout = m[\"fanout_total_pct_supply\"]\n rcpts = m[\"fanout_recipients_total\"]\n stale = bool(m.get(\"fanout_metric_stale\"))\n metric_source = m.get(\"fanout_metric_source\") or \"unknown\"\n if hubs >= 3 or fout >= 2.0 or rcpts >= 30:\n sev = \"STRONG\"\n elif hubs >= 1 or fout >= 0.5:\n sev = \"MEDIUM\"\n else:\n sev = \"OFF\"\n if stale and sev == \"STRONG\":\n # Pattern can still be STRONG by hub count alone, but without a\n # trusted size we downgrade to MEDIUM and mark the metric.\n sev = \"MEDIUM\"\n if sev != \"OFF\":\n if stale:\n summary_zh = t(\"behavior.summary.A2_stale\",\n hubs=hubs, metric_source=metric_source)\n else:\n fout_circ = m.get(\"fanout_total_pct_circ\")\n if fout_circ is not None:\n summary_zh = t(\"behavior.summary.A2_circ\",\n hubs=hubs, rcpts=rcpts,\n fout_circ=fout_circ, fout=fout)\n else:\n summary_zh = t(\"behavior.summary.A2_supply\",\n hubs=hubs, rcpts=rcpts, fout=fout)\n out[\"A2\"] = _label(\n sev, \"A\",\n {\"fanout_hubs\": hubs, \"fanout_recipients\": rcpts,\n \"fanout_pct_supply\": round(fout, 2),\n \"fanout_metric_stale\": stale,\n \"fanout_metric_source\": metric_source},\n summary_zh,\n )\n else:\n out[\"A2\"] = _label(\"OFF\", \"A\", {}, \"\")\n\n # ------------------------------------------------------------------\n # A3 MINT_SUPPLY_SOURCE\n # v0.7.25.5: ChatGPT's \"balance_pct\" doesn't exist on skeleton.\n # Using mint_pct_supply (lifetime mint%, cap 100% for inflationary).\n # ------------------------------------------------------------------\n mint_max = m[\"mint_pct_supply_max\"]\n mint_sum = m[\"mint_pct_supply_sum\"]\n mint_count = m[\"mint_authority_count\"]\n if mint_max >= 5.0 or mint_sum >= 10.0:\n sev = \"STRONG\"\n elif mint_max >= 1.0 or mint_sum >= 1.0:\n sev = \"MEDIUM\"\n elif mint_count >= 1:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n out[\"A3\"] = _label(\n sev, \"A\",\n {\"mint_authorities\": mint_count,\n \"mint_pct_supply_max\": round(mint_max, 2),\n \"mint_pct_supply_sum\": round(mint_sum, 2)},\n t(\"behavior.summary.A3\", mint_count=mint_count, mint_sum=mint_sum)\n )\n else:\n out[\"A3\"] = _label(\"OFF\", \"A\", {}, \"\")\n\n # ------------------------------------------------------------------\n # B1 WASH_VOLUME — keep verbatim per calibration\n # ------------------------------------------------------------------\n swap = m[\"wash_swap_count\"]\n share = m[\"wash_top_bot_share\"]\n sellers = m[\"wash_n_dex_sellers\"]\n if swap >= 100_000 and share >= 0.05:\n sev = \"STRONG\"\n elif swap >= 10_000 and 0 \u003c sellers \u003c= 300:\n sev = \"MEDIUM\"\n elif m[\"wash_dominated\"]:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n # v0.7.26.1 adversarial review HIGH fix: \"卖家\"→\"参与方\"; \"接盘\"→\"成交承接\" to\n # avoid banned-word \"卖\" substring.\n out[\"B1\"] = _label(\n sev, \"B\",\n {\"wash_swap_count\": swap, \"wash_top_bot_share\": round(share, 3),\n \"wash_n_dex_addrs\": sellers},\n t(\"behavior.summary.B1\", swap=swap, sellers=sellers,\n share_pct=share * 100)\n )\n else:\n out[\"B1\"] = _label(\"OFF\", \"B\", {}, \"\")\n\n # ------------------------------------------------------------------\n # B2 FAKE_LIQUIDITY — keep verbatim (composite vol/LP + LP/mcap)\n # ------------------------------------------------------------------\n vol_lp = m[\"vol_lp_ratio\"]\n lp_mcap = m[\"lp_mcap_ratio\"]\n lp_usd = m[\"lp_usd\"]\n if (vol_lp is not None and vol_lp >= 20) or (lp_usd is not None and lp_usd \u003c 5_000 and lp_usd > 0):\n sev = \"STRONG\"\n elif (\n (vol_lp is not None and vol_lp >= 10)\n or (lp_usd is not None and lp_usd \u003c 20_000 and lp_usd > 0)\n or (lp_mcap is not None and lp_mcap \u003c 0.02)\n ):\n sev = \"MEDIUM\"\n elif vol_lp is not None and vol_lp >= 5:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n # v0.7.26.1 adversarial review HIGH fix: \"进出\"→\"成交\" to avoid banned word.\n out[\"B2\"] = _label(\n sev, \"B\",\n {\"vol_lp_ratio\": round(vol_lp, 2) if vol_lp else None,\n \"lp_mcap_ratio\": round(lp_mcap, 4) if lp_mcap else None,\n \"lp_usd\": lp_usd},\n t(\"behavior.summary.B2_ratio\", vol_lp=vol_lp, lp_mcap=lp_mcap)\n if vol_lp is not None and lp_mcap is not None\n else t(\"behavior.summary.B2_thin\", lp_usd=lp_usd)\n )\n else:\n out[\"B2\"] = _label(\"OFF\", \"B\", {}, \"\")\n\n # ------------------------------------------------------------------\n # C1 DIRECT_DUMP — keep verbatim\n # ------------------------------------------------------------------\n net_sell = m[\"net_sellout_usd\"]\n sell_pct = m[\"sell_pct_circ\"]\n if net_sell >= 100_000 or sell_pct >= 2.0:\n sev = \"STRONG\"\n elif net_sell >= 10_000 or sell_pct >= 0.2:\n sev = \"MEDIUM\"\n elif m[\"confirmed_cex_tokens\"] > 0 or m[\"confirmed_dex_tokens\"] > 0:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n # v0.7.26.1 adversarial review HIGH fix: \"确认卖出\"→\"链上确认流出 (CEX 充值 + DEX swap)\"\n # to avoid banned-word \"卖\" substring. 链上侦测-neutral.\n out[\"C1\"] = _label(\n sev, \"C\",\n {\"net_sellout_usd\": round(net_sell, 0),\n \"sell_pct_circ\": round(sell_pct, 3)},\n t(\"behavior.summary.C1\", net_sell=net_sell, sell_pct=sell_pct)\n )\n else:\n out[\"C1\"] = _label(\"OFF\", \"C\", {}, \"\")\n\n # ------------------------------------------------------------------\n # C2 HISTORICAL_OPERATOR_DUMP\n # v0.7.25.5 CALIBRATION DELTA: collapse 3-tier to STRONG/OFF binary.\n # Detector is exhaustive (count = 0 or ≥50); MEDIUM/WEAK never fire\n # in 8-token sample.\n # ------------------------------------------------------------------\n ht_count = m[\"high_throughput_count\"]\n ht_pct = m[\"high_throughput_total_in_pct_supply\"]\n if ht_count >= 50 or ht_pct >= 10.0:\n sev = \"STRONG\"\n elif ht_count >= 5 or ht_pct >= 1.0:\n # v0.7.25.5 noted MEDIUM/WEAK rarely fire in sample, but keep them\n # so that future tokens with mid-range detector hits aren't lost.\n sev = \"MEDIUM\" if ht_count >= 10 else \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n out[\"C2\"] = _label(\n sev, \"C\",\n {\"ht_operator_count\": ht_count,\n \"ht_throughput_pct_supply\": round(ht_pct, 2)},\n t(\"behavior.summary.C2\", ht_count=ht_count, ht_pct=ht_pct)\n )\n else:\n out[\"C2\"] = _label(\"OFF\", \"C\", {}, \"\")\n\n # ------------------------------------------------------------------\n # C3 RECENT_ANOMALY_TRANSFER\n # v0.7.25.5 CALIBRATION DELTA: STRONG also fires on detector\n # truncation (count cap = 100). Saturated signal indistinguishable\n # from real ≥50 — treat as STRONG.\n # ------------------------------------------------------------------\n a72 = m[\"anomaly_72h_count\"]\n trunc = m[\"anomaly_72h_truncated\"]\n if a72 >= 50 or trunc:\n sev = \"STRONG\"\n elif a72 >= 10:\n sev = \"MEDIUM\"\n elif a72 >= 5:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n trunc_note = t(\"behavior.summary.C3_trunc_note\") if trunc and a72 >= 100 else \"\"\n out[\"C3\"] = _label(\n sev, \"C\",\n {\"anomaly_72h_count\": a72, \"truncated\": trunc},\n t(\"behavior.summary.C3\", a72=a72, trunc_note=trunc_note)\n )\n else:\n out[\"C3\"] = _label(\"OFF\", \"C\", {}, \"\")\n\n # ------------------------------------------------------------------\n # D1 CROSS_ALPHA\n # v0.7.25.5 CALIBRATION DELTA: lower STRONG ≥5 → ≥4.\n # Sample max=3 means ≥5 never fires; flagged for v0.8 expanded\n # calibration. Per-whale active/inactive sub-type (D1A/B/C) deferred\n # to monitoring_ranker (v0.7.27).\n # ------------------------------------------------------------------\n cs = m[\"cross_sym_count\"]\n if cs >= 4:\n sev = \"STRONG\"\n elif cs >= 3:\n sev = \"MEDIUM\"\n elif cs >= 1:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n # v0.7.26.1 adversarial review HIGH fix: \"持有\"/\"持仓\"→\"余额命中\" to avoid banned\n # word. Forensic-neutral describes detected cross-token presence.\n out[\"D1\"] = _label(\n sev, \"D\",\n {\"cross_sym_whales\": cs},\n t(\"behavior.summary.D1\", cs=cs)\n )\n else:\n out[\"D1\"] = _label(\"OFF\", \"D\", {}, \"\")\n\n # ------------------------------------------------------------------\n # D2 MULTICHAIN_COORDINATION — keep verbatim\n # ------------------------------------------------------------------\n non_prim = m[\"non_primary_chain_count\"]\n cg = m[\"coingecko_chain_count\"]\n if non_prim >= 2:\n sev = \"STRONG\"\n elif non_prim >= 1:\n sev = \"MEDIUM\"\n elif cg >= 2:\n sev = \"WEAK\"\n else:\n sev = \"OFF\"\n if sev != \"OFF\":\n out[\"D2\"] = _label(\n sev, \"D\",\n {\"non_primary_chains\": non_prim, \"cg_chains\": cg},\n t(\"behavior.summary.D2\", cg=cg, non_prim=non_prim)\n )\n else:\n out[\"D2\"] = _label(\"OFF\", \"D\", {}, \"\")\n\n return out\n\n\ndef build_profile(skel: dict) -> dict:\n \"\"\"Top-level entry point. Compute metrics, classify, package into\n skeleton-injectable behavior_profile dict.\n \"\"\"\n m = derive_metrics(skel)\n by_label = classify(m)\n\n # active_labels: non-OFF, sorted by severity (STRONG first), then by\n # category/order for stable ties.\n active = []\n for lid in _BEHAVIOR_ORDER:\n if lid in by_label and by_label[lid][\"severity\"] != \"OFF\":\n active.append(lid)\n active.sort(\n key=lambda lid: (\n -_SEVERITY_RANK[by_label[lid][\"severity\"]],\n _BEHAVIOR_ORDER.index(lid),\n )\n )\n primary = active[0] if active else None\n\n return {\n \"_schema_version\": \"v0.7.26\",\n # v0.7.26 self-audit: chain-of-custody marker so downstream\n # consumers (monitoring_ranker v0.7.27, future Context A\n # promotion) can detect calibration scope mismatch.\n \"_calibrated_for\": \"context_B_crypto_operator\",\n \"_calibration_sample_size\": 8,\n \"_calibration_doc\": \"v0725_5_CALIBRATION.md\",\n \"active_labels\": active,\n \"primary_behavior\": primary,\n \"by_label\": by_label,\n \"category_order\": [\"A\", \"B\", \"C\", \"D\"],\n \"category_names_zh\": _category_names_zh(),\n \"label_names_zh\": _label_names_zh(),\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27935,"content_sha256":"03728e1a109988a90846f07a0b0fc9ed0189a156d56768362fcb77cf1f47abc6"},{"filename":"alpha/v06/helpers/bscscan_label_probe.py","content":"#!/usr/bin/env python3\n\"\"\"bscscan_label_probe.py — BscScan public-label resolver for BSC addresses.\n\nDesigned to catch shared-pool / DEX-router / CEX-infrastructure addresses\nthat Surf/Arkham don't label but BscScan publicly tags. This is a\ncritical anti-pollution layer for cross_sym detection — without it,\nevery Alpha-listed token will see Binance Alpha 2.0 Router Proxy\n(0x73d8bd54f7cf5fab43fe4ef40a62d390644946db) as a \"cross-sym whale\"\nbecause it routes trades for ~all Alpha tokens.\n\nUsage:\n from bscscan_label_probe import resolve_labels, classify_label\n\n labels = resolve_labels([\n \"0x73d8bd54f7cf5fab43fe4ef40a62d390644946db\",\n \"0x...\",\n ])\n # labels = {addr: {\"label\": \"Binance: Alpha 2.0 Router Proxy\",\n # \"classification\": \"SHARED_POOL_CUSTODIAL\"}}\n\nCache:\n File at ~/.binance-alpha-data/bscscan_labels.json\n TTL 30 days. BscScan public labels change rarely.\n\nFailure mode:\n On scrape error / Cloudflare block / timeout, returns\n classification=\"UNKNOWN\" for the address. Caller decides whether\n to fail-closed (exclude unknown) or fail-open (keep unknown).\n For cross_sym: fail-OPEN (keep) since most unlabeled addresses\n are real wallets, not infrastructure.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport sys\nimport time\nimport urllib.request\nfrom concurrent.futures import ThreadPoolExecutor\nfrom pathlib import Path\nfrom typing import Iterable\n\n# 30-day cache; BscScan labels rarely change once assigned.\nCACHE_TTL_SECS = 30 * 86400\nDEFAULT_CACHE_PATH = os.environ.get(\n \"BINANCE_ALPHA_LABEL_CACHE\",\n os.path.expanduser(\"~/.binance-alpha-data/bscscan_labels.json\"),\n)\n\n# Address-classification regexes — applied to BscScan title text.\n# Order matters: more specific patterns first.\n\n# Curated lookalike for \"Binance: Alpha 2.0 Router Proxy\" — explicit so we\n# don't rely on the generic CEX_GENERIC fallback to catch this one critical\n# pattern (which polluted every Alpha cross_sym result before this helper).\nDEX_ROUTER_RE = re.compile(\n r\"^(Binance:\\s*Alpha|PancakeSwap.*(Router|Vault|Universal)|\"\n r\"Universal Router|1inch|0x(?:\\s|:)|Paraswap|OpenOcean|Beethoven|\"\n r\"Curve|Balancer|Uniswap.*Router|SushiSwap|Thena|Wombat|Mu\\sFinance|\"\n r\"Biswap.*Router|ApeSwap.*Router|MDEX|BabySwap)\\b\",\n re.IGNORECASE,\n)\n\nPRIVACY_AGGREGATOR_RE = re.compile(\n r\"^(ChangeNOW|FixedFloat|SimpleSwap|Sideshift|ThorSwap|\"\n r\"Tornado\\s*Cash|Railgun|Cyclone|Aztec|Privacy\\s*Pool)\\b\",\n re.IGNORECASE,\n)\n\nCROSS_CHAIN_BRIDGE_RE = re.compile(\n r\"^(deBridge|Stargate|cBridge|LayerZero|Wormhole|Across|Squid|\"\n r\"Multichain|Synapse|Hop|Connext|Symbiosis|XY\\s*Finance|Rubic|\"\n r\"Bridgers|Allbridge|Celer|Polyhedra|Orbiter)\\b\",\n re.IGNORECASE,\n)\n\nCEX_INFRA_KW = re.compile(\n r\"\\b(Hot Wallet|Cold Wallet|Deposit Funder|Deposit Wallet|\"\n r\"Withdrawal(?:s)?|Reserve|Treasury|MultiSender|Distributor|\"\n r\"Custody|Wallet\\s*\\d+)\\b\",\n re.IGNORECASE,\n)\n\nCEX_NAME_RE = re.compile(\n r\"^(Binance|HTX|OKX|Gate|Bybit|MEXC|KuCoin|Bitget|Coinbase|\"\n r\"Kraken|Upbit|Bitfinex|Crypto\\.com|Bitstamp|Bithumb|Bitkub|\"\n r\"Aster\\b)\",\n re.IGNORECASE,\n)\n\nENS_NAMED_RE = re.compile(\n r\"^[a-z0-9_-]+\\.(eth|bnb|crypto|nft|dao|wallet|x|polygon|sol)\\s*$\",\n re.IGNORECASE,\n)\n\n\ndef classify_label(label: str | None) -> str:\n \"\"\"Map BscScan label text to one of:\n\n - SHARED_POOL_CUSTODIAL — drop from cross_sym (CEX/router/bridge)\n - PRIVACY_AGGREGATOR — keep with anti_forensic flag\n - CROSS_CHAIN_BRIDGE — keep with caveat\n - DEX_ROUTER — drop from cross_sym\n - ENS_NAMED — keep (highest signal)\n - UNKNOWN_LABELED — has label but no classifier match\n - UNLABELED — no BscScan label\n - UNKNOWN — fetch failed\n \"\"\"\n if label is None:\n return \"UNLABELED\"\n if not isinstance(label, str) or not label.strip():\n return \"UNLABELED\"\n if ENS_NAMED_RE.match(label):\n return \"ENS_NAMED\"\n if PRIVACY_AGGREGATOR_RE.match(label):\n return \"PRIVACY_AGGREGATOR\"\n if CROSS_CHAIN_BRIDGE_RE.match(label):\n return \"CROSS_CHAIN_BRIDGE\"\n if DEX_ROUTER_RE.match(label):\n return \"DEX_ROUTER\"\n if CEX_INFRA_KW.search(label) or CEX_NAME_RE.match(label):\n return \"SHARED_POOL_CUSTODIAL\"\n return \"UNKNOWN_LABELED\"\n\n\n# Set of classifications that should be EXCLUDED from cross_sym whales —\n# they are infrastructure, not operator wallets.\nEXCLUDE_FROM_CROSS_SYM = frozenset({\n \"SHARED_POOL_CUSTODIAL\",\n \"DEX_ROUTER\",\n})\n\n\ndef _load_cache(path: str) -> dict:\n if not os.path.exists(path):\n return {}\n try:\n return json.load(open(path))\n except Exception:\n return {}\n\n\ndef _save_cache(cache: dict, path: str) -> None:\n os.makedirs(os.path.dirname(path), exist_ok=True)\n tmp = path + \".tmp\"\n with open(tmp, \"w\") as fp:\n json.dump(cache, fp, indent=2)\n os.replace(tmp, path)\n\n\n_TITLE_RE = re.compile(r\"\u003ctitle>([^\u003c]+)\u003c/title>\")\n\n\ndef _fetch_one(addr: str) -> tuple[str, str | None, str]:\n \"\"\"Scrape BscScan for one address. Returns (addr, label, source).\"\"\"\n try:\n req = urllib.request.Request(\n f\"https://bscscan.com/address/{addr}\",\n headers={\n \"User-Agent\": (\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X) \"\n \"AppleWebKit/537.36\"\n ),\n },\n )\n body = urllib.request.urlopen(req, timeout=12).read().decode(\n \"utf-8\", errors=\"ignore\"\n )\n except Exception as e:\n return (addr, None, f\"failed:{e}\")\n # Prefer the explicit \"Public Name Tag\" if present — more reliable than\n # title which sometimes shows the contract name.\n pnt_match = re.search(\n r\"Public Name Tag[^\u003c]*\u003cbr\\s*/?>\\s*([^\u003c']+?)'>\", body\n )\n if pnt_match:\n return (addr, pnt_match.group(1).strip(), \"scrape:pnt\")\n # Fallback to \u003ctitle>X | Address: 0x... | BscScan\u003c/title>.\n title_match = _TITLE_RE.search(body)\n if title_match:\n title = title_match.group(1).strip()\n parts = title.split(\" | \")\n if parts and not parts[0].startswith(\"Address:\"):\n return (addr, parts[0].strip(), \"scrape:title\")\n return (addr, None, \"scrape:no_label\")\n\n\ndef resolve_labels(\n addrs: Iterable[str],\n cache_path: str = DEFAULT_CACHE_PATH,\n workers: int = 8,\n verbose: bool = False,\n) -> dict[str, dict]:\n \"\"\"Resolve a batch of addresses to {label, classification}.\n\n Returns {addr_lower: {\"label\": str|None, \"classification\": str,\n \"fetched_at\": int, \"source\": str}}.\n\n Hits cache first (30-day TTL); scrapes BscScan in parallel for misses.\n \"\"\"\n addrs_lower = [a.lower() for a in addrs if a]\n if not addrs_lower:\n return {}\n\n cache = _load_cache(cache_path)\n now = int(time.time())\n out: dict[str, dict] = {}\n to_fetch: list[str] = []\n\n for a in addrs_lower:\n rec = cache.get(a)\n if rec and (now - rec.get(\"fetched_at\", 0)) \u003c CACHE_TTL_SECS:\n # Recompute classification in case classifier rules changed\n cls = classify_label(rec.get(\"label\"))\n out[a] = {\n \"label\": rec.get(\"label\"),\n \"classification\": cls,\n \"source\": \"cache\",\n \"fetched_at\": rec.get(\"fetched_at\"),\n }\n else:\n to_fetch.append(a)\n\n if to_fetch:\n if verbose:\n print(\n f\"[bscscan_label_probe] fetching {len(to_fetch)} addresses \"\n f\"({len(addrs_lower) - len(to_fetch)} cache hits)\",\n file=sys.stderr,\n )\n with ThreadPoolExecutor(max_workers=min(workers, len(to_fetch))) as ex:\n for addr, label, source in ex.map(_fetch_one, to_fetch):\n cls = classify_label(label)\n if source.startswith(\"failed\"):\n cls = \"UNKNOWN\"\n out[addr] = {\n \"label\": label,\n \"classification\": cls,\n \"source\": source,\n \"fetched_at\": now,\n }\n # Persist to cache (only successful fetches; failures retried\n # next run instead of caching the failure for 30 days)\n if not source.startswith(\"failed\"):\n cache[addr] = {\n \"label\": label,\n \"fetched_at\": now,\n }\n _save_cache(cache, cache_path)\n\n return out\n\n\ndef find_excluded(\n addrs: Iterable[str],\n cache_path: str = DEFAULT_CACHE_PATH,\n verbose: bool = False,\n) -> set[str]:\n \"\"\"Return the subset of `addrs` whose BscScan label classifies as\n SHARED_POOL_CUSTODIAL or DEX_ROUTER. Used by section_cross_sym to\n filter candidate whales before they reach the report.\n\n Fail-OPEN: UNKNOWN / UNLABELED / cache miss errors are NOT excluded.\n This means a transient BscScan outage degrades to noisier cross_sym\n results, not to data loss.\n \"\"\"\n labels = resolve_labels(addrs, cache_path=cache_path, verbose=verbose)\n return {\n a for a, r in labels.items()\n if r[\"classification\"] in EXCLUDE_FROM_CROSS_SYM\n }\n\n\n# ============================================================\n# CLI for ad-hoc inspection / cache warm-up\n# ============================================================\n\ndef main() -> int:\n import argparse\n ap = argparse.ArgumentParser()\n ap.add_argument(\"addresses\", nargs=\"+\", help=\"Addresses to resolve\")\n ap.add_argument(\"--cache\", default=DEFAULT_CACHE_PATH)\n ap.add_argument(\"--verbose\", action=\"store_true\")\n args = ap.parse_args()\n labels = resolve_labels(\n args.addresses, cache_path=args.cache, verbose=args.verbose,\n )\n for addr, rec in labels.items():\n print(f\"{addr}\\t{rec['classification']}\\t{rec.get('label')}\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10079,"content_sha256":"02a391faa33def0ff68d3c05fb4acfae1296125fd31fdc2a8c4e1142bf1f386f"},{"filename":"alpha/v06/helpers/chain_router.py","content":"\"\"\"chain_router.py — v0.7.20 cross-chain SQL table router.\n\nBackground\n==========\n\nforensic_pipeline.py and every helper (rule_11, dump_tracker, wash_infra,\nsection_a, section_anomaly_72h, section_liq, section_tge, role_classifier,\nsection_wash_infra) hardcoded `agent.bsc_transfers` and `agent.bsc_dex_trades`\nin their SQL strings, dating to v0.6 when the skill was BSC-only. v0.7.20\nadds a chain-routing layer so the same pipeline can run against any of\nsurf's 7 EVM-compatible chains plus Solana.\n\nSurf's data coverage (re-verified 2026-06-02 after v0.7.21.7 FARTCOIN\nship surfaced a coverage gap I had previously documented wrong):\n\n ✅ BSC agent.bsc_transfers / agent.bsc_dex_trades\n ✅ Ethereum agent.ethereum_transfers / agent.ethereum_dex_trades\n ✅ Arbitrum agent.arbitrum_transfers / agent.arbitrum_dex_trades\n ✅ Base agent.base_transfers / agent.base_dex_trades\n ✅ Polygon agent.polygon_transfers / agent.polygon_dex_trades\n ✅ Optimism agent.optimism_transfers / agent.optimism_dex_trades\n ⚠️ Solana **NOT covered** by surf's onchain-sql (no\n `agent.solana_*` table exists; `surf onchain-schema`\n lists `arbitrum/base/bsc/ethereum/tempo` as the\n SQL-supported chains as of 2026-06-02).\n\nFor Solana tokens we can still hit:\n - `surf token-holders --chain solana --address \u003cCA> --include labels`\n (REST endpoint, returns top-N holders with Arkham labels)\n - `surf project-detail` for realtime price / vol / mcap\n\n…but the SQL-driven detectors (rule_11 mint trace, dump_tracker\ntop-seller table, wash_infra_detector closed-loop SQL, flow_operator\ncross-Alpha SQL, section_anomaly_72h transfer SQL, section_liq pool\nSQL, section_tge LP-first-trade SQL, section_l_distribution dumper-\ndestination SQL, cross_sym registry SQL) all require those missing\nSolana tables.\n\nv0.7.21.8 therefore routes Solana through a HOLDER_SNAPSHOT mode:\nSQL-only sections short-circuit with a `_skip_reason=\"surf_no_sql_solana\"`\nstatus; section_f_holders (REST), section_alloc (Alpha API), and the\nmonitoring-paste export still run. Pre-v0.7.21.8 those SQL sections\nsilently returned empty rows and the report shipped looking like a\nforensic clean bill of health, which is the misleading failure mode\nthe user caught when FARTCOIN's report showed 0 m6 / 0 cross-sym\nwhales / 0 wash setups despite being a $151M MC live token.\n\nHistory note: v0.7.20's \"surf supports Solana SQL\" line came from me\nmisreading a `surf onchain-sql` error message — when a Solana table\nidentifier was passed in the SQL parser ran its missing-`block_date`\npre-check and echoed the table name back in the error string, which I\ninterpreted as \"table exists\". A subsequent probe with a real\nblock_date filter returned a UNKNOWN_TABLE error explicitly listing\nthe supported chains (no Solana). v0.7.21.7's pipeline ran end-to-end\non FARTCOIN but every SQL detector hit the same UNKNOWN_TABLE error\nand degraded to empty rows, producing the misleading \"looks ok\"\nskeleton. v0.7.21.8 fail-loud-but-graceful on that gap.\n\nUsage pattern\n=============\n\n`forensic_pipeline.py` calls `set_active_chain(chain_id)` immediately\nafter `section_a_scope` returns the scope dict (which contains the\nAlpha API's `chainId`). Every downstream helper module reads the active\nchain via `transfers_table()` / `dex_trades_table()` instead of hard-\ncoding the BSC table name.\n\nDefaults to `bsc` if `set_active_chain` was never called, preserving\nbehavior for any out-of-pipeline callers that import a helper directly\n(e.g. CLI smoke tests).\n\nThread-safety\n=============\n\n`forensic_pipeline.py` is a single-shot CLI. The active chain is set\nexactly once in the main thread immediately after Section A returns the\nAlpha-API `chainId`, and is read by all downstream helpers — including\nhelpers that fan out via `ThreadPoolExecutor` inside `wash_infra_detector`\n/ `dump_tracker` / `rule_11_backward_trace`.\n\nWe deliberately use a **plain module-level string** rather than\n`ContextVar` or `threading.local`. Empirical test (verified during\nv0.7.20 development): a `ContextVar` with a `default=` value is NOT\ninherited by `ThreadPoolExecutor` workers — they see the default, not\nthe value set in the main thread. That would silently route SQL in\nparallel sections to BSC even when the main thread set Base, exactly\nthe v0.7.19.x mis-routing bug we're fixing. `threading.local` has the\nsame problem (workers get a fresh local).\n\nModule-level string works because Python's GIL gives every thread the\nsame view of module globals. The \"global mutable adversarial review flag\" cost is\nworth paying here — the alternative is a silent cross-thread routing\nbug.\n\nUse the `chain_lock()` context manager (or `set_active_chain` + explicit\nreset in tests) when you need to temporarily route SQL to a different\nchain.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport warnings\nfrom contextlib import contextmanager\nfrom typing import Final, Iterator\n\n\n# chain_id (int) → table prefix used by surf's `agent.{prefix}_transfers`\n# and `agent.{prefix}_dex_trades`. Solana is non-EVM and Alpha API\n# encodes it with a sentinel chain_id; we currently see -1 / None on\n# some API responses, so we also accept the string token \"solana\" via\n# `set_active_chain(\"solana\")`.\n#\n# Sources for chain_id mapping (EIP-155):\n# 1 = Ethereum mainnet\n# 10 = Optimism\n# 56 = BNB Smart Chain (BSC)\n# 137 = Polygon PoS\n# 8453 = Base\n# 42161 = Arbitrum One\n_CHAIN_ID_TO_PREFIX: Final[dict[int, str]] = {\n 1: \"ethereum\",\n 10: \"optimism\",\n 56: \"bsc\",\n 137: \"polygon\",\n 8453: \"base\",\n 42161: \"arbitrum\",\n}\n\n# v0.7.21.7: Alpha API uses non-numeric chain identifiers for non-EVM\n# chains. Solana is reported as \"CT_501\" (the prefix \"CT_\" plus the\n# SLIP-44 coin type 501 for Solana). Map those explicitly so\n# `set_active_chain(\"CT_501\")` from forensic_pipeline works without\n# the caller having to translate first.\n_NON_NUMERIC_CHAIN_ID_TO_PREFIX: Final[dict[str, str]] = {\n \"CT_501\": \"solana\",\n}\n\n# All chain prefixes the router accepts as direct strings (case-insensitive).\n_VALID_PREFIXES: Final[frozenset[str]] = frozenset({\n \"bsc\", \"ethereum\", \"optimism\", \"polygon\", \"base\", \"arbitrum\", \"solana\",\n})\n\n# Default to BSC so any direct-import call site without a pipeline\n# context (e.g. unit test importing rule_11_backward_trace.SQL_FIND_MINT)\n# gets the v0.6 - v0.7.19.5 behavior. Plain module-level string — see\n# Thread-safety section in module docstring for why this beats ContextVar.\n_active_chain: str = \"bsc\"\n\n\nclass UnsupportedChainError(ValueError):\n \"\"\"Raised when `set_active_chain` is called with an unmapped chain_id\n or an unrecognized chain name. We fail-loud rather than silently\n fall back to BSC because a silent fallback would re-create the\n PLAY-style mis-attribution bug (running a Base token against BSC\n tables and producing an empty-but-shipped forensic report).\"\"\"\n\n\ndef set_active_chain(chain: int | str) -> str:\n \"\"\"Set the active chain for downstream SQL helpers.\n\n Args:\n chain: an EIP-155 `chain_id` int (e.g. 8453 for Base) OR a\n chain prefix string (e.g. \"base\", \"ethereum\", \"solana\").\n Case-insensitive on strings.\n\n Returns:\n The resolved chain prefix (lowercase, e.g. \"base\").\n\n Raises:\n UnsupportedChainError: if `chain` is unmapped. Fail-loud is\n intentional — a silent fallback would let a non-BSC token\n run against BSC tables and produce a misleading \"empty\n forensic\" report (the v0.7.19.x PLAY bug we're fixing).\n \"\"\"\n global _active_chain # v0.7.21.7: single declaration covers all branches\n if isinstance(chain, str):\n raw_original = chain.strip()\n raw = raw_original.lower()\n if not raw:\n raise UnsupportedChainError(\n \"chain_router: empty chain identifier\"\n )\n # v0.7.21.7: Alpha API non-numeric chain IDs (currently just\n # \"CT_501\" for Solana). Match case-insensitively against the\n # mapping keys.\n for key, value in _NON_NUMERIC_CHAIN_ID_TO_PREFIX.items():\n if raw == key.lower():\n _active_chain = value\n if value == \"solana\":\n warnings.warn(\n \"chain_router: Solana routing enabled. Validators \"\n \"(Section A CA regex, rule_11/dump_tracker/wash_infra/\"\n \"flow_operators address checks) are chain-aware as of \"\n \"v0.7.21.7. Surf DEX-trades coverage on Solana lags \"\n \"BSC — expect fewer rows in trade-volume sections.\",\n stacklevel=2,\n )\n return value\n # Accept numeric chain_id encoded as string (\"8453\") — Alpha API\n # responses arrive at the pipeline as `str(entry.get(\"chainId\"))`\n # so by the time we reach the router the type has been lost.\n if raw.lstrip(\"-\").isdigit():\n try:\n chain_id = int(raw)\n except ValueError as e:\n raise UnsupportedChainError(\n f\"chain_router: invalid numeric chain identifier {chain!r}\"\n ) from e\n if chain_id not in _CHAIN_ID_TO_PREFIX:\n raise UnsupportedChainError(\n f\"chain_router: unmapped chain_id {chain_id}. \"\n f\"Supported chain_ids: {sorted(_CHAIN_ID_TO_PREFIX)} \"\n f\"(plus 'solana' via string). Add the mapping in \"\n f\"chain_router._CHAIN_ID_TO_PREFIX to onboard a new chain.\"\n )\n prefix = _CHAIN_ID_TO_PREFIX[chain_id]\n else:\n if raw not in _VALID_PREFIXES:\n raise UnsupportedChainError(\n f\"chain_router: unrecognized chain prefix {chain!r}. \"\n f\"Supported: {sorted(_VALID_PREFIXES)}\"\n )\n prefix = raw\n else:\n try:\n chain_id = int(chain)\n except (TypeError, ValueError) as e:\n raise UnsupportedChainError(\n f\"chain_router: invalid chain identifier {chain!r}\"\n ) from e\n if chain_id not in _CHAIN_ID_TO_PREFIX:\n raise UnsupportedChainError(\n f\"chain_router: unmapped chain_id {chain_id}. \"\n f\"Supported chain_ids: {sorted(_CHAIN_ID_TO_PREFIX)} \"\n f\"(plus 'solana' via string). Add the mapping in \"\n f\"chain_router._CHAIN_ID_TO_PREFIX to onboard a new chain.\"\n )\n prefix = _CHAIN_ID_TO_PREFIX[chain_id]\n if prefix == \"solana\":\n # v0.7.21.7: Solana validators are now chain-aware (Section A CA regex,\n # rule_11/dump_tracker/wash_infra/flow_operators/role_classifier all\n # delegate to chain_router.is_valid_addr). Warn instead about surf\n # DEX-trades coverage on Solana, which lags BSC and may yield fewer\n # rows in trade-volume sections.\n warnings.warn(\n \"chain_router: Solana routing enabled. Validators are chain-aware \"\n \"as of v0.7.21.7; surf DEX-trades coverage on Solana lags BSC.\",\n stacklevel=2,\n )\n _active_chain = prefix\n return prefix\n\n\ndef get_active_chain() -> str:\n \"\"\"Return the active chain prefix (lowercase). Defaults to 'bsc'.\"\"\"\n return _active_chain\n\n\ndef transfers_table() -> str:\n \"\"\"Return the surf SQL `agent.{prefix}_transfers` for the active chain.\n\n Use this in every SQL string that previously hardcoded\n `agent.bsc_transfers`. The substitution is a pure rename — the table\n schema is identical across chains (block_date / block_time / from /\n to / amount_raw / contract_address / tx_hash / tx_from columns).\n \"\"\"\n return f\"agent.{_active_chain}_transfers\"\n\n\ndef dex_trades_table() -> str:\n \"\"\"Return the surf SQL `agent.{prefix}_dex_trades` for the active chain.\n\n Use this in every SQL string that previously hardcoded\n `agent.bsc_dex_trades`. Schema identical across chains\n (block_date / block_time / token_sold_address / token_sold_amount /\n amount_usd / tx_from columns).\n \"\"\"\n return f\"agent.{_active_chain}_dex_trades\"\n\n\n# v0.9.7 fix #2 (FOLKS 6-decimal 2026-06-16): module-level token decimals.\n# Pre-v0.9.7 every SQL hardcoded `/1e18` assuming 18-decimal ERC20. FOLKS\n# is 6-decimal (Folks Finance was originally an Algorand project, 6-decimal\n# default carried to its multichain EVM mirrors). Every token-amount field\n# computed by the skill (m6 received, dump_tracker tokens, insider holdings,\n# anomaly amounts) was wrong by `10^(18-decimals)` factor — for FOLKS that\n# is 10^12. Ratios (dumped_pct, % supply) cancelled the error so verdict\n# logic survived, but raw token counts in the report were 1e12 too small.\n#\n# section_a sets this on every CA via the EVM `decimals()` RPC call; if\n# the RPC fails or the chain is Solana (uses SPL mint metadata path), we\n# fall back to the v0.9.6 default 18 so behavior never regresses.\n_token_decimals: int = 18\n\n\ndef set_token_decimals(d: int | None) -> None:\n \"\"\"Set the active token's `decimals` value. Called by section_a after\n successful RPC lookup. None / invalid input falls back to 18 (v0.9.6\n behavior — never regresses for the 99% case).\"\"\"\n global _token_decimals\n try:\n d_int = int(d) if d is not None else 18\n except (TypeError, ValueError):\n d_int = 18\n # Bound check: ERC20 spec allows 0-255 but realistic tokens use 6-18.\n # Reject obvious garbage so downstream SQL doesn't `/1e0` divide.\n if d_int \u003c 0 or d_int > 30:\n d_int = 18\n _token_decimals = d_int\n\n\ndef get_token_decimals() -> int:\n \"\"\"Return active token decimals (default 18).\"\"\"\n return _token_decimals\n\n\ndef decimals_factor() -> float:\n \"\"\"Return the divisor for raw amounts. SQL builders should use\n `decimals_factor_str()` instead — this is for Python-side math.\"\"\"\n return 10.0 ** _token_decimals\n\n\ndef decimals_factor_str() -> str:\n \"\"\"Return SQL-safe scientific notation for the active decimals factor.\n Defaults to `1e18` (v0.9.6 baseline) — exact string match so SQL plans\n remain identical for 18-decimal tokens. For FOLKS (6 decimals) returns\n `1e6`. Used in every SQL template's `{decimals_factor}` placeholder\n via `.format(decimals_factor=decimals_factor_str())`.\"\"\"\n return f\"1e{_token_decimals}\"\n\n\n@contextmanager\ndef chain_lock(chain: int | str) -> Iterator[str]:\n \"\"\"Temporarily set the active chain, then restore the prior value.\n\n Useful in tests / cross-chain ad-hoc scripts that want to flip the\n router for a single block without leaking state into other tests.\n \"\"\"\n global _active_chain\n prior = _active_chain\n try:\n yield set_active_chain(chain)\n finally:\n _active_chain = prior\n\n\ndef supported_chains() -> list[str]:\n \"\"\"List of chain prefixes the router accepts (for diagnostics).\"\"\"\n return sorted(_VALID_PREFIXES)\n\n\n# v0.7.21.8: surf onchain-sql coverage. Solana is intentionally absent —\n# surf has no `agent.solana_*` table (verified 2026-06-02 against\n# `surf onchain-schema`; the error path also lists supported chains\n# explicitly). Helpers that build SQL against `agent.{chain}_transfers`\n# / `agent.{chain}_dex_trades` should gate on this set before issuing\n# the query and short-circuit with `_skip_reason=\"surf_no_sql_solana\"`\n# (or similar) otherwise the surf call wastes a credit and returns an\n# empty rowset that downstream code mis-reads as \"0 hits\".\n_SQL_SUPPORTED_CHAINS: Final[frozenset[str]] = frozenset({\n \"bsc\", \"ethereum\", \"arbitrum\", \"base\", \"polygon\", \"optimism\",\n})\n\n\ndef sql_supported() -> bool:\n \"\"\"True when the active chain has `agent.{chain}_transfers/dex_trades`\n tables in surf's onchain-sql. False for chains where forensic helpers\n must skip SQL-based detection (currently: Solana).\n \"\"\"\n return _active_chain in _SQL_SUPPORTED_CHAINS\n\n\ndef requires_holder_snapshot() -> bool:\n \"\"\"True when the active chain has NO surf onchain-sql coverage and the\n forensic pipeline should run in HOLDER_SNAPSHOT mode (Alpha API\n realtime + surf token-holders REST + section_alloc only).\n Currently equivalent to `not sql_supported()`; kept as a separate\n name so callers can express intent (\"am I in holder-snapshot mode?\")\n rather than \"is SQL available?\".\n \"\"\"\n return not sql_supported()\n\n\n# v0.7.21.7: chain-aware address validation. Every helper that used to\n# hardcode `^0x[0-9a-f]{40} hertzflow — Skillopedia (rule_11_backward_trace, dump_tracker,\n# wash_infra_detector, flow_operator_detector, section_cross_sym, etc.)\n# now delegates to `is_valid_addr` so the same code path produces correct\n# checks on Solana base58 wallets too. Pre-v0.7.21.7 Solana CAs cleared\n# Section A but every downstream SQL helper rejected every base58 address\n# in their _valid_addr filters → m6/dump_tracker/wash/flow_operators all\n# returned empty for Solana tokens.\n_EVM_ADDR_RE: Final[re.Pattern] = re.compile(r\"^0x[0-9a-f]{40}$\")\n# Solana base58 — alphanumeric minus 0/O/I/l, 32-44 chars. Length 44 is\n# typical for pump.fun tokens / token mints; vanity-derived PDAs and some\n# program-owned accounts can be slightly shorter.\n_SOLANA_ADDR_RE: Final[re.Pattern] = re.compile(\n r\"^[1-9A-HJ-NP-Za-km-z]{32,44}$\"\n)\n\n\ndef is_valid_addr(addr) -> bool:\n \"\"\"Return True if `addr` is a syntactically valid wallet / contract\n address for the *currently active chain*.\n\n On EVM chains: matches `^0x[0-9a-f]{40} hertzflow — Skillopedia (case-insensitive — the\n helper lowercases before matching). On Solana: matches base58\n 32-44 chars (excludes 0/O/I/l per the Bitcoin base58 alphabet).\n\n Use this in every helper that filters addresses out of a SQL `IN`\n list or guards against SQL injection. Direct callers that only\n want one chain's regex should grab `addr_re_pattern()` instead.\n \"\"\"\n if not isinstance(addr, str):\n return False\n if _active_chain == \"solana\":\n return bool(_SOLANA_ADDR_RE.fullmatch(addr))\n return bool(_EVM_ADDR_RE.fullmatch(addr.lower()))\n\n\ndef addr_re_pattern() -> re.Pattern:\n \"\"\"Return the compiled address-format regex for the active chain.\n\n Helpers that need to embed the pattern in a larger regex (e.g.\n multi-address tokenisers) should call this rather than maintain\n their own copy of the EVM/Solana split.\n \"\"\"\n return _SOLANA_ADDR_RE if _active_chain == \"solana\" else _EVM_ADDR_RE\n\n\n# Solana burn / system addresses that section_f_holders should treat as\n# \"not a holder\" the way 0xdead / 0x0 are treated on EVM. Includes:\n# - 11111111111111111111111111111111 (System Program; common dust sink)\n# - 1nc1nerator11111111111111111111111111111111 (Solana Incinerator program)\n_SOLANA_BURN_ADDRS: Final[frozenset[str]] = frozenset({\n \"11111111111111111111111111111111\",\n \"1nc1nerator11111111111111111111111111111111\",\n})\n\n_EVM_BURN_ADDRS: Final[frozenset[str]] = frozenset({\n \"0x0000000000000000000000000000000000000000\",\n \"0x000000000000000000000000000000000000dead\",\n})\n\n\ndef burn_addrs() -> frozenset[str]:\n \"\"\"Return the set of burn / dust-sink addresses for the active chain.\n\n section_f_holders subtracts these from top_holders into a dedicated\n burn bucket (v0.7.20.2). The active chain decides which set applies.\n \"\"\"\n return _SOLANA_BURN_ADDRS if _active_chain == \"solana\" else _EVM_BURN_ADDRS\n\n\n# v0.7.20.1: explorer URL templates per chain. Pre-v0.7.20.1 render_report.py\n# hardcoded `bscscan.com/address/` in 6 places — a Base PLAY report rendered\n# every wallet link to BSCScan (where the wallet does not exist or has\n# unrelated activity), making the report useless for cross-checking\n# anything off the primary chain. v0.7.20.1 routes the explorer URL by\n# the active chain alongside the SQL routing fix.\n_EXPLORER_URL_TEMPLATES: Final[dict[str, str]] = {\n \"bsc\": \"https://bscscan.com/address/{addr}\",\n \"ethereum\": \"https://etherscan.io/address/{addr}\",\n \"base\": \"https://basescan.org/address/{addr}\",\n \"arbitrum\": \"https://arbiscan.io/address/{addr}\",\n \"polygon\": \"https://polygonscan.com/address/{addr}\",\n \"optimism\": \"https://optimistic.etherscan.io/address/{addr}\",\n \"solana\": \"https://solscan.io/account/{addr}\",\n}\n\n_EXPLORER_NAMES: Final[dict[str, str]] = {\n \"bsc\": \"BscScan\",\n \"ethereum\": \"Etherscan\",\n \"base\": \"BaseScan\",\n \"arbitrum\": \"Arbiscan\",\n \"polygon\": \"PolygonScan\",\n \"optimism\": \"Optimistic Etherscan\",\n \"solana\": \"Solscan\",\n}\n\n\ndef explorer_url(addr: str) -> str:\n \"\"\"Return the explorer URL for `addr` on the currently active chain.\n\n Use this in render_report.py templates instead of hardcoding\n `https://bscscan.com/address/...`. For Base / Arbitrum / Polygon /\n Optimism / Solana the returned URL points at the corresponding\n public block explorer.\n \"\"\"\n tpl = _EXPLORER_URL_TEMPLATES.get(_active_chain) or _EXPLORER_URL_TEMPLATES[\"bsc\"]\n return tpl.format(addr=addr)\n\n\ndef explorer_name() -> str:\n \"\"\"Return the explorer display name for the active chain\n (e.g. 'BaseScan' on Base). Used by i18n strings that reference the\n explorer brand name (e.g. 'View on BaseScan').\"\"\"\n return _EXPLORER_NAMES.get(_active_chain) or _EXPLORER_NAMES[\"bsc\"]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":21423,"content_sha256":"21855e62c799a7ff656f0e1d7a28c738fb7ad5ad65f2f3b51bab80b382afe5c7"},{"filename":"alpha/v06/helpers/cross_sym_detector.py","content":"#!/usr/bin/env python3\n\"\"\"cross_sym_detector.py — Forward-detection of cross-sym whale candidates.\n\nv0.7 core logic. Given a fresh CA's top-100 holders + the universal\ncross-sym registry, identifies wallets that are top-100 holders in ≥N\nOTHER Alpha tokens. These are the candidates for downstream identity\nclassification (KOL_MANAGER / ACTIVE_MM / ARB_DESK / OTC_DESK / UNKNOWN_WHALE).\n\nThis module is PURE FUNCTIONAL — no I/O, no Surf calls. All inputs are\npassed in (top_holders, excluded_addrs, registry). Section runner is\nresponsible for fetching those.\n\nFilters applied (in order):\n 1. Address excluded? (m6 / operator_relay / Arkham-labeled CEX/MM/bridge)\n 2. Position size in current token \u003c min_pct? (default 0.5%)\n 3. Cross-sym count in OTHER tokens \u003c min_cross_sym? (default 3)\n 4. Rank-cap (top N candidates by cross_sym_count, default 5)\n\nWhy these defaults:\n - min_pct=0.5%: smaller positions are unlikely to be meaningful\n operators (would be retail-scale even with cross-sym pattern)\n - min_cross_sym=3: 1-2 hits could be coincidence (same wallet\n happens to hold 2 random Alpha tokens); 3+ is a strong portfolio\n signal\n - max=5: limits downstream identity-classification SQL cost per\n report (each candidate = 1 unified SQL)\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\n\n# ----- Constants -----\n\nDEFAULT_MIN_PCT = 0.5 # candidate must hold ≥0.5% of current CA's supply\nDEFAULT_MIN_CROSS_SYM = 3 # appear in top-100 of ≥3 OTHER Alpha tokens\nDEFAULT_MAX_CANDIDATES = 5 # cap downstream cost\nDEFAULT_TOP_RANK_IN_OTHER = 100 # \"top-100\" in other tokens counts\n\n\n# ----- Public API -----\n\ndef detect(\n ca: str,\n top_holders: list[dict],\n excluded_addrs: set[str],\n registry: dict[str, Any],\n *,\n min_pct: float = DEFAULT_MIN_PCT,\n min_cross_sym: int = DEFAULT_MIN_CROSS_SYM,\n max_candidates: int = DEFAULT_MAX_CANDIDATES,\n top_rank_in_other: int = DEFAULT_TOP_RANK_IN_OTHER,\n) -> list[dict]:\n \"\"\"Detect cross-sym whale candidates for a fresh CA.\n\n Args:\n ca: current CA being analyzed (lowercase 0x address)\n top_holders: list of {address, balance, percentage, entity_name?, ...}\n from `surf token-holders`. Each holder is a dict.\n excluded_addrs: set of lowercase addresses to exclude from candidates\n (m6 receivers + operator_relay + Arkham-labeled).\n registry: output of cross_sym_registry.get_reverse_index().\n Must contain `reverse_index` dict.\n min_pct: minimum supply % in current CA (default 0.5%).\n min_cross_sym: minimum number of OTHER Alpha tokens the holder\n must appear in top-100 of (default 3).\n max_candidates: cap on returned candidates (default 5).\n top_rank_in_other: rank threshold within other tokens\n (default 100 = any top-100 appearance counts).\n\n Returns:\n Sorted list of candidate dicts, max length `max_candidates`.\n Each candidate is shaped:\n {\n \"address\": str (lowercase 0x),\n \"this_token_pct\": float,\n \"this_token_balance\": str (preserve precision),\n \"cross_sym_count\": int,\n \"cross_sym_tokens\": [\n {\"sym\": str, \"ca\": str, \"pct\": float, \"rank\": int}, ...\n ],\n \"top_cross_sym_token\": {\"sym\": str, \"pct\": float}, # by pct\n \"arkham_label\": str | None, # always None at this stage\n }\n\n Sort key: cross_sym_count DESC, then top_cross_sym_pct DESC.\n Cap: max_candidates.\n Empty list if no qualifying candidates.\n \"\"\"\n if not isinstance(registry, dict) or \"reverse_index\" not in registry:\n return []\n\n reverse_index = registry[\"reverse_index\"]\n ca_lower = ca.lower()\n\n # v0.7 shape normalization: accept either surf token-holders shape\n # (`address`, `percentage`, `entity_name`) OR section_f_holders shape\n # (`addr`, `pct_of_total`, no entity). Internally use `address` + `percentage`.\n def _h_addr(h):\n return (h.get(\"address\") or h.get(\"addr\") or \"\").lower()\n def _h_pct(h):\n v = h.get(\"percentage\") if h.get(\"percentage\") is not None else h.get(\"pct_of_total\")\n try:\n return float(v) if v is not None else 0\n except (ValueError, TypeError):\n return None # signal malformed\n\n # adversarial review P2a audit fix: dedupe top_holders by address. Keep entry with\n # highest pct if duplicates appear. Defends against malformed input.\n holders_by_addr: dict[str, dict] = {}\n for holder in top_holders:\n addr = _h_addr(holder)\n if not addr:\n continue\n pct = _h_pct(holder)\n if pct is None:\n continue # malformed pct, skip\n existing = holders_by_addr.get(addr)\n if existing is None or (_h_pct(existing) or 0) \u003c pct:\n holders_by_addr[addr] = holder\n\n candidates: list[dict] = []\n\n for addr, holder in holders_by_addr.items():\n if addr in excluded_addrs:\n continue\n pct = _h_pct(holder)\n if pct is None or pct \u003c min_pct:\n continue\n # Skip already-labeled entities (CEX hot, MM, bridge, etc.) — they're\n # not \"whales\" in the operator sense, they're infrastructure.\n # Only present in surf token-holders shape (entity_name field).\n if holder.get(\"entity_name\"):\n continue\n\n # Cross-sym lookup with defensive type-coerce.\n # adversarial review P2a audit fix: rank/pct may be non-int from corrupt registry.\n appearances = reverse_index.get(addr, [])\n other_appearances = []\n seen_cas: set[str] = set() # dedupe by ca (one entry per token)\n for a in appearances:\n ca_a = (a.get(\"ca\") or \"\").lower()\n if not ca_a or ca_a == ca_lower:\n continue\n if ca_a in seen_cas:\n continue # dedupe duplicate appearances of same ca\n try:\n rank_v = int(a.get(\"rank\") or 9999)\n except (ValueError, TypeError):\n continue\n if rank_v > top_rank_in_other:\n continue\n try:\n pct_v = float(a.get(\"pct\") or 0)\n except (ValueError, TypeError):\n pct_v = 0.0\n seen_cas.add(ca_a)\n other_appearances.append({\"_obj\": a, \"ca\": ca_a, \"rank\": rank_v, \"pct\": pct_v})\n\n if len(other_appearances) \u003c min_cross_sym:\n continue\n\n # Build candidate entry (using coerced rank/pct values from above)\n top_cross_sym = max(other_appearances, key=lambda a: a[\"pct\"])\n candidates.append({\n \"address\": addr,\n \"this_token_pct\": pct,\n \"this_token_balance\": str(holder.get(\"balance\") or holder.get(\"total_in\") or \"0\"),\n \"cross_sym_count\": len(other_appearances),\n \"cross_sym_tokens\": [\n {\n \"sym\": a[\"_obj\"].get(\"sym\"),\n \"ca\": a[\"ca\"],\n \"pct\": a[\"pct\"],\n \"rank\": a[\"rank\"],\n }\n for a in other_appearances\n ],\n \"top_cross_sym_token\": {\n \"sym\": top_cross_sym[\"_obj\"].get(\"sym\"),\n \"pct\": top_cross_sym[\"pct\"],\n },\n \"arkham_label\": None, # entity_name filter already excluded labeled\n })\n\n # Sort: cross_sym_count DESC, then top_cross_sym pct DESC\n candidates.sort(\n key=lambda c: (c[\"cross_sym_count\"], c[\"top_cross_sym_token\"][\"pct\"]),\n reverse=True,\n )\n\n return candidates[:max_candidates]\n\n\n# ----- CLI -----\n\nif __name__ == \"__main__\":\n import argparse\n import json\n import sys\n from pathlib import Path\n\n sys.path.insert(0, str(Path(__file__).parent))\n import cross_sym_registry\n\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"--ca\", required=True, help=\"Fresh CA to detect whales for\")\n ap.add_argument(\"--top-holders-json\", help=\"Path to a JSON file with top holders (output of surf token-holders --json)\")\n ap.add_argument(\"--excluded\", nargs=\"*\", default=[], help=\"Addresses to exclude (m6, operator_relay, labeled)\")\n ap.add_argument(\"--scope\", default=\"active\", choices=(\"active\", \"full\", \"top100\"))\n ap.add_argument(\"--min-pct\", type=float, default=DEFAULT_MIN_PCT)\n ap.add_argument(\"--min-cross-sym\", type=int, default=DEFAULT_MIN_CROSS_SYM)\n ap.add_argument(\"--max-candidates\", type=int, default=DEFAULT_MAX_CANDIDATES)\n ap.add_argument(\"--verbose\", \"-v\", action=\"store_true\")\n args = ap.parse_args()\n\n if args.top_holders_json:\n holders_doc = json.loads(Path(args.top_holders_json).read_text(encoding=\"utf-8\"))\n top_holders = holders_doc.get(\"data\") if isinstance(holders_doc, dict) else holders_doc\n else:\n # Fetch fresh via surf\n # v0.7.20.1: route to active chain (was hardcoded bsc). This path\n # is the standalone CLI fallback; production pipeline calls\n # set_active_chain() before reaching here.\n from chain_router import get_active_chain\n import subprocess\n result = subprocess.run(\n [\"surf\", \"token-holders\", \"--address\", args.ca, \"--chain\", get_active_chain(),\n \"--limit\", \"100\", \"--include\", \"labels\", \"--json\"],\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\", timeout=30,\n )\n if result.returncode != 0:\n print(f\"surf failed: {result.stderr}\", file=sys.stderr)\n sys.exit(1)\n top_holders = json.loads(result.stdout).get(\"data\", [])\n\n registry = cross_sym_registry.get_reverse_index(\n scope=args.scope, verbose=args.verbose,\n )\n excluded = set(a.lower() for a in args.excluded)\n\n candidates = detect(\n args.ca, top_holders, excluded, registry,\n min_pct=args.min_pct,\n min_cross_sym=args.min_cross_sym,\n max_candidates=args.max_candidates,\n )\n\n print(json.dumps({\n \"ca\": args.ca,\n \"n_candidates\": len(candidates),\n \"candidates\": candidates,\n }, ensure_ascii=False, indent=2))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10228,"content_sha256":"d62f8bd51536171dccef957c1be8bb48074ba0e6db92929e1ab7c6bf01a73a3b"},{"filename":"alpha/v06/helpers/cross_sym_registry.py","content":"#!/usr/bin/env python3\n\"\"\"cross_sym_registry.py — Universal Alpha BSC top-100 holders cache.\n\nv0.7 forward-detection foundation. Maintains a user-local cache of every\nactive Alpha BSC token's top-100 holders, with a reverse index keyed by\nholder address. Enables O(1) lookup of \"which Alpha tokens does this\nwallet hold a top-100 position in?\".\n\nDesign constraints:\n - User-local cache: `~/.binance-alpha-data/cross_sym_registry.json`\n - Lazy refresh: TTL 7 days, refresh on stale OR forced\n - Universe scope: BSC chain (chainId 56) + 24h vol > $100K (active)\n - User-configurable via SCOPE env: 'active' (default) | 'full' | 'top100'\n - Refresh cost: ~$0.75-2 per call (1 credit per token, ~$0.005-0.01)\n - All Surf calls run via the user's authenticated CLI; no central\n service, no telemetry.\n\nConcurrency: refresh uses ThreadPoolExecutor with bounded workers (8)\nto stay under Surf API rate limits while keeping wall-clock low.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport subprocess\nimport sys\nimport time\nimport uuid\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom pathlib import Path\nfrom typing import Any\n\n# ----- Module constants -----\n\nDEFAULT_CACHE_DIR = Path.home() / \".binance-alpha-data\"\nDEFAULT_CACHE_PATH = DEFAULT_CACHE_DIR / \"cross_sym_registry.json\"\nDEFAULT_LOCK_PATH = DEFAULT_CACHE_DIR / \"cross_sym_registry.lock\"\nDEFAULT_TTL_SECS = 7 * 86400 # 7 days\nDEFAULT_LOCK_TIMEOUT_SECS = 120 # max time to wait for another refresher\nDEFAULT_LOCK_STALE_SECS = 600 # treat lock as stale after 10 min (orphan)\nDEFAULT_HOLDERS_PER_TOKEN = 100\nDEFAULT_ACTIVE_VOL_THRESHOLD_USD = 100_000.0\nDEFAULT_TOP_N_BY_VOL = 100\nDEFAULT_PARALLEL_WORKERS = 8\nDEFAULT_SCHEMA_VERSION = \"0.7.0\"\nDEFAULT_RETRY_MAX = 2 # retries per Surf call on retryable errors\nDEFAULT_RETRY_BASE_SECS = 1.5 # exponential backoff base\nMIN_COVERAGE_RATIO = 0.7 # refresh must capture ≥70% of universe to commit\n\nALPHA_TOKEN_LIST_URL = (\n \"https://www.binance.com/bapi/defi/v1/public/wallet-direct/\"\n \"buw/wallet/cex/alpha/all/token/list\"\n)\nBSC_CHAIN_ID = \"56\"\n\n\n# ----- Errors -----\n\nclass RegistryError(Exception):\n \"\"\"Raised when the registry cannot be loaded or refreshed.\"\"\"\n\n\nclass SurfCallError(RegistryError):\n \"\"\"Raised when a Surf CLI call fails unexpectedly.\"\"\"\n\n\n# ----- Public API -----\n\ndef get_reverse_index(\n *,\n cache_path: Path = DEFAULT_CACHE_PATH,\n lock_path: Path = DEFAULT_LOCK_PATH,\n ttl_secs: int = DEFAULT_TTL_SECS,\n force_refresh: bool = False,\n scope: str = \"active\",\n verbose: bool = False,\n) -> dict[str, Any]:\n \"\"\"Return the reverse index. Refreshes lazily if cache is stale.\n\n audit P1a fixes applied:\n - Cross-process refresh lock (no thundering herd)\n - Stale-cache fallback on refresh failure\n - Minimum coverage guard (reject low-coverage refresh)\n - Schema validation on loaded cache\n\n Returns:\n dict with shape:\n {\n \"_schema_version\": str,\n \"snapshot_ts\": int,\n \"scope\": str,\n \"n_tokens\": int,\n \"reverse_index\": {addr_lower: [{sym, ca, pct, rank, balance}, ...]}\n }\n\n Raises:\n RegistryError if neither cached nor refreshable.\n \"\"\"\n cached = _load_cache(cache_path)\n if cached is not None and not force_refresh:\n age = int(time.time()) - cached.get(\"snapshot_ts\", 0)\n if age \u003c ttl_secs:\n if verbose:\n print(f\"[cross_sym_registry] cache hit (age {age // 60}min)\", file=sys.stderr)\n return cached\n if verbose:\n print(\n f\"[cross_sym_registry] cache stale (age {age // 3600}h > TTL {ttl_secs // 3600}h), refreshing\",\n file=sys.stderr,\n )\n elif verbose:\n print(\"[cross_sym_registry] no cache, building from scratch\", file=sys.stderr)\n\n # Cross-process lock: another invocation may already be refreshing.\n # Wait briefly; if it finishes, just use its result.\n lock_owned = _acquire_lock(lock_path, verbose=verbose)\n try:\n if not lock_owned:\n # Someone else is refreshing or held lock. Re-read cache after waiting.\n if verbose:\n print(\"[cross_sym_registry] another process refreshing, re-reading cache\", file=sys.stderr)\n cached2 = _load_cache(cache_path)\n if cached2 and (int(time.time()) - cached2.get(\"snapshot_ts\", 0)) \u003c ttl_secs:\n return cached2\n # Their refresh failed or still running too long — fall through and\n # try ourselves (best-effort)\n\n try:\n refreshed = refresh(scope=scope, verbose=verbose)\n except RegistryError as e:\n if verbose:\n print(f\"[cross_sym_registry] refresh failed ({e}); falling back to stale cache if any\", file=sys.stderr)\n if cached is not None:\n # Stale-cache fallback: better than nothing\n cached[\"_stale_fallback\"] = True\n cached[\"_refresh_error\"] = str(e)\n return cached\n raise\n\n # Coverage guard: don't commit a low-coverage refresh\n n_ok = refreshed.get(\"n_tokens\", 0)\n n_universe = refreshed.get(\"n_universe\", 0)\n if n_universe > 0 and n_ok / n_universe \u003c MIN_COVERAGE_RATIO:\n if verbose:\n print(\n f\"[cross_sym_registry] refresh coverage {n_ok}/{n_universe} \"\n f\"({100*n_ok/n_universe:.0f}%) \u003c {MIN_COVERAGE_RATIO*100:.0f}%; \"\n f\"keeping stale cache\",\n file=sys.stderr,\n )\n if cached is not None:\n cached[\"_stale_fallback\"] = True\n cached[\"_low_coverage_skip\"] = f\"{n_ok}/{n_universe}\"\n return cached\n # No prior cache and low coverage — still better than nothing, but warn\n refreshed[\"_low_coverage\"] = True\n\n _save_cache(cache_path, refreshed)\n return refreshed\n finally:\n if lock_owned:\n _release_lock(lock_path)\n\n\ndef lookup(addr: str, registry: dict | None = None) -> list[dict]:\n \"\"\"O(1) reverse lookup. Returns list of token entries the address\n is a top-100 holder in.\n\n Each entry: {sym, ca, pct, rank, balance}\n Returns [] if not in any tracked token's top-100.\n \"\"\"\n if registry is None:\n registry = get_reverse_index()\n addr_lower = addr.lower()\n return list(registry.get(\"reverse_index\", {}).get(addr_lower, []))\n\n\ndef refresh(\n *,\n scope: str = \"active\",\n holders_per_token: int = DEFAULT_HOLDERS_PER_TOKEN,\n workers: int = DEFAULT_PARALLEL_WORKERS,\n verbose: bool = False,\n) -> dict[str, Any]:\n \"\"\"Force a registry refresh from Surf. Does NOT write cache (caller does).\n\n Steps:\n 1. Fetch Alpha token list from Binance API\n 2. Filter to BSC + scope (active/full/top100)\n 3. Parallel surf token-holders for each\n 4. Build reverse index\n \"\"\"\n if scope not in (\"active\", \"full\", \"top100\"):\n raise RegistryError(f\"unknown scope: {scope!r}\")\n\n tokens = _fetch_alpha_universe(scope=scope, verbose=verbose)\n if verbose:\n print(f\"[cross_sym_registry] universe size: {len(tokens)} tokens\", file=sys.stderr)\n\n if not tokens:\n raise RegistryError(\"Alpha token universe is empty after filtering\")\n\n holders_by_token: dict[str, list[dict]] = {}\n\n def _fetch_one(token: dict) -> tuple[str, list[dict] | None]:\n ca = token[\"ca\"]\n try:\n holders = _surf_token_holders(ca, limit=holders_per_token)\n return ca, holders\n except SurfCallError as e:\n if verbose:\n print(f\" [cross_sym_registry] skipping {token['sym']}: {e}\", file=sys.stderr)\n return ca, None\n\n n_ok = 0\n n_fail = 0\n with ThreadPoolExecutor(max_workers=workers) as ex:\n future_to_token = {ex.submit(_fetch_one, t): t for t in tokens}\n for fut in as_completed(future_to_token):\n ca, holders = fut.result()\n if holders is None:\n n_fail += 1\n continue\n holders_by_token[ca] = holders\n n_ok += 1\n\n if verbose:\n print(\n f\"[cross_sym_registry] fetched {n_ok}/{len(tokens)} tokens ({n_fail} failed)\",\n file=sys.stderr,\n )\n\n reverse_index = _build_reverse_index(tokens, holders_by_token)\n\n return {\n \"_schema_version\": DEFAULT_SCHEMA_VERSION,\n \"snapshot_ts\": int(time.time()),\n \"scope\": scope,\n \"n_tokens\": n_ok,\n \"n_universe\": len(tokens),\n \"n_fetch_failed\": n_fail,\n \"reverse_index\": reverse_index,\n }\n\n\n# ----- Internals -----\n\ndef _load_cache(path: Path) -> dict | None:\n \"\"\"Load cache with light schema validation. Returns None on any failure.\"\"\"\n if not path.exists():\n return None\n try:\n data = json.loads(path.read_text(encoding=\"utf-8\"))\n except (json.JSONDecodeError, OSError):\n return None\n # Schema validation: reject obviously-bad shapes\n if not isinstance(data, dict):\n return None\n if not isinstance(data.get(\"snapshot_ts\"), int):\n return None\n if not isinstance(data.get(\"reverse_index\"), dict):\n return None\n return data\n\n\ndef _save_cache(path: Path, data: dict) -> None:\n \"\"\"Atomic write with unique temp name to avoid concurrent-writer clobber.\n\n audit P1a fix: previously used fixed `.tmp` suffix, two concurrent\n refreshes would clobber each other's temp file. Now uses uuid-suffixed\n temp file per write.\n \"\"\"\n path.parent.mkdir(parents=True, exist_ok=True)\n tmp = path.with_suffix(f\"{path.suffix}.{uuid.uuid4().hex[:8]}.tmp\")\n try:\n tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n os.replace(tmp, path)\n finally:\n # If os.replace failed, clean up the orphan tmp\n if tmp.exists():\n try:\n tmp.unlink()\n except OSError:\n pass\n\n\ndef _acquire_lock(lock_path: Path, *, verbose: bool = False) -> bool:\n \"\"\"Try to acquire a cross-process refresh lock.\n\n Returns True if we own the lock. False if another process holds it\n (caller should re-read cache and use it).\n\n Uses O_CREAT|O_EXCL for race-free creation. Stale locks (older than\n DEFAULT_LOCK_STALE_SECS) are forcibly removed and re-acquired.\n \"\"\"\n lock_path.parent.mkdir(parents=True, exist_ok=True)\n deadline = time.time() + DEFAULT_LOCK_TIMEOUT_SECS\n pid = os.getpid()\n payload = f\"{pid}:{int(time.time())}\\n\".encode()\n while True:\n try:\n fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)\n os.write(fd, payload)\n os.close(fd)\n return True\n except FileExistsError:\n pass\n # Check if existing lock is stale\n try:\n mtime = lock_path.stat().st_mtime\n if time.time() - mtime > DEFAULT_LOCK_STALE_SECS:\n if verbose:\n print(f\"[cross_sym_registry] removing stale lock (age {int(time.time()-mtime)}s)\", file=sys.stderr)\n try:\n lock_path.unlink()\n except OSError:\n pass\n continue\n except OSError:\n pass\n if time.time() >= deadline:\n return False # give up, fall back to other process's result\n time.sleep(2.0)\n\n\ndef _release_lock(lock_path: Path) -> None:\n try:\n lock_path.unlink()\n except OSError:\n pass\n\n\ndef _fetch_alpha_universe(*, scope: str, verbose: bool) -> list[dict]:\n \"\"\"Fetch Binance Alpha listing and filter by scope.\n\n Returns list of {sym, name, ca, volume24h}.\n \"\"\"\n import urllib.request\n req = urllib.request.Request(\n ALPHA_TOKEN_LIST_URL,\n headers={\"User-Agent\": \"binance-alpha-skill/0.7\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=15) as resp:\n doc = json.loads(resp.read().decode(\"utf-8\"))\n except (OSError, json.JSONDecodeError) as e:\n raise RegistryError(f\"Alpha API fetch failed: {e}\") from e\n\n raw = doc.get(\"data\") or []\n bsc_tokens = []\n for t in raw:\n if t.get(\"chainId\") != BSC_CHAIN_ID:\n continue\n ca = (t.get(\"contractAddress\") or \"\").lower()\n if not ca.startswith(\"0x\") or len(ca) != 42:\n continue\n vol = t.get(\"volume24h\")\n try:\n vol_f = float(vol) if vol else 0.0\n except (ValueError, TypeError):\n vol_f = 0.0\n bsc_tokens.append({\n \"sym\": (t.get(\"symbol\") or \"\").upper(),\n \"name\": t.get(\"name\") or \"\",\n \"ca\": ca,\n \"volume24h\": vol_f,\n })\n\n if scope == \"full\":\n return bsc_tokens\n\n if scope == \"active\":\n return [t for t in bsc_tokens if t[\"volume24h\"] > DEFAULT_ACTIVE_VOL_THRESHOLD_USD]\n\n if scope == \"top100\":\n return sorted(bsc_tokens, key=lambda t: t[\"volume24h\"], reverse=True)[:DEFAULT_TOP_N_BY_VOL]\n\n raise RegistryError(f\"unknown scope: {scope!r}\")\n\n\ndef _surf_token_holders(\n ca: str,\n *,\n limit: int,\n max_retries: int = DEFAULT_RETRY_MAX,\n) -> list[dict]:\n \"\"\"Call surf token-holders for one token. Returns list of holder dicts.\n\n audit P1a fix: bounded retry with exponential backoff + jitter\n for retryable Surf errors (rate-limit / timeout). Hard errors (bad\n address, auth) fail fast without retry.\n\n Returns list capped to `limit` (defensive, in case Surf returns more).\n \"\"\"\n # Defensive: validate ca format before invoking subprocess\n if not (ca.startswith(\"0x\") and len(ca) == 42):\n raise SurfCallError(f\"invalid ca format: {ca!r}\")\n\n # v0.7.20.1: kept hardcoded \"bsc\" intentionally — cross_sym_registry\n # is a BSC-only Alpha-token catalogue today (see bsc_tokens variable\n # in fetch_alpha_token_list above). Cross-chain registry support\n # tracked separately; once it lands this should accept a per-token\n # chain hint instead of reading the active forensic chain.\n cmd = [\n \"surf\", \"token-holders\",\n \"--address\", ca,\n \"--chain\", \"bsc\",\n \"--limit\", str(limit),\n \"--include\", \"labels\",\n \"--json\",\n ]\n\n last_err: Exception | None = None\n for attempt in range(max_retries + 1):\n try:\n result = subprocess.run(\n cmd,\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n timeout=30,\n check=False,\n )\n except subprocess.TimeoutExpired as e:\n last_err = e\n if attempt \u003c max_retries:\n _sleep_backoff(attempt)\n continue\n raise SurfCallError(f\"surf timed out for {ca}: {e}\") from e\n except OSError as e:\n # OSError = surf binary missing / fork failure — not retryable\n raise SurfCallError(f\"surf invocation failed for {ca}: {e}\") from e\n\n if result.returncode == 0:\n try:\n doc = json.loads(result.stdout)\n except json.JSONDecodeError as e:\n raise SurfCallError(f\"surf returned non-JSON for {ca}: {e}\") from e\n if doc.get(\"error\"):\n # Check if retryable\n err = doc[\"error\"]\n err_msg = err.get(\"message\", \"\")\n if (err.get(\"retryable\") or \"TIMEOUT\" in (err.get(\"code\") or \"\")) and attempt \u003c max_retries:\n last_err = SurfCallError(f\"retryable: {err_msg}\")\n _sleep_backoff(attempt)\n continue\n raise SurfCallError(f\"surf API error for {ca}: {err_msg}\")\n holders = doc.get(\"data\") or []\n return holders[:limit] # defensive cap\n\n # Non-zero exit. Check stderr for hint at retryable.\n stderr = result.stderr.lower()\n if (\"rate\" in stderr or \"timeout\" in stderr or \"503\" in stderr or \"429\" in stderr) and attempt \u003c max_retries:\n last_err = SurfCallError(f\"retryable exit {result.returncode}\")\n _sleep_backoff(attempt)\n continue\n raise SurfCallError(\n f\"surf exited {result.returncode} for {ca}: {result.stderr[:200]}\"\n )\n\n # Should not reach here, but just in case\n raise SurfCallError(f\"surf exhausted retries for {ca}: {last_err}\")\n\n\ndef _sleep_backoff(attempt: int) -> None:\n \"\"\"Exponential backoff with full jitter.\"\"\"\n delay = DEFAULT_RETRY_BASE_SECS * (2 ** attempt)\n delay = random.uniform(0, delay)\n time.sleep(delay)\n\n\ndef _build_reverse_index(\n tokens: list[dict],\n holders_by_token: dict[str, list[dict]],\n) -> dict[str, list[dict]]:\n \"\"\"Build addr_lower → [{sym, ca, pct, rank, balance, entity_name}, ...].\"\"\"\n index: dict[str, list[dict]] = {}\n by_ca = {t[\"ca\"]: t for t in tokens}\n for ca, holders in holders_by_token.items():\n sym = by_ca.get(ca, {}).get(\"sym\", \"\")\n for rank, h in enumerate(holders, 1):\n addr = (h.get(\"address\") or \"\").lower()\n if not addr or not addr.startswith(\"0x\"):\n continue\n entry = {\n \"sym\": sym,\n \"ca\": ca,\n \"rank\": rank,\n \"pct\": float(h.get(\"percentage\", 0)),\n \"balance\": str(h.get(\"balance\", \"0\")), # keep precision, large numbers\n }\n ent_name = h.get(\"entity_name\")\n if ent_name:\n entry[\"entity_name\"] = ent_name\n entry[\"entity_type\"] = h.get(\"entity_type\", \"\")\n index.setdefault(addr, []).append(entry)\n\n # Sort each holder's appearances by pct descending (most concentrated first)\n for addr in index:\n index[addr].sort(key=lambda e: e[\"pct\"], reverse=True)\n\n return index\n\n\n# ----- CLI ------\n\nif __name__ == \"__main__\":\n import argparse\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"--scope\", default=\"active\", choices=(\"active\", \"full\", \"top100\"))\n ap.add_argument(\"--refresh\", action=\"store_true\", help=\"Force refresh even if cache fresh\")\n ap.add_argument(\"--verbose\", \"-v\", action=\"store_true\")\n ap.add_argument(\"--lookup\", metavar=\"ADDR\", help=\"Lookup a single address and exit\")\n args = ap.parse_args()\n\n if args.lookup:\n idx = get_reverse_index(verbose=args.verbose, scope=args.scope)\n hits = lookup(args.lookup, idx)\n print(json.dumps({\n \"address\": args.lookup.lower(),\n \"n_hits\": len(hits),\n \"tokens\": hits,\n }, ensure_ascii=False, indent=2))\n else:\n registry = get_reverse_index(\n force_refresh=args.refresh,\n scope=args.scope,\n verbose=args.verbose,\n )\n print(json.dumps({\n \"snapshot_ts\": registry[\"snapshot_ts\"],\n \"scope\": registry[\"scope\"],\n \"n_tokens\": registry[\"n_tokens\"],\n \"n_universe\": registry.get(\"n_universe\"),\n \"n_fetch_failed\": registry.get(\"n_fetch_failed\"),\n \"n_unique_addrs\": len(registry[\"reverse_index\"]),\n }, indent=2))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19358,"content_sha256":"6c741d6b1998320b76a4c25f4699fc2f7542d0923152e8e75537ee49064b2d17"},{"filename":"alpha/v06/helpers/dump_tracker.py","content":"#!/usr/bin/env python3\n\"\"\"dump_tracker.py — entity-level realized-sell (\"派发\"/Wyckoff distribution)\ntracking, distinct from wallet-to-wallet transfers (\"分发\").\n\nWhy (Phase 2, 2026-05-27):\n\n v0.7.10.x `dumped_pct` is wallet-level outflow — it counts ANY transfer\n out (to another wallet, to a vesting contract, to an exchange) as\n \"distributed\". The user wants the Wyckoff sense of 派发: an entity (and\n its wallet cluster) actually SELLING — tokens reaching a CEX deposit, a\n DEX swap, or a cross-chain bridge. A wallet shuffling tokens to its own\n next address is 分发 (transfer), not 派发 (sell).\n\nCost design (validated 2026-05-27):\n\n Pulling the whole-token transfer graph (even segmented + parallel) is too\n heavy — `GROUP BY from,to` over monthly bsc_transfers partitions ran\n >350s before being killed. INSTEAD this module is purely POST-PROCESSING\n of rule_11's already-fetched graph:\n - rule_11 already pulled deployer → receivers → m6 sub-receivers and\n each dumper's `dumper_destinations` (targeted WHERE from=addr queries,\n which are fast). That graph is in memory.\n - We classify the terminal/destination nodes via Arkham (batch) into\n CEX-custody / DEX-infra / bridge = SELL terminals.\n - A \"sell\" = token amount flowing from a cluster wallet (or its m6\n descendants) into a sell terminal.\n - One `surf market-price` series query values every sell at its\n timestamp (in-memory binary search, no per-sell query).\n Net new on-chain cost: 0 transfer SQL + 1 price query + label batches\n (mostly reused from rule_11's enrichment). Wall-clock +20-30s.\n\nOutput (consumed by the skeleton's dump_tracking block + verdict + render).\n\nNOTE: v0.7.12 replaced the earlier per-cluster disposal shape (clusters[] with\nper-cluster sold_tokens / est_profit_usd) with a single conservation-safe\nCONFIRMED-SOLD LOWER BOUND. Per-entity profit attribution was dropped on\npurpose: `received_from_deployer` is a flow that double-counts on relays, so\nper-entity disposal could not be attributed without over-claiming. The headline\nis now the aggregate confirmed lower bound + a single estimated-proceeds figure\n(`confirmed_total_tokens × robust median DEX price`; insiders are zero-cost\ngenesis allocations, so estimated proceeds ≈ realized profit):\n\n {\n \"insider_n_wallets\": int,\n # CONFIRMED-SOLD LOWER BOUND (a) insider→CEX-deposit + (b) insider DEX swaps,\n # capped at (total_supply − tree_holds) by stock conservation:\n \"confirmed_cex_tokens\": float, \"confirmed_cex_pct\": float | None,\n \"confirmed_cex_labels\": [str, ...],\n \"confirmed_dex_tokens\": float, \"confirmed_dex_pct\": float | None,\n \"confirmed_dex_swaps\": int,\n \"confirmed_total_tokens\": float, \"confirmed_total_pct\": float | None,\n \"confirmed_est_profit_usd\": float | None, # 估算套现 = total × median px\n \"confirmed_capped\": bool, # floor clamped to the ceiling\n # v0.7.19.4: split fields.\n # TREE holdings (stock, ALL traced wallets incl. lockup + exit-infra) —\n # conservation anchor for `max_left` / confirmed_capped:\n \"tree_holds_tokens\": float, \"tree_holds_pct_supply\": float | None,\n # PURE insider holdings (stock, EXCLUDING Arkham-confirmed\n # protocol_lockup + cex_custody + dex_infra) — narrative anchor:\n \"pure_insider_holds_tokens\": float, \"pure_insider_holds_pct_supply\": float | None,\n # Backward-compat alias for the (now misleading) old field name —\n # points at tree_holds, kept so external consumers don't break:\n \"insider_holds_tokens\": float, \"insider_holds_pct_supply\": float | None,\n \"median_price_usd\": float | None,\n # Wash context:\n \"wash_dominated\": bool, \"n_dex_sellers\": int,\n \"total_dex_swaps\": int, \"top_seller_swaps\": int,\n \"buckets_complete\": bool, # False → a query failed; floor may undercount\n }\n\"\"\"\n\nfrom __future__ import annotations\n\nimport bisect\nimport concurrent.futures\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom chain_router import transfers_table, dex_trades_table # v0.7.20\nfrom chain_router import decimals_factor_str # v0.9.7\nfrom pathlib import Path\nfrom push_airdrop_detector import fetch_push_airdrop_recipients # v0.8.1\nfrom typing import Any\n\n# v0.7.19: parallelize the 4 independent surf SQL queries in `run()`.\n# Each query is independent (no inter-query data dependency) and goes\n# through `_run_surf_with_retry` which itself handles per-attempt\n# back-off — so submitting all 4 at once cannot create more concurrent\n# surf calls than max_workers=4 would anyway. Override via env if a\n# future token's fan-out spikes credit usage past safe levels.\nDUMP_TRACKER_WORKERS = max(1, int(\n os.environ.get(\"BINANCE_ALPHA_DUMP_TRACKER_WORKERS\", \"4\")\n))\n\nsys.path.insert(0, str(Path(__file__).parent))\n\n# SQL-injection guard (adversarial review MEDIUM #4): every address / CA reaching an\n# f-string SQL must pass `chain_router.is_valid_addr` for the active\n# chain. v0.7.21.7: chain-aware — EVM 0x40-hex on EVM chains, Solana\n# base58 32-44 on Solana. Pre-v0.7.21.7 this was hardcoded EVM, which\n# silently swallowed every base58 address downstream — dump_tracker\n# returned empty on Solana even after Section A accepted the CA.\nfrom chain_router import is_valid_addr as _chain_is_valid_addr # noqa: E402\nfrom chain_router import get_active_chain as _chain_get_active # noqa: E402\n\n# Kept for any out-of-pipeline caller; the SQL helpers below use\n# `_chain_is_valid_addr` so behaviour follows the router.\n_HEX_ADDR_RE = re.compile(r\"^0x[0-9a-f]{40}$\")\n\n\ndef _clean_addrs(addrs: list[str]) -> list[str]:\n \"\"\"Keep only addresses that pass the active chain's format check,\n de-duplicated + sorted. EVM normalises case (lowercase); Solana\n base58 is case-sensitive and preserved as-is.\"\"\"\n is_solana = _chain_get_active() == \"solana\"\n if is_solana:\n return sorted({a for a in addrs if a and _chain_is_valid_addr(a)})\n return sorted({\n a.lower() for a in addrs\n if a and _chain_is_valid_addr(a.lower())\n })\n\n\ndef _valid_ca(ca: str) -> str:\n \"\"\"Return the validated CA (lowercased on EVM, preserved on Solana)\n iff it passes the active chain's format check, else '' so callers'\n `if not ca` guards short-circuit to a safe empty result.\"\"\"\n if not ca:\n return \"\"\n if _chain_get_active() == \"solana\":\n return ca if _chain_is_valid_addr(ca) else \"\"\n ca_lower = ca.lower()\n return ca_lower if _chain_is_valid_addr(ca_lower) else \"\"\n\n\ndef fetch_current_balances(ca: str, wallets: list[str], date_floor: str) -> dict[str, float]:\n \"\"\"Current token balance per wallet via one bsc_transfers in/out CTE.\n\n balance = sum(in) − sum(out) since date_floor. Used to anchor the\n apparatus disposed amount on STOCK (conservation-safe), since rule_11\n only computes balances for depth-1 receivers (deep relays are None).\n \"\"\"\n ca = _valid_ca(ca)\n wallets = _clean_addrs(wallets)\n if not wallets or not ca:\n return {}\n arr = \"[\" + \",\".join(f\"'{w}'\" for w in wallets) + \"]\"\n sql = (\n f\"WITH ins AS (SELECT \\\"to\\\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS amt \"\n f\"FROM {transfers_table()} WHERE contract_address='{ca.lower()}' AND block_date >= '{date_floor}' GROUP BY a), \"\n f\"outs AS (SELECT \\\"from\\\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS amt \"\n f\"FROM {transfers_table()} WHERE contract_address='{ca.lower()}' AND block_date >= '{date_floor}' GROUP BY a) \"\n \"SELECT r.a AS a, COALESCE(ins.amt,0) - COALESCE(outs.amt,0) AS bal \"\n f\"FROM (SELECT arrayJoin({arr}) AS a) r LEFT JOIN ins ON r.a=ins.a LEFT JOIN outs ON r.a=outs.a\"\n )\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": 500}), base_timeout=60,\n max_attempts=4, # v0.7.23 per surf-team guidance (2026-06-09): \"30s+ query retried 3× = load×3, exacerbates contention → backoff or fail loud, not brute-force retry\". 4 attempts is the client-side cooperation pattern, not a vendor-outage fallback.\n )\n except Exception as e:\n doc, err = None, f\"exception: {str(e)[:120]}\"\n if not doc:\n # audit H6 fix: surface surf failure via sentinel key so the\n # caller can mark `balances_ok=False` and trip buckets_complete.\n # Returning a plain {} silently zeroes pure_insider_holds /\n # tree_holds downstream, indistinguishable from a token with no\n # wallet activity.\n return {\"__ERR\": err or \"surf returned no doc\"}\n out: dict[str, float] = {}\n for r in (doc.get(\"data\") or []):\n a = (r.get(\"a\") or \"\").lower()\n if a:\n try:\n out[a] = float(r.get(\"bal\") or 0)\n except (TypeError, ValueError):\n out[a] = 0.0\n return out\n\n\ndef fetch_dex_sell_profile(ca: str, from_date: str) -> dict[str, Any]:\n \"\"\"One bsc_dex_trades query → token-level sell profile for the wash /\n pricing narrative. Returns:\n {\n \"median_price_usd\": float | None, # robust price (amount_usd/token);\n # median defeats MEV/aggregator\n # garbage amount_usd outliers\n \"n_sellers\": int, \"total_swaps\": int,\n \"top_seller_swaps\": int, # max swaps by one tx_from\n \"wash_dominated\": bool, # top seller >1000 swaps OR\n # swaps/sellers > 50 → wash bots\n }\n \"\"\"\n ca = _valid_ca(ca)\n if not ca:\n return {}\n sql = (\n \"SELECT tx_from AS w, count() AS n, \"\n \"median(amount_usd / nullIf(token_sold_amount,0)) AS unit_px \"\n f\"FROM {dex_trades_table()} WHERE token_sold_address='{ca.lower()}' \"\n f\"AND block_date >= '{from_date}' AND amount_usd > 0 AND token_sold_amount > 0 \"\n \"GROUP BY w ORDER BY n DESC LIMIT 200\"\n )\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": 200}), base_timeout=40,\n max_attempts=4, # v0.7.23 per surf-team guidance (2026-06-09): \"30s+ query retried 3× = load×3, exacerbates contention → backoff or fail loud, not brute-force retry\". 4 attempts is the client-side cooperation pattern, not a vendor-outage fallback.\n )\n except Exception as e:\n doc, err = None, f\"exception: {str(e)[:120]}\"\n if not doc:\n # audit H6 fix: sentinel error key — caller checks and\n # marks profile_ok=False to feed buckets_complete. Empty dict\n # would silently zero wash_dominated / top_seller_addrs.\n return {\"__ERR\": err or \"surf returned no doc\"}\n rows = doc.get(\"data\") or []\n if not rows:\n return {\"median_price_usd\": None, \"n_sellers\": 0, \"total_swaps\": 0,\n \"top_seller_swaps\": 0, \"wash_dominated\": False}\n # Robust unit price = median across sellers of their per-seller median\n # unit price, then clamp insane values (>1e6 $/token is garbage).\n pxs = sorted(\n p for r in rows\n if (p := r.get(\"unit_px\")) is not None and 0 \u003c float(p) \u003c 1e6\n )\n median_px = pxs[len(pxs) // 2] if pxs else None\n total_swaps = sum(int(r.get(\"n\") or 0) for r in rows)\n top_swaps = max((int(r.get(\"n\") or 0) for r in rows), default=0)\n n_sellers = len(rows)\n wash_dominated = top_swaps > 1000 or (n_sellers and total_swaps / n_sellers > 50)\n # v0.7.21: expose the seller address list so flow_operator_detector\n # can reuse it without paying for a second SQL. Rows already come\n # ordered by swap count DESC from the SELECT, so the list is the\n # ordered top sellers (cap 200 — matches the LIMIT clause).\n top_sellers = [\n str(r.get(\"w\") or \"\").lower() for r in rows\n if r.get(\"w\") and _HEX_ADDR_RE.match(str(r.get(\"w\")).lower())\n ]\n return {\n \"median_price_usd\": float(median_px) if median_px else None,\n \"n_sellers\": n_sellers,\n \"total_swaps\": total_swaps,\n \"top_seller_swaps\": top_swaps,\n \"wash_dominated\": bool(wash_dominated),\n \"top_seller_addrs\": top_sellers,\n }\n\n\ndef fetch_apparatus_dex_sold(ca: str, wallets: list[str], from_date: str) -> dict[str, Any]:\n \"\"\"(b) tokens the APPARATUS WALLETS THEMSELVES sold on DEX.\n\n dex_trades token_sold=CA AND tx_from IN apparatus → these are confirmed\n on-chain sells BY the insider wallets (real buyer paid the other side).\n Returns {\"dex_sold_tokens\": float, \"n_swaps\": int,\n \"dex_sold_usd\": float, \"dex_twap_usd_per_token\": float | None}.\n\n v0.7.21.10: also SUM(amount_usd) on the same query so the caller can\n build a \"Net Sell Out\" estimate that uses the apparatus' own\n time-weighted-average price (TWAP) rather than the wash-inflatable\n `fetch_dex_sell_profile.median_price_usd`. Zero new credits — same\n table / filter / partition, just two extra SELECT columns.\n\n Note: gross swap volume (wash-inflatable if an apparatus wallet itself\n wash-trades); caller caps confirmed total at net outflow.\n \"\"\"\n ca = _valid_ca(ca)\n wallets = _clean_addrs(wallets)\n if not wallets or not ca:\n return {\"dex_sold_tokens\": 0.0, \"n_swaps\": 0,\n \"dex_sold_usd\": 0.0, \"dex_twap_usd_per_token\": None}\n in_list = \",\".join(f\"'{w}'\" for w in wallets)\n # v0.9.2: per-window split via SQL CASE WHEN — same partition scan,\n # same surf cost, just extra SELECT columns. EmberCN-style short-term\n # alarms (\"过去 14 小时套现 $27.7M\") need 7d / 30d breakdown to be\n # comparable to our previously-only-cumulative number.\n sql = (\n \"SELECT sum(token_sold_amount) AS sold, count() AS n, \"\n # v0.7.21.10: amount_usd already on the row in surf's dex_trades\n # schema; ClickHouse SUM is free against the existing partition\n # scan. Filter `amount_usd > 0` defends against the\n # NULL / 0 rows surf occasionally emits for MEV-aggregator txs\n # without a priced quote leg.\n \"sum(if(amount_usd > 0, amount_usd, 0)) AS sold_usd, \"\n # Per-window splits\n \"sum(if(block_date >= today() - 7, token_sold_amount, 0)) AS sold_7d, \"\n \"sum(if(block_date >= today() - 7, if(amount_usd > 0, amount_usd, 0), 0)) AS sold_usd_7d, \"\n \"countIf(block_date >= today() - 7) AS n_7d, \"\n \"sum(if(block_date >= today() - 30, token_sold_amount, 0)) AS sold_30d, \"\n \"sum(if(block_date >= today() - 30, if(amount_usd > 0, amount_usd, 0), 0)) AS sold_usd_30d, \"\n \"countIf(block_date >= today() - 30) AS n_30d \"\n f\"FROM {dex_trades_table()} WHERE token_sold_address='{ca.lower()}' \"\n f\"AND tx_from IN ({in_list}) AND block_date >= '{from_date}'\"\n )\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": 5}), base_timeout=40,\n max_attempts=4, # v0.7.23 per surf-team guidance (2026-06-09): \"30s+ query retried 3× = load×3, exacerbates contention → backoff or fail loud, not brute-force retry\". 4 attempts is the client-side cooperation pattern, not a vendor-outage fallback.\n )\n except Exception as e:\n doc, err = None, str(e)[:120]\n # ok=False means the query never succeeded (surf throttled past all\n # retries) — caller must NOT treat the 0 as a real \"no DEX sells\", it is\n # unknown. A flaked 0 would silently understate confirmed disposal.\n if not doc:\n return {\"dex_sold_tokens\": 0.0, \"n_swaps\": 0,\n \"dex_sold_usd\": 0.0, \"dex_twap_usd_per_token\": None,\n \"ok\": False, \"error\": err}\n rows = doc.get(\"data\") or []\n r = rows[0] if rows else {}\n sold = float(r.get(\"sold\") or 0)\n sold_usd = float(r.get(\"sold_usd\") or 0)\n # TWAP = volume-weighted average price the apparatus actually got. Defaults\n # to None when there is no DEX swap (avoids divide-by-zero into 0).\n twap = (sold_usd / sold) if sold > 0 and sold_usd > 0 else None\n sold_7d = float(r.get(\"sold_7d\") or 0)\n sold_usd_7d = float(r.get(\"sold_usd_7d\") or 0)\n sold_30d = float(r.get(\"sold_30d\") or 0)\n sold_usd_30d = float(r.get(\"sold_usd_30d\") or 0)\n return {\n \"dex_sold_tokens\": sold,\n \"n_swaps\": int(r.get(\"n\") or 0),\n \"dex_sold_usd\": sold_usd,\n \"dex_twap_usd_per_token\": twap,\n # v0.9.2: per-window split\n \"dex_sold_tokens_7d\": sold_7d,\n \"dex_sold_usd_7d\": sold_usd_7d,\n \"n_swaps_7d\": int(r.get(\"n_7d\") or 0),\n \"dex_sold_tokens_30d\": sold_30d,\n \"dex_sold_usd_30d\": sold_usd_30d,\n \"n_swaps_30d\": int(r.get(\"n_30d\") or 0),\n \"ok\": True,\n }\n\n\ndef fetch_apparatus_to_cex(ca: str, wallets: list[str], from_date: str) -> dict[str, Any]:\n \"\"\"(a) tokens the APPARATUS transferred to CEX-deposit addresses.\n\n One query: transfers FROM apparatus wallets GROUP BY destination; then\n Arkham-classify destinations and sum those routed to a CEX. CEX deposit\n addresses are sinks (tokens go in, get swept internally, don't return to\n the depositor), so cumulative transfer ≈ net deposited. Convention:\n deposit to CEX = exit/sell (the actual sale is off-chain).\n\n Returns {\"cex_tokens\": float, \"cex_labels\": [str]}.\n \"\"\"\n ca = _valid_ca(ca)\n wallets = _clean_addrs(wallets)\n if not wallets or not ca:\n return {\"cex_tokens\": 0.0, \"cex_labels\": []}\n in_list = \",\".join(f\"'{w}'\" for w in wallets)\n # v0.9.2: per-dest per-window split. Total still GROUP BY dest so\n # the Arkham classification flow downstream is unchanged; the bucket\n # columns let the caller compute confirmed_cex_tokens_7d / _30d at\n # post-process time (sum of per-dest bucket if dest classified CEX).\n sql = (\n 'SELECT \"to\" AS dest, '\n f'sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS tok, '\n 'sum(if(block_date >= today() - 7, '\n f' toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}, 0)) AS tok_7d, '\n 'sum(if(block_date >= today() - 30, '\n f' toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}, 0)) AS tok_30d '\n f\"FROM {transfers_table()} WHERE contract_address='{ca.lower()}' \"\n f'AND \"from\" IN ({in_list}) AND block_date >= \\'{from_date}\\' '\n \"GROUP BY dest ORDER BY tok DESC LIMIT 100\"\n )\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": 100}), base_timeout=60,\n max_attempts=4, # v0.7.23 per surf-team guidance (2026-06-09): \"30s+ query retried 3× = load×3, exacerbates contention → backoff or fail loud, not brute-force retry\". 4 attempts is the client-side cooperation pattern, not a vendor-outage fallback.\n )\n except Exception as e:\n doc, err = None, str(e)[:120]\n # ok=False means surf never returned (throttled past all retries); the 0\n # is unknown, not a real \"no CEX deposits\" — see fetch_apparatus_dex_sold.\n if not doc:\n return {\"cex_tokens\": 0.0, \"cex_labels\": [], \"ok\": False, \"error\": err}\n dests = [(r.get(\"dest\") or \"\").lower() for r in (doc.get(\"data\") or []) if r.get(\"dest\")]\n apparatus_set = set(wallets)\n # Exclude apparatus-internal transfers; classify external destinations.\n ext = [d for d in dests if d not in apparatus_set]\n from protocol_lockup_detector import enrich_addresses_with_lockup_classification\n cls = enrich_addresses_with_lockup_classification(ext) if ext else {}\n cex_tokens = 0.0\n cex_tokens_7d = 0.0\n cex_tokens_30d = 0.0\n cex_labels: list[str] = []\n for r in (doc.get(\"data\") or []):\n d = (r.get(\"dest\") or \"\").lower()\n if d in apparatus_set:\n continue\n c = cls.get(d) or {}\n if c.get(\"is_cex_custody\"):\n cex_tokens += float(r.get(\"tok\") or 0)\n cex_tokens_7d += float(r.get(\"tok_7d\") or 0)\n cex_tokens_30d += float(r.get(\"tok_30d\") or 0)\n lab = c.get(\"display_label\")\n if lab and lab not in cex_labels:\n cex_labels.append(lab)\n return {\n \"cex_tokens\": cex_tokens,\n # v0.9.2: per-window CEX-routed tokens (subset of cex_tokens\n # where the transfer's block_date is within the window).\n \"cex_tokens_7d\": cex_tokens_7d,\n \"cex_tokens_30d\": cex_tokens_30d,\n \"cex_labels\": cex_labels[:5],\n \"ok\": True,\n }\n\n\ndef _sane_effective_price(dex_px: float | None,\n spot_px: float | None) -> float | None:\n \"\"\"v1.2.5 (SLX 2026-06-29): bound a DEX-derived per-token price by the\n reliable spot price before using it to value the CEX disposal leg.\n\n The DEX prices (twap / median) are computed from surf `dex_trades.amount`,\n which surf decimal-adjusts assuming 18 decimals. For a non-18-decimal token\n (SLX = 8) the token amount is mis-scaled by 10^(18-d), so the implied\n USD/token EXPLODES (SLX: $243B/token → a $2.7e18 CEX estimate). Spot price\n comes from a different source (Alpha API / LP), so it is trustworthy.\n\n Rule: if there is a usable spot price, reject a DEX price that is >100x or\n \u003c0.01x spot (decimals-broken) and fall back to spot. The 100x band keeps\n legitimate new-token volatility; a decimals break is ≥10^9x for common\n 6/8/9-decimal tokens, so it is always caught. With no spot price we cannot\n sanity-check, so the DEX price is returned unchanged (legacy behaviour).\n \"\"\"\n if spot_px and spot_px > 0:\n if not dex_px or not (0.01 * spot_px \u003c= dex_px \u003c= 100 * spot_px):\n return spot_px\n return dex_px\n\n\n# ---- main entry ----\n\n\ndef run(\n *,\n rule11: dict[str, Any],\n ca: str,\n symbol: str,\n listing_ts_ms: int | None,\n listing_date: str | None,\n circulating_supply: float | None,\n total_supply: float | None,\n extra_receivers: list[dict] | None = None,\n spot_price_usd: float | None = None,\n) -> dict[str, Any]:\n \"\"\"Insider disposal (\"派发\") tracking — CONFIRMED-SOLD LOWER BOUND only.\n\n Methodology (2026-05-28 redesign, leader call — see\n reference_dump_vs_distribution_methodology §6):\n\n We report ONLY what the chain can prove, because that is the number a\n holder should act on (defensive posture). We deliberately DROPPED the\n \"allocation − current balance\" upper bound: allocation was a SUM of\n `received_from_deployer`, a FLOW that double-counts whenever the deployer\n round-trips / relays tokens (R2 cycled ~1B through 4 wallets, each logged\n ~1B → a 1013%-of-circulating, >total-supply impossibility). An upper bound\n that is routinely wrong is worse than no upper bound.\n\n What we report:\n * CONFIRMED-SOLD LOWER BOUND = (a) transfers from insider wallets into\n CEX-deposit addresses (deposit = sale by convention; fill is off-chain)\n + (b) insider wallets' OWN DEX swaps selling the token (token_sold=CA,\n tx_from ∈ insiders → a real buyer paid). Both are DIRECTED exits, so no\n round-trip double-count. (a)+(b) is a FLOOR: true selling is at least\n this, possibly more via routes we cannot label.\n * CURRENT INSIDER HOLDINGS (stock) = Σ current balance of the insider set\n — round-trip-immune (≤ total supply always). Includes still-locked\n vesting, so it is \"% of supply under insider control\", not free float.\n * PRICE = robust median DEX unit price (defeats amount_usd outliers);\n est. proceeds = confirmed_total × median.\n * Insider set = all traced rule_11 receivers EXCEPT exit infrastructure\n (cex_custody / dex_infra are destinations, not insider-held wallets).\n\n Args:\n rule11: raw rule_11 return (pre_launch_receivers).\n ca / symbol / listing_ts_ms / listing_date / circulating_supply /\n total_supply: from Section A.\n \"\"\"\n receivers = list(rule11.get(\"pre_launch_receivers\", []) or [])\n # v0.8.6.6: mining mode fallback — when rule_11 m6 lineage is empty\n # (cross-chain bridge / mining token w/ placeholder deployer), pipeline\n # injects mining cluster wallets (mint_authority + ht_dumpers + fanout +\n # cluster_graph union) as synthetic receivers so dump_tracker (a)/(b)\n # CEX/DEX confirmed-sell SQL runs against them. Unlocks \"confirmed net\n # sellout\" estimation for mining-class tokens (JCT/AOP/BTX previously\n # all showed '无法 reliably 估算').\n if extra_receivers:\n seen = {(r.get(\"addr\") or \"\").lower() for r in receivers if r.get(\"addr\")}\n for er in extra_receivers:\n ea = (er.get(\"addr\") or \"\").lower()\n if ea and ea not in seen:\n receivers.append(er)\n seen.add(ea)\n dumper_dests = rule11.get(\"dumper_destinations\", {}) or {}\n recv_by_addr: dict[str, dict] = {\n (r.get(\"addr\") or \"\").lower(): r for r in receivers\n }\n circ = circulating_supply or total_supply or 0\n if not listing_date and listing_ts_ms:\n from datetime import datetime, timezone\n listing_date = datetime.fromtimestamp(listing_ts_ms / 1000, tz=timezone.utc).strftime(\"%Y-%m-%d\")\n\n # v0.9.1: surf enforces a 365-day block_date window on bsc_transfers /\n # bsc_dex_trades (discovered via SIREN 2026-06-14, listing 2025-02-19,\n # age 480 days → INVALID_REQUEST → confirmed_sellout silently $0).\n # Clamp date_floor to the 364-day safe window via surf_constraints.\n from surf_constraints import surf_safe_date_floor\n date_floor = surf_safe_date_floor(listing_date)\n\n # ── INSIDER SET ── all traced wallets EXCEPT exit infrastructure.\n # CEX-deposit / DEX-router addresses are destinations (tokens there have\n # LEFT insider control), not insider-held wallets — excluded from both the\n # source set (a)/(b) and the holdings stock. No depth / allocation logic:\n # we don't sum the `received_from_deployer` flow at all anymore (it\n # double-counts on round-trips), so the whole apparatus/perimeter/_depth\n # machinery is gone.\n #\n # v0.8.1: also detect & exclude PUSH AIRDROP RECIPIENTS. A `disperse`-\n # style batch transfer from the deployer to >=50 distinct retail\n # wallets in the pre-launch window is a retail airdrop — those EOAs\n # are not insiders, and counting their post-launch sells as \"insider\n # 真实变现\" inflates the (a)+(b) floor. Detection is one surf SQL\n # against the deployer-outflow tx window. See push_airdrop_detector\n # module docstring for threshold rationale (= 50 conservative).\n deployer_addr = (rule11.get(\"deployer\") or \"\").lower() or None\n # v0.9.1: same 365-day clamp as date_floor above. The pre-launch\n # window for push-airdrop detection is bounded to listing_date itself,\n # so for old tokens this falls back to the 364-day surf safe floor.\n trace_floor = surf_safe_date_floor(\n rule11.get(\"trace_floor\") or rule11.get(\"date_floor\")\n )\n push_airdrop_result: dict[str, Any] = {}\n push_airdrop_set: set[str] = set()\n if deployer_addr and listing_date:\n try:\n push_airdrop_result = fetch_push_airdrop_recipients(\n ca, deployer_addr, trace_floor, listing_date,\n total_supply=float(total_supply) if total_supply else None,\n ) or {}\n except Exception as e:\n push_airdrop_result = {\"__ERR\": f\"detector exception: {str(e)[:120]}\",\n \"airdrop_recipients\": []}\n push_airdrop_set = {\n a.lower() for a in push_airdrop_result.get(\"airdrop_recipients\") or []\n if isinstance(a, str) and a\n }\n\n insider_addrs = sorted({\n (r.get(\"addr\") or \"\").lower() for r in receivers\n if r.get(\"addr\")\n and not (r.get(\"is_cex_custody\") or r.get(\"is_dex_infra\"))\n and (r.get(\"addr\") or \"\").lower() not in push_airdrop_set\n })\n # `supply` (the conservation ceiling for the cap + the %-supply denominator)\n # must be the REAL total supply — NOT a circulating fallback. circulating is\n # smaller than total, so using it as the \"total\" ceiling would over-cap the\n # floor downward and spuriously trip confirmed_capped (adversarial review HIGH #1). When\n # total_supply is unknown we simply do not cap (no reliable ceiling).\n supply = float(total_supply) if total_supply else None\n\n def _pct(x):\n return (x / circ * 100) if circ else None\n\n # TREE HOLDINGS prep — figure out which addresses need a balance fetch.\n # `missing` is computed UP-FRONT so the balance query can run alongside\n # the 3 confirmed-disposal queries in the parallel pool below.\n all_addrs = sorted({(r.get(\"addr\") or \"\").lower() for r in receivers if r.get(\"addr\")})\n known_bal = {\n (r.get(\"addr\") or \"\").lower(): float(r.get(\"current_balance\") or 0)\n for r in receivers if r.get(\"current_balance\") is not None\n }\n missing = [a for a in all_addrs if a not in known_bal]\n\n # v0.7.19: 4 independent surf SQL queries run in parallel (was serial,\n # ~270s on COLLECT). Each `_run_surf_with_retry` call handles its own\n # back-off so the parallel pool does NOT amplify retry pressure — it\n # just collapses 4× sequential waits into max-of-4. Empirically\n # COLLECT drops from 270s → ~80s.\n #\n # (a) insider → CEX-deposit (FLOOR component, gross flow capped later)\n # (b) insider OWN DEX swaps of CA (FLOOR component, directed exit)\n # (c) dex_sell_profile (median price + wash signal context)\n # (d) current_balances for receivers whose balance rule_11 did not pre-fill\n #\n # All four are independent — no inter-query data dependency.\n def _fetch_cex():\n return fetch_apparatus_to_cex(ca, insider_addrs, date_floor)\n\n def _fetch_dex_sold():\n return fetch_apparatus_dex_sold(ca, insider_addrs, date_floor)\n\n def _fetch_profile():\n return fetch_dex_sell_profile(ca, date_floor) or {}\n\n def _fetch_balances():\n return fetch_current_balances(ca, missing, date_floor) if missing else {}\n\n with concurrent.futures.ThreadPoolExecutor(\n max_workers=DUMP_TRACKER_WORKERS,\n thread_name_prefix=\"dump_tracker\",\n ) as _ex:\n _fut_cex = _ex.submit(_fetch_cex)\n _fut_dex = _ex.submit(_fetch_dex_sold)\n _fut_prof = _ex.submit(_fetch_profile)\n _fut_bal = _ex.submit(_fetch_balances)\n # Defensive: a worker that raises propagates via .result(); we\n # default each bucket to its empty-shape so a single bad surf\n # response cannot crash the whole dump_tracker section. The\n # buckets_complete flag downstream will trip if cex/dexb came\n # back with ok=False.\n try:\n cex = _fut_cex.result()\n except Exception as e:\n print(f\"[dump_tracker] cex fetch worker raised: {type(e).__name__}: {e}\",\n file=sys.stderr)\n cex = {\"cex_tokens\": 0.0, \"cex_labels\": [], \"ok\": False,\n \"error\": f\"{type(e).__name__}: {e}\"[:120]}\n try:\n dexb = _fut_dex.result()\n except Exception as e:\n print(f\"[dump_tracker] dex_sold fetch worker raised: {type(e).__name__}: {e}\",\n file=sys.stderr)\n dexb = {\"dex_sold_tokens\": 0.0, \"n_swaps\": 0, \"ok\": False,\n \"error\": f\"{type(e).__name__}: {e}\"[:120]}\n try:\n profile = _fut_prof.result()\n except Exception as e:\n print(f\"[dump_tracker] profile fetch worker raised: {type(e).__name__}: {e}\",\n file=sys.stderr)\n profile = {}\n try:\n fetched_bal = _fut_bal.result()\n except Exception as e:\n print(f\"[dump_tracker] balances fetch worker raised: {type(e).__name__}: {e}\",\n file=sys.stderr)\n fetched_bal = {}\n\n # audit H6 fix: detect surf failure via sentinel __ERR key.\n # fetch_dex_sell_profile() and fetch_current_balances() now return\n # {\"__ERR\": \u003creason>} on transport failure instead of a plain {}.\n # Without this check, a surf 429 storm silently zeroes wash_dominated\n # / pure_insider_holds / top_seller_addrs while the report says\n # \"buckets_complete: true\" — a false-clean forensic.\n profile_ok = not (isinstance(profile, dict) and \"__ERR\" in profile)\n if not profile_ok:\n profile_err = profile.get(\"__ERR\", \"unknown\")\n print(\n f\"[dump_tracker] dex_sell_profile surf-failed: {profile_err}\",\n file=sys.stderr,\n )\n # Replace the sentinel with an empty dict so downstream .get()\n # calls still work; the profile_ok flag captures the failure.\n profile = {}\n balances_ok = not (isinstance(fetched_bal, dict) and \"__ERR\" in fetched_bal)\n if not balances_ok:\n balances_err = fetched_bal.get(\"__ERR\", \"unknown\")\n print(\n f\"[dump_tracker] current_balances surf-failed: {balances_err}\",\n file=sys.stderr,\n )\n fetched_bal = {}\n\n cex_tokens = cex.get(\"cex_tokens\", 0.0)\n dex_tokens = dexb.get(\"dex_sold_tokens\", 0.0)\n confirmed_raw = cex_tokens + dex_tokens\n # v0.9.2: per-window splits. Source columns added in the SQL via\n # CASE WHEN block_date >= today() - N, so zero extra surf credits.\n # These let users reconcile against short-horizon alerts (e.g.\n # EmberCN-style \"过去 14 小时套现 $X\") without our number being\n # dominated by 364-day cumulative.\n cex_tokens_7d = cex.get(\"cex_tokens_7d\", 0.0)\n cex_tokens_30d = cex.get(\"cex_tokens_30d\", 0.0)\n dex_tokens_7d = dexb.get(\"dex_sold_tokens_7d\", 0.0)\n dex_tokens_30d = dexb.get(\"dex_sold_tokens_30d\", 0.0)\n dex_real_usd_7d = float(dexb.get(\"dex_sold_usd_7d\") or 0)\n dex_real_usd_30d = float(dexb.get(\"dex_sold_usd_30d\") or 0)\n # ok=False = surf threw past all retries → the bucket's 0 is unknown, not a\n # real zero; flag so the report says \"floor incomplete\" not a false low.\n # audit H6: extended from (cex + dexb) to also require profile +\n # balances. A surf failure on EITHER profile or balances zeroes\n # downstream forensic claims (wash_dominated false-negative, etc.).\n # v0.8.1 audit HIGH fix: include push_airdrop detector status.\n # If it errored, we fall through to keeping ALL pre_launch_receivers in\n # the insider set (fail-open to old false-positive). Mark untrusted so\n # the report can disclose the partial-completeness state.\n push_airdrop_ok = push_airdrop_result.get(\"__ERR\") is None or not deployer_addr\n buckets_complete = (\n bool(cex.get(\"ok\")) and bool(dexb.get(\"ok\"))\n and profile_ok and balances_ok and push_airdrop_ok\n )\n\n # TREE HOLDINGS (stock) — Σ current balance of the ENTIRE traced set (ALL\n # receivers, INCLUDING lockup/vesting and exit-infra-labelled wallets that\n # still hold a balance). This is the clean conservation anchor: balances\n # are stocks, never double-count, always ≤ total supply. total − tree_holds\n # = what physically LEFT the traced tree to end-buyers. (R2: tree holds 972M\n # = 97.3%; only ~27M left — the earlier 1013% was pure flow double-count.\n # Do NOT exclude exit-infra here: tokens sitting in a Binance-omnibus /\n # router wallet have not reached an end-buyer yet, so they are still \"in the\n # tree\" for the conservation bound — excluding them wrongly inflates \"left\".)\n tree_holds = sum(known_bal.get(a, fetched_bal.get(a, 0.0)) for a in all_addrs)\n\n # CONSERVATION CAP on the floor (stock guard). The (a) CEX bucket is a gross\n # directed flow; CEX deposit→sweep→re-deposit round-trips can inflate it\n # past what physically left the tree. The tree can only have SOLD what is no\n # longer in ANY traced wallet: max_left = total_supply − tree_holds (pure\n # stock, round-trip-immune). Cap confirmed-sold at that ceiling so the floor\n # stays physically valid. (R2: raw gross 135M > max_left 27M → capped; the\n # gross CEX flow was round-trip-inflated.)\n max_left = max(supply - tree_holds, 0.0) if supply else None\n confirmed_capped = (max_left is not None) and (confirmed_raw > max_left * 1.001)\n confirmed_total = min(confirmed_raw, max_left) if max_left is not None else confirmed_raw\n\n # Robust median DEX price (defeats amount_usd outliers) + wash signal.\n # `profile` was already filled in parallel above (v0.7.19); only the\n # median_px / wash-signal extraction happens here.\n # v1.2.5: bound median_px at the source — it is also decimals-broken for\n # non-18-decimal tokens (same surf dex_trades amount), and it feeds BOTH\n # confirmed_profit below AND the cex_effective_px fallback later. Bounding\n # here makes every downstream use sane.\n median_px = _sane_effective_price(profile.get(\"median_price_usd\"), spot_price_usd)\n confirmed_profit = (confirmed_total * median_px) if (median_px and confirmed_total) else None\n\n # v0.7.21.10: NET SELL OUT (Net Sell Out / 确认净卖出)\n #\n # Gross Sell Out (above, `confirmed_est_profit_usd`) = confirmed_total ×\n # median price. Two problems on a wash-dominated token:\n #\n # (1) median price across 200 top sellers is wash-inflatable — bot\n # atomic round-trips ship token at MEV-aggregator quote prices\n # that drag the per-seller median up; LAB ships median $2.91\n # vs the apparatus' actual TWAP $0.88 (3.3× over-estimate).\n #\n # (2) CEX deposit tokens are assumed to clear at median price even\n # though the apparatus never directly traded at that level —\n # they trade once they leave the exchange omnibus and we cannot\n # see that leg.\n #\n # Net Sell Out is the apparatus' best on-chain-evidenced estimate:\n #\n # - DEX leg: SUM(amount_usd) the apparatus themselves got on every\n # swap (real settlement, time-weighted by ClickHouse partition).\n # Free — fetched by the same SQL as `dex_sold_tokens` after\n # v0.7.21.10.\n #\n # - CEX leg: cex_tokens × apparatus DEX TWAP. The apparatus traded\n # the same token in the same window; their TWAP is the closest\n # time-weighted price we have. Falls back to median_px when DEX\n # leg is empty (apparatus never self-swapped → no TWAP), and to\n # None when both are unavailable.\n #\n # Net Sell Out is therefore strictly ≤ Gross Sell Out on\n # wash-dominated tokens (LAB: ≈ $131M vs Gross $389M) and ≈ equal on\n # clean tokens (no wash, TWAP ≈ median).\n dex_real_usd = float(dexb.get(\"dex_sold_usd\") or 0)\n dex_twap_px = dexb.get(\"dex_twap_usd_per_token\")\n # Effective price for the CEX leg: prefer the apparatus' own TWAP,\n # fall back to median_px (legacy estimator) when DEX leg is empty.\n cex_effective_px = _sane_effective_price(\n dex_twap_px if dex_twap_px else median_px, spot_price_usd)\n cex_value_usd = (cex_tokens * cex_effective_px) if (cex_effective_px and cex_tokens) else None\n if cex_value_usd is not None and dex_real_usd > 0:\n net_sellout_usd = cex_value_usd + dex_real_usd\n elif cex_value_usd is not None:\n net_sellout_usd = cex_value_usd\n elif dex_real_usd > 0:\n net_sellout_usd = dex_real_usd\n else:\n net_sellout_usd = None\n # v0.9.2: per-window Net Sell Out (same formula, just constrained to\n # the time bucket's CEX/DEX flows).\n cex_value_usd_7d = (cex_tokens_7d * cex_effective_px) if (cex_effective_px and cex_tokens_7d) else None\n cex_value_usd_30d = (cex_tokens_30d * cex_effective_px) if (cex_effective_px and cex_tokens_30d) else None\n def _net(_cex_usd, _dex_usd):\n if _cex_usd is not None and _dex_usd > 0:\n return _cex_usd + _dex_usd\n if _cex_usd is not None:\n return _cex_usd\n if _dex_usd > 0:\n return _dex_usd\n return None\n net_sellout_usd_7d = _net(cex_value_usd_7d, dex_real_usd_7d)\n net_sellout_usd_30d = _net(cex_value_usd_30d, dex_real_usd_30d)\n # Per-window totals (token-side) — capped same as cumulative.\n confirmed_total_7d = min(cex_tokens_7d + dex_tokens_7d, max_left or float(\"inf\")) if (cex_tokens_7d + dex_tokens_7d) > 0 else 0.0\n confirmed_total_30d = min(cex_tokens_30d + dex_tokens_30d, max_left or float(\"inf\")) if (cex_tokens_30d + dex_tokens_30d) > 0 else 0.0\n # Cap Net at Gross — Net should never exceed Gross in well-defined\n # cases (TWAP ≤ median in wash + TWAP > median is rare but possible\n # for clean tokens on a falling market). When Net > Gross by more\n # than 5% it almost certainly means the apparatus traded post-pump,\n # which is fine information but should be flagged as an upward\n # revision rather than a cap-breach.\n net_above_gross_pct = None\n if (confirmed_profit is not None and net_sellout_usd is not None\n and confirmed_profit > 0):\n net_above_gross_pct = (net_sellout_usd / confirmed_profit - 1.0) * 100.0\n\n tree_holds_pct_supply = (tree_holds / supply * 100) if supply else None\n\n # v0.7.19.4: split tree_holds (with lockup, conservation anchor) vs\n # pure_insider_holds (without lockup, narrative anchor). The old\n # field name `insider_holds_*` was semantically tree_holds — it\n # included Arkham-confirmed vesting / multisig / treasury / dex_infra\n # / cex_custody wallets, because the conservation cap math needs the\n # full stock (tokens sitting in a vesting contract have not reached\n # an end-buyer and so are still \"in the tree\" for the round-trip-\n # immune ceiling). But narrative templates paraphrased the field as\n # \"内幕方 N 钱包仍掌控 X% 总供应\", conflating vesting (public\n # schedule) with insider 潜伏 (opaque hand). User reading COLLECT\n # v0.7.19 caught the bug: vesting + Gnosis Safe at 80% supply were\n # narrated as \"insider hoarding 80%\" → misleading EXIT_IF_HOLDING\n # framing. Right fix: keep tree_holds as the conservation anchor\n # (used by max_left + confirmed_capped math), expose a SEPARATE\n # pure_insider_holds that excludes Arkham-labeled lockup wallets,\n # and have the render template cite pure_insider_holds in the\n # narrative table while keeping tree_holds in the conservation row.\n receivers_by_addr = {(r.get(\"addr\") or \"\").lower(): r for r in receivers}\n pure_insider_holds = 0.0\n for a in all_addrs:\n r = receivers_by_addr.get(a) or {}\n # Skip Arkham-confirmed lockup wallets — they are NOT insider\n # potential dump pressure, their release is public schedule.\n if r.get(\"is_protocol_lockup\"):\n continue\n # Exit-infra (cex_custody / dex_infra) wallets are destinations,\n # not insider holdings — tokens sitting in a Binance omnibus or\n # PancakeSwap router are not under insider control any more.\n if r.get(\"is_cex_custody\") or r.get(\"is_dex_infra\"):\n continue\n # v0.8.1: airdrop platform / claim distributor residuals are\n # earmarked for retail — un-claimed pool stock, not insider\n # potential dump pressure. is_protocol_lockup already covers\n # this when set by classifier (we routed is_airdrop_platform\n # into is_protocol_lockup), but be explicit for code clarity.\n if r.get(\"is_airdrop_platform\"):\n continue\n # v0.8.1: push-airdrop retail recipients — flagged by SQL\n # detector above. Their balance is retail-held, not insider\n # potential. Removed from BOTH insider_addrs (so (a)/(b) skip)\n # AND pure_insider_holds (so the潜伏抛压 % does not inflate).\n if a in push_airdrop_set:\n continue\n bal = known_bal.get(a, fetched_bal.get(a, 0.0))\n pure_insider_holds += bal\n pure_insider_holds_pct_supply = (\n (pure_insider_holds / supply * 100) if supply else None\n )\n\n return {\n \"insider_n_wallets\": len(insider_addrs),\n # CONFIRMED-SOLD LOWER BOUND (a)+(b) — the headline. % of circulating.\n \"confirmed_cex_tokens\": cex_tokens,\n \"confirmed_cex_pct\": _pct(cex_tokens),\n \"confirmed_cex_labels\": cex.get(\"cex_labels\", []),\n \"confirmed_dex_tokens\": dex_tokens,\n \"confirmed_dex_pct\": _pct(dex_tokens),\n \"confirmed_dex_swaps\": dexb.get(\"n_swaps\", 0),\n \"confirmed_total_tokens\": confirmed_total,\n \"confirmed_total_pct\": _pct(confirmed_total),\n # v0.7.21.10: Gross Sell Out — historical name `confirmed_est_profit_usd`\n # kept for back-compat (Field-Authority locked field name); the render\n # template + i18n now label it \"Gross Sell Out (确认毛卖出)\".\n \"confirmed_est_profit_usd\": confirmed_profit,\n # v0.7.21.10: Net Sell Out — apparatus' time-weighted estimate.\n # DEX leg from real SUM(amount_usd); CEX leg from cex_tokens × DEX TWAP.\n # Strictly ≤ Gross on wash-dominated tokens (LAB: ≈ $131M vs $389M).\n \"confirmed_net_sellout_usd\": net_sellout_usd,\n # v0.9.2: per-window splits. EmberCN-style short-horizon alerts\n # reconcile against the 7d/30d numbers; the cumulative remains as\n # the long-horizon \"this token has been getting harvested\" signal.\n # Numbers are subsets of the cumulative: _7d ≤ _30d ≤ cumulative.\n \"confirmed_net_sellout_usd_7d\": net_sellout_usd_7d,\n \"confirmed_net_sellout_usd_30d\": net_sellout_usd_30d,\n \"confirmed_cex_tokens_7d\": cex_tokens_7d,\n \"confirmed_cex_tokens_30d\": cex_tokens_30d,\n \"confirmed_dex_tokens_7d\": dex_tokens_7d,\n \"confirmed_dex_tokens_30d\": dex_tokens_30d,\n \"confirmed_dex_real_usd_7d\": dex_real_usd_7d if dex_real_usd_7d > 0 else None,\n \"confirmed_dex_real_usd_30d\": dex_real_usd_30d if dex_real_usd_30d > 0 else None,\n \"confirmed_total_tokens_7d\": confirmed_total_7d,\n \"confirmed_total_tokens_30d\": confirmed_total_30d,\n \"confirmed_total_pct_7d\": _pct(confirmed_total_7d),\n \"confirmed_total_pct_30d\": _pct(confirmed_total_30d),\n # Breakdown components so the render template (and any audit reader)\n # can show the user how the Net number was constructed without\n # re-deriving from the raw SQL.\n \"confirmed_dex_real_usd\": dex_real_usd if dex_real_usd > 0 else None,\n \"confirmed_cex_estimated_usd\": cex_value_usd,\n # v1.2.5: report the EFFECTIVE per-token price actually used (spot-bounded),\n # not the raw DEX twap — otherwise a decimals-broken $243B/token leaks into\n # the rendered net-sellout line even though the USD value is now sane.\n \"apparatus_dex_twap_usd_per_token\": cex_effective_px,\n # Diagnostic only — Net should be \u003c Gross on wash tokens; > Gross\n # signals the apparatus traded into a price spike (rare but valid).\n \"net_above_gross_pct\": net_above_gross_pct,\n # True = floor hit the stock conservation ceiling (total − holdings):\n # the gross CEX/DEX flow exceeded what physically left insider wallets\n # (round-trips), so the floor was clamped down to stay valid.\n \"confirmed_capped\": confirmed_capped,\n # v0.7.19.4: TREE holdings (stock, ALL traced wallets incl. lockup +\n # exit-infra) — conservation anchor for `max_left` / confirmed_capped.\n # Used by render's \"内幕树当前持有 (含未解锁锁仓)\" row.\n \"tree_holds_tokens\": tree_holds,\n \"tree_holds_pct_supply\": tree_holds_pct_supply,\n # v0.7.19.4: PURE insider holdings (stock, EXCLUDING Arkham-confirmed\n # protocol_lockup + cex_custody + dex_infra) — narrative anchor.\n # Used by render's \"纯内幕当前持有 (不含 vesting / 多签 / 中转)\" row\n # and by the verdict one-liner, so the report no longer paraphrases\n # vesting as \"insider 抛压\".\n \"pure_insider_holds_tokens\": pure_insider_holds,\n \"pure_insider_holds_pct_supply\": pure_insider_holds_pct_supply,\n # v0.7.19.4 backward-compat alias for any external consumer (none\n # known in-tree): keep the old name pointing at tree_holds so a\n # consumer that didn't read the changelog still gets the\n # conservation-anchor value. Internal render template + skeleton\n # writer use the new explicit names.\n \"insider_holds_tokens\": tree_holds,\n \"insider_holds_pct_supply\": tree_holds_pct_supply,\n \"median_price_usd\": median_px,\n # Context: is the token's DEX selling bot-laundered?\n \"wash_dominated\": profile.get(\"wash_dominated\", False),\n \"n_dex_sellers\": profile.get(\"n_sellers\", 0),\n \"total_dex_swaps\": profile.get(\"total_swaps\", 0),\n \"top_seller_swaps\": profile.get(\"top_seller_swaps\", 0),\n # v0.7.21: expose the seller address list (up to 200, sorted by\n # swap count DESC) so flow_operator_detector reuses dump_tracker's\n # already-paid SQL instead of refetching. Empty when DEX profile\n # fetch failed.\n \"top_seller_addrs\": profile.get(\"top_seller_addrs\", []),\n # False = a confirmation query failed past all retries → floor may undercount.\n \"buckets_complete\": buckets_complete,\n # v0.8.1: push airdrop detector audit trail. n=0 means no batch\n # transfer in the pre-launch window passed the threshold (project\n # did not push-airdrop, or did it through a claim contract instead\n # — claim contracts trigger is_airdrop_platform below). Diagnostic\n # so the report can show \"N retail recipients excluded from insider\n # set, M tokens diverted\" and the user can sanity-check the call.\n \"push_airdrop_n_recipients\": int(push_airdrop_result.get(\"n_recipients\") or 0),\n \"push_airdrop_n_tx\": int(push_airdrop_result.get(\"n_tx\") or 0),\n \"push_airdrop_tokens_total\": float(push_airdrop_result.get(\"tokens_airdropped_total\") or 0.0),\n \"push_airdrop_threshold_used\": int(push_airdrop_result.get(\"_threshold_used\") or 0),\n \"push_airdrop_max_recipients_per_tx\": int(push_airdrop_result.get(\"max_recipients_per_tx\") or 0),\n \"push_airdrop_error\": push_airdrop_result.get(\"__ERR\"),\n # v0.8.1: airdrop-platform (third-party distribution) count among\n # receivers. >0 = at least one Arkham-labeled launchpad / task /\n # SaaS contract held un-claimed retail stock; its balance was\n # removed from pure_insider_holds.\n \"airdrop_platform_n_wallets\": sum(\n 1 for r in receivers if r.get(\"is_airdrop_platform\")\n ),\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":50993,"content_sha256":"d6ca182e4497390fb3000295cd657023a19e6c2219ebdbc1cf61a985ffd5c7e6"},{"filename":"alpha/v06/helpers/evidence_graph.py","content":"#!/usr/bin/env python3\n\"\"\"evidence_graph.py — canonical evidence dict with stable provenance IDs.\n\nThe evidence graph is the SOURCE OF TRUTH for all locked facts in a v0.6\nforensic report. Pipeline populates it during data collection; LLM narrative\nfields reference entries by ID (e.g. `evt_017`); validator looks up the ID\nand checks narrative claims for consistency.\n\nWhy this exists (cross-LLM audit 2026-05-24 finding #1):\n> \"No canonical evidence-linking model. LLM can write correct-looking\n> narrative not traceable to locked facts. PoC: anomaly.waves references\n> '$7.4M routed' but points to wrong tx/time; passes R3 string match.\"\n\nThe evidence graph + provenance IDs in waves.events[].evt_ref close this hole.\n\n## ID schema\n\nStable IDs are assigned in insertion order within their type prefix:\n- `evt_NNN` — temporal events (mint, deployer outflow, recent transfer, CEX hop)\n- `m6_NNN` — confirmed insider rows (Rule 11 quiet + M6 window accumulators)\n- `node_NNN` — lineage flowchart nodes (deployer / pool / router / etc.)\n- `mon_NNN` — monitoring wallet entries\n- `anc_NNN` — decision anchors\n\nID format is `\u003cprefix>_\u003c3-digit-zero-padded>`, guaranteed unique within graph.\nIDs are byte-stable across runs given byte-identical input data.\n\n## Usage\n\n```python\nfrom helpers.evidence_graph import EvidenceGraph\ng = EvidenceGraph()\n\nevt = g.add_event(\n type=\"mint\",\n ts=1772109008,\n amount=850000000,\n to_addr=\"0x70554...\",\n tx_hash=\"0x...\",\n)\n# evt == \"evt_001\"\n\n# Reference in wave event:\nwaves[0][\"events\"].append({\n \"evt_ref\": evt,\n \"nature\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n})\n\n# Serialize to report_data.json:\ndata[\"evidence_graph\"] = g.to_dict()\n```\n\nv0.6 (2026-05-24, cross-LLM audit condition.)\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\n\n# Allowed event types per prefix (closed enum — validator rejects unknown).\n_EVENT_TYPES = {\n \"mint\",\n \"deployer_outflow\",\n \"recent_transfer\",\n \"cex_hop\",\n \"dex_swap\",\n \"lp_add\",\n \"lp_remove\",\n}\n\n_M6_TYPES = {\n \"rule11_quiet\",\n \"rule11_partial_dumper\",\n \"rule11_full_dumper\",\n \"m6_window_accum_then_sold\",\n \"m6_window_accum_held\",\n # v0.7.9 (re-applied from v0.7.8 revert): recursive m6 expansion —\n # sub-receiver promoted via rule_11 trace at depth >= 2 (e.g. ESPORTS\n # main distributor 0x94111012 receiving 100M from 1st-level dumper\n # 0x1552160b on 5/19).\n \"rule11_sub_dumper\",\n}\n\n_NODE_TYPES = {\n \"deployer\",\n \"dumper\",\n \"quiet_wallet\",\n \"active_holder\",\n \"lp_pool\",\n \"router\",\n \"cex_hot_wallet\",\n \"cex_cold_wallet\",\n \"cex_deposit\",\n \"retail_fanout\",\n \"multisig_project\",\n}\n\n_ANCHOR_TYPES = {\n \"alpha_tier\",\n \"supply_chain\",\n \"price_vs_tge\",\n \"circulating_ratio\",\n \"alpha_5pct_depth\",\n \"dex_pool_depth\",\n \"rule11_dumped_pct\",\n \"rule11_quiet_balance_usd\",\n \"m6_confirmed_count\",\n \"lp_24h_net_flow_pct\",\n \"binance_perp_status\",\n \"spot_graduation_status\",\n}\n\n\n@dataclass\nclass EvidenceGraph:\n \"\"\"Append-only, ID-stable evidence registry. NOT thread-safe by design —\n the v0.6 pipeline runs single-process per report.\n \"\"\"\n\n _counters: dict[str, int] = field(default_factory=lambda: {\n \"evt\": 0, \"m6\": 0, \"node\": 0, \"mon\": 0, \"anc\": 0,\n })\n _store: dict[str, dict[str, Any]] = field(default_factory=dict)\n\n def _next_id(self, prefix: str) -> str:\n if prefix not in self._counters:\n raise ValueError(f\"unknown evidence prefix {prefix!r}\")\n self._counters[prefix] += 1\n return f\"{prefix}_{self._counters[prefix]:03d}\"\n\n # ----- Temporal events (evt_NNN) -----\n\n def add_event(\n self,\n *,\n type: str,\n ts: int | str,\n amount: float | None = None,\n from_addr: str | None = None,\n to_addr: str | None = None,\n tx_hash: str | None = None,\n usd_value: float | None = None,\n notes: str | None = None,\n ) -> str:\n \"\"\"Add a temporal event (mint / deployer_outflow / recent_transfer /\n cex_hop / dex_swap / lp_add / lp_remove). Returns evidence ID.\n\n `ts` should be Unix seconds (int) or ISO 'YYYY-MM-DD HH:MM' string.\n `amount` is in token units (NOT raw wei). `usd_value` optional.\n \"\"\"\n if type not in _EVENT_TYPES:\n raise ValueError(f\"invalid event type {type!r} (allowed: {_EVENT_TYPES})\")\n eid = self._next_id(\"evt\")\n self._store[eid] = {\n \"kind\": \"event\",\n \"type\": type,\n \"ts\": ts,\n \"amount\": amount,\n \"from_addr\": from_addr,\n \"to_addr\": to_addr,\n \"tx_hash\": tx_hash,\n \"usd_value\": usd_value,\n \"notes\": notes,\n }\n return eid\n\n # ----- M6 confirmed insiders (m6_NNN) -----\n\n def add_m6(\n self,\n *,\n type: str,\n addr: str,\n received: float,\n balance: float | None,\n dumped_pct: float | None,\n source_section: str, # 'rule_11' | 'm6_window' | 'rule_11_depth_N'\n ts_received: int | str | None = None,\n ts_first_dump: int | str | None = None,\n usd_value: float | None = None,\n ) -> str:\n if type not in _M6_TYPES:\n raise ValueError(f\"invalid m6 type {type!r}\")\n eid = self._next_id(\"m6\")\n self._store[eid] = {\n \"kind\": \"m6\",\n \"type\": type,\n \"addr\": addr.lower(),\n \"received\": received,\n \"balance\": balance,\n \"dumped_pct\": dumped_pct,\n \"source_section\": source_section,\n \"ts_received\": ts_received,\n \"ts_first_dump\": ts_first_dump,\n \"usd_value\": usd_value,\n }\n return eid\n\n # ----- Lineage flowchart nodes (node_NNN) -----\n\n def add_node(\n self,\n *,\n type: str,\n addr: str,\n balance: float | None = None,\n usd_value: float | None = None,\n label_hint: str | None = None,\n ) -> str:\n if type not in _NODE_TYPES:\n raise ValueError(f\"invalid node type {type!r}\")\n eid = self._next_id(\"node\")\n self._store[eid] = {\n \"kind\": \"node\",\n \"type\": type,\n \"addr\": addr.lower(),\n \"balance\": balance,\n \"usd_value\": usd_value,\n \"label_hint\": label_hint,\n }\n return eid\n\n # ----- Monitoring wallet entries (mon_NNN) -----\n\n def add_monitoring(\n self,\n *,\n addr: str,\n role: str,\n status_emoji: str, # 🔴🟠🟡🟢⚪\n evt_refs: list[str] | None = None, # links back to events that triggered monitoring\n m6_ref: str | None = None,\n ) -> str:\n eid = self._next_id(\"mon\")\n self._store[eid] = {\n \"kind\": \"monitoring\",\n \"addr\": addr.lower(),\n \"role\": role,\n \"status_emoji\": status_emoji,\n \"evt_refs\": evt_refs or [],\n \"m6_ref\": m6_ref,\n }\n return eid\n\n # ----- Decision anchors (anc_NNN) -----\n\n def add_anchor(\n self,\n *,\n type: str,\n value: Any, # numeric or string depending on type\n status_emoji: str,\n derivation: str, # short text explaining how the value was computed\n ) -> str:\n if type not in _ANCHOR_TYPES:\n raise ValueError(f\"invalid anchor type {type!r}\")\n eid = self._next_id(\"anc\")\n self._store[eid] = {\n \"kind\": \"anchor\",\n \"type\": type,\n \"value\": value,\n \"status_emoji\": status_emoji,\n \"derivation\": derivation,\n }\n return eid\n\n # ----- Lookup + serialization -----\n\n def lookup(self, eid: str) -> dict[str, Any] | None:\n return self._store.get(eid)\n\n def all_ids(self) -> list[str]:\n return list(self._store.keys())\n\n def by_kind(self, kind: str) -> dict[str, dict[str, Any]]:\n return {eid: e for eid, e in self._store.items() if e.get(\"kind\") == kind}\n\n def by_event_type(self, event_type: str) -> dict[str, dict[str, Any]]:\n return {\n eid: e for eid, e in self._store.items()\n if e.get(\"kind\") == \"event\" and e.get(\"type\") == event_type\n }\n\n def to_dict(self) -> dict[str, dict[str, Any]]:\n \"\"\"Serialize for report_data.json. Sorted by ID for byte-stable output.\"\"\"\n return {k: dict(v) for k, v in sorted(self._store.items())}\n\n def count_by_kind(self) -> dict[str, int]:\n c = {}\n for e in self._store.values():\n c[e[\"kind\"]] = c.get(e[\"kind\"], 0) + 1\n return c\n\n # ----- Validation helpers (used by validate_report_data.py) -----\n\n def event_matches_narrative(\n self,\n evt_ref: str,\n narrative: str,\n amount_tolerance_pct: float = 5.0,\n ) -> tuple[bool, str]:\n \"\"\"Check if a narrative string is consistent with the referenced event.\n\n Used by R3-causal: when LLM writes `events[].nature = \"...\"`, validator\n calls this. Returns (ok, reason). Currently checks:\n - If narrative mentions a $ amount, must be within amount_tolerance_pct\n of evt.usd_value (or evt.amount if usd_value is None).\n - If narrative mentions an address fragment (0xABCD…EFGH or 0xABCD),\n must match evt.from_addr or evt.to_addr.\n\n Returns (True, \"\") on match, (False, reason) on mismatch.\n \"\"\"\n evt = self.lookup(evt_ref)\n if evt is None:\n return False, f\"evt_ref={evt_ref!r} not found in evidence_graph\"\n if evt.get(\"kind\") != \"event\":\n return False, f\"evt_ref={evt_ref!r} is a {evt.get('kind')!r}, not an event\"\n\n import re\n\n # Extract any $ amounts from narrative (e.g. \"$7.4M\", \"$1.2k\", \"$284k\")\n amounts_in_narrative = []\n for m in re.finditer(r\"\\$([\\d.,]+)\\s*([kMB])?\", narrative):\n num = float(m.group(1).replace(\",\", \"\"))\n unit = m.group(2)\n multiplier = {\"k\": 1e3, \"M\": 1e6, \"B\": 1e9, None: 1.0}.get(unit, 1.0)\n amounts_in_narrative.append(num * multiplier)\n\n # If narrative mentions amounts, at least one must be close to evt amount\n if amounts_in_narrative:\n evt_amt = evt.get(\"usd_value\") or 0\n if evt_amt > 0:\n tol = evt_amt * (amount_tolerance_pct / 100.0)\n if not any(abs(a - evt_amt) \u003c= max(tol, evt_amt * 0.05) for a in amounts_in_narrative):\n return False, (\n f\"evt_ref={evt_ref!r} narrative mentions amounts \"\n f\"{amounts_in_narrative} but event usd_value={evt_amt}, \"\n f\"no match within {amount_tolerance_pct}% tolerance\"\n )\n\n # Extract any 0x addresses or address fragments\n for m in re.finditer(r\"0x[a-fA-F0-9]{4,}\", narrative):\n frag = m.group(0).lower()\n from_match = (evt.get(\"from_addr\") or \"\").lower().startswith(frag)\n to_match = (evt.get(\"to_addr\") or \"\").lower().startswith(frag)\n if not (from_match or to_match):\n # Allow short fragments to match middle of address too\n if len(frag) >= 6: # 0x + 4 hex\n if not (\n frag in (evt.get(\"from_addr\") or \"\").lower()\n or frag in (evt.get(\"to_addr\") or \"\").lower()\n ):\n return False, (\n f\"evt_ref={evt_ref!r} narrative mentions address \"\n f\"{frag!r} but event from={evt.get('from_addr')} \"\n f\"to={evt.get('to_addr')}, no match\"\n )\n\n return True, \"\"\n\n\ndef build_empty() -> EvidenceGraph:\n \"\"\"Convenience factory for tests.\"\"\"\n return EvidenceGraph()\n\n\nif __name__ == \"__main__\":\n # Quick self-test\n g = EvidenceGraph()\n e1 = g.add_event(type=\"mint\", ts=1772109008, amount=850_000_000, to_addr=\"0x70554000\")\n e2 = g.add_event(\n type=\"recent_transfer\",\n ts=\"2026-05-23 01:34\",\n amount=6_063_612,\n from_addr=\"0xf440139a\",\n to_addr=\"0xf89d7b9c\",\n usd_value=7_402_435,\n )\n m1 = g.add_m6(\n type=\"rule11_quiet\",\n addr=\"0x9e81a6b3\",\n received=101_387_472,\n balance=101_387_472,\n dumped_pct=0.0,\n source_section=\"rule_11\",\n )\n print(f\"IDs: {g.all_ids()}\")\n print(f\"Count by kind: {g.count_by_kind()}\")\n\n # Test narrative match\n ok, why = g.event_matches_narrative(e2, \"约 $7.40M tokens routed via 0xf440 → 0xf89d\")\n print(f\"Match ok={ok} why={why!r}\")\n bad, why2 = g.event_matches_narrative(e2, \"约 $1.2M routed elsewhere\")\n print(f\"Bad ok={bad} why={why2!r}\")\n print(\"evidence_graph.py self-test PASS\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12919,"content_sha256":"9a4c8115775c66ec97da0e5181d790d782941a99c600b9e365eff92f445736d6"},{"filename":"alpha/v06/helpers/fake_mining_detector.py","content":"#!/usr/bin/env python3\n\"\"\"fake_mining_detector.py — distinguish real mining tokens from\n\"fake mining\" operator clusters that use mint-contract distribution as\ncamouflage for wholesale token allocation.\n\n# Why this exists (v0.8.2)\n\nJCT (Janction) review surfaced the case: a token can present itself as\nmining-distributed (mint_authorities exist, mint contract has large\noutflows to many addresses) but actually be an operator-cluster\ndistribution — mint contract → ~10-50 operator-controlled EOAs → DEX\nsell or warehouse. The generic \"mining token\" disjoint accounting then\ninflates the \"non-operator pressure\" bucket by including the cluster\nEOAs as if they were retail miners.\n\nReal mining (e.g. Bitcoin pool distributions, established staking\nrewards) has a power-law distribution: thousands of recipient\naddresses, small per-recipient amounts (\u003c 0.01% supply per reward),\nmany transactions per recipient, identifiable mining pool Arkham labels\non a meaningful fraction of them.\n\nFake mining operator clusters have the opposite shape: \u003c 50\nrecipients, 0.5-3% supply per recipient, 1-3 transactions per\nrecipient, all UNLABELED.\n\n# Heuristic (5 signals, ≥2 required to flag fake)\n\n| # | Signal | Real mining | Fake mining (operator cluster) |\n|---|---|---|---|\n| 1 | distinct destinations per mint authority | ≥ 500 | ≤ 50 |\n| 2 | mean tokens received per destination as % total supply | \u003c 0.01% | ≥ 0.5% |\n| 3 | mean tx count per destination | ≥ 100 (continuous small rewards) | ≤ 10 (large lump sums) |\n| 4 | fraction of UNLABELED destinations | ≤ 50% (mining pools labeled) | ≥ 80% |\n| 5 | top-10 destination share of total minted | \u003c 20% (power-law tail dominates) | ≥ 80% (oligarchy concentration) |\n\nConservative gate: ≥ 2 signals must trip per mint authority. Across\nmultiple authorities, the verdict is the union — if any authority\ntrips, the whole token is flagged.\n\n# Effect on disjoint bucket accounting\n\nWhen `is_fake_mining_distribution=True`:\n - mint contract reserve (current balance) → moved to operator-ammo\n bucket (project-controlled, not retail-bound supply).\n - Already-distributed tokens (mint - reserve) → most of it landed in\n operator cluster (not real miners), should be excluded from\n \"non-operator pressure\" upper-bound estimate.\n\nWhen `is_fake_mining_distribution=False` AND mint_authorities exist:\n - mint contract reserve → separate \"mid-term supply pressure\" bucket\n (not immediate sell pressure, but dilutes future pump windows).\n - Already-distributed tokens → mostly in real miner wallets, retains\n immediate sell pressure status.\n\n# Caveats\n\n- A token can have one fake-mining mint authority + one real-mining\n one (rare hybrid). Per-authority flag is preserved so caller can\n refine accounting per authority.\n- 5 signals collapse to top-destination shape only — the detector does\n NOT chase funding-source clusters (a stricter detector would verify\n the destinations also share a common funder, but that needs an extra\n surf query per destination).\n- Threshold values chosen conservative on the \"real mining\" side to\n avoid mis-flagging genuine mining pool distributions. Calibration\n against more tokens (real and fake) is a v0.8.3 backlog item.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\n# Thresholds — documented in docstring above\nFAKE_MINING_MAX_DEST_COUNT = 50\nFAKE_MINING_MIN_MEAN_PCT_SUPPLY = 0.5 # ≥ 0.5% supply per dest\nFAKE_MINING_MAX_MEAN_TX_COUNT = 10\nFAKE_MINING_MIN_UNLABELED_FRAC = 0.80\nFAKE_MINING_MIN_TOP10_SHARE = 0.80\nFAKE_MINING_MIN_SIGNALS = 2\n\n# Real mining (positive identification) — used to flag genuine mining\n# tokens so the disjoint accounting can apply mid-term-supply-pressure\n# semantics correctly.\nREAL_MINING_MIN_DEST_COUNT = 500\nREAL_MINING_MAX_MEAN_PCT_SUPPLY = 0.01 # \u003c 0.01% per dest\n\n\ndef classify_mining_authority(\n auth_data: dict[str, Any],\n total_supply: float | None = None,\n) -> dict[str, Any]:\n \"\"\"Classify a single mint authority by its outflow distribution.\n\n Args:\n auth_data: per-authority entry from\n `funding_attribution.mint_authority_dumps.per_addr[addr]`. We\n use:\n - `top_destinations`: [{dest, amt, n_tx, arkham_label,\n arkham_classification}, ...]\n - `total_minted` (optional, for top-10-share calc)\n total_supply: token total supply (for pct calc). When None,\n per-recipient % signal is skipped — fewer signals available\n but other 4 still gate.\n\n Returns:\n {\n \"n_destinations_visible\": int,\n \"mean_pct_supply_per_dest\": float | None,\n \"mean_n_tx_per_dest\": float,\n \"unlabeled_fraction\": float,\n \"top10_share_of_total\": float | None,\n \"signals_tripped\": [str], # subset of 5 signal names\n \"n_signals_tripped\": int,\n \"is_fake_mining_cluster\": bool,\n \"is_real_mining_pool\": bool,\n }\n \"\"\"\n dests = auth_data.get(\"top_destinations\") or []\n n_visible = len(dests)\n # v0.8.2.2 audit HIGH #1 fix: prefer the SQL-emitted\n # `n_destinations_total` (true count before Python-side [:5] slice)\n # for the low-count signal. When absent (legacy skeleton), the signal\n # is marked indeterminate and not tripped.\n n_destinations_true = auth_data.get(\"n_destinations_total\")\n total_minted = float(auth_data.get(\"total_minted\") or 0)\n\n # Compute distributions\n if n_visible == 0:\n return {\n \"n_destinations_visible\": 0,\n \"mean_pct_supply_per_dest\": None,\n \"mean_n_tx_per_dest\": 0,\n \"unlabeled_fraction\": 0,\n \"top10_share_of_total\": None,\n \"signals_tripped\": [],\n \"n_signals_tripped\": 0,\n \"is_fake_mining_cluster\": False,\n \"is_real_mining_pool\": False,\n }\n\n total_amt = sum(float(d.get(\"amt\") or 0) for d in dests)\n mean_amt = total_amt / n_visible if n_visible else 0\n mean_pct_supply = (mean_amt / total_supply * 100) if total_supply else None\n mean_n_tx = sum(int(d.get(\"n_tx\") or 0) for d in dests) / n_visible if n_visible else 0\n unlabeled = sum(\n 1 for d in dests\n if not d.get(\"arkham_label\")\n and (d.get(\"arkham_classification\") or \"UNLABELED\") == \"UNLABELED\"\n )\n unlabeled_frac = unlabeled / n_visible if n_visible else 0\n\n # Top 10 share — Phase 2 surf returns sorted descending so first 10\n # are largest. If total_minted available, use it as denominator;\n # otherwise use sum of visible (less accurate but available).\n top10_share = None\n if dests:\n top10_amt = sum(float(d.get(\"amt\") or 0) for d in dests[:10])\n denom = total_minted if total_minted else total_amt\n if denom > 0:\n top10_share = top10_amt / denom\n\n # Trip signals\n signals: list[str] = []\n # v0.8.2.2 audit HIGH #1 fix: signal 1 must gate on the TRUE\n # destination count, not the [:5] sliced view. n_destinations_total\n # is emitted by funding_source_attribution v0.8.2.2+. When absent\n # (legacy skeleton), we DON'T trip — better to under-flag than to\n # systematically false-positive on real mining tokens.\n if (n_destinations_true is not None\n and n_destinations_true \u003c= FAKE_MINING_MAX_DEST_COUNT):\n signals.append(\"low_destination_count\")\n if mean_pct_supply is not None and mean_pct_supply >= FAKE_MINING_MIN_MEAN_PCT_SUPPLY:\n signals.append(\"high_mean_pct_per_dest\")\n if mean_n_tx \u003c= FAKE_MINING_MAX_MEAN_TX_COUNT:\n signals.append(\"low_mean_tx_per_dest\")\n if unlabeled_frac >= FAKE_MINING_MIN_UNLABELED_FRAC:\n signals.append(\"high_unlabeled_fraction\")\n if top10_share is not None and top10_share >= FAKE_MINING_MIN_TOP10_SHARE:\n signals.append(\"top10_oligarchy\")\n\n is_fake = len(signals) >= FAKE_MINING_MIN_SIGNALS\n\n # v0.8.2.2 audit MED #1 fix: positive real-mining ID requires\n # the TRUE destination count (n_destinations_total), not the visible\n # top-5 slice. Until SQL surfaces a count > REAL_MINING_MIN_DEST_COUNT\n # we cannot positively identify any token as real mining. The fix is\n # to gate on n_destinations_true; legacy skeletons (no field) get\n # `is_real_mining_pool=False` rather than always-False.\n is_real = (\n n_destinations_true is not None\n and n_destinations_true >= REAL_MINING_MIN_DEST_COUNT\n and (mean_pct_supply is not None and mean_pct_supply \u003c REAL_MINING_MAX_MEAN_PCT_SUPPLY)\n and mean_n_tx > FAKE_MINING_MAX_MEAN_TX_COUNT\n and unlabeled_frac \u003c FAKE_MINING_MIN_UNLABELED_FRAC\n )\n\n return {\n \"n_destinations_visible\": n_visible,\n \"n_destinations_total\": n_destinations_true, # may be None on legacy\n \"mean_pct_supply_per_dest\": mean_pct_supply,\n \"mean_n_tx_per_dest\": mean_n_tx,\n \"unlabeled_fraction\": unlabeled_frac,\n \"top10_share_of_total\": top10_share,\n \"signals_tripped\": signals,\n \"n_signals_tripped\": len(signals),\n \"is_fake_mining_cluster\": is_fake,\n \"is_real_mining_pool\": is_real,\n }\n\n\ndef classify_token_mining_mode(\n mint_authority_dumps: dict[str, Any],\n total_supply: float | None = None,\n) -> dict[str, Any]:\n \"\"\"Aggregate per-authority classifications into a single token-level\n flag.\n\n Args:\n mint_authority_dumps: skeleton `funding_attribution.mint_authority_dumps`.\n total_supply: token total supply.\n\n Returns:\n {\n \"is_mining_token\": bool, # any mint authorities exist\n \"is_fake_mining_distribution\": bool, # any authority looks fake\n \"is_real_mining_distribution\": bool, # all authorities look real\n \"per_authority_classifications\": {addr: per_auth_result, ...},\n \"n_authorities_total\": int,\n \"n_fake_authorities\": int,\n \"n_real_authorities\": int,\n }\n \"\"\"\n if not mint_authority_dumps:\n return {\n \"is_mining_token\": False,\n \"is_fake_mining_distribution\": False,\n \"is_real_mining_distribution\": False,\n \"per_authority_classifications\": {},\n \"n_authorities_total\": 0,\n \"n_fake_authorities\": 0,\n \"n_real_authorities\": 0,\n }\n per_addr = mint_authority_dumps.get(\"per_addr\") or {}\n per_class: dict[str, dict] = {}\n n_fake = 0\n n_real = 0\n for addr, data in per_addr.items():\n res = classify_mining_authority(data, total_supply=total_supply)\n per_class[addr] = res\n if res[\"is_fake_mining_cluster\"]:\n n_fake += 1\n if res[\"is_real_mining_pool\"]:\n n_real += 1\n\n n_total = len(per_addr)\n return {\n \"is_mining_token\": n_total > 0,\n \"is_fake_mining_distribution\": n_fake > 0,\n \"is_real_mining_distribution\": n_real == n_total and n_total > 0,\n \"per_authority_classifications\": per_class,\n \"n_authorities_total\": n_total,\n \"n_fake_authorities\": n_fake,\n \"n_real_authorities\": n_real,\n }\n\n\n__all__ = [\n \"classify_mining_authority\",\n \"classify_token_mining_mode\",\n \"FAKE_MINING_MAX_DEST_COUNT\",\n \"FAKE_MINING_MIN_MEAN_PCT_SUPPLY\",\n \"FAKE_MINING_MAX_MEAN_TX_COUNT\",\n \"FAKE_MINING_MIN_UNLABELED_FRAC\",\n \"FAKE_MINING_MIN_TOP10_SHARE\",\n \"FAKE_MINING_MIN_SIGNALS\",\n \"REAL_MINING_MIN_DEST_COUNT\",\n \"REAL_MINING_MAX_MEAN_PCT_SUPPLY\",\n]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11427,"content_sha256":"b6499e090cd654a4d4d4e1ef47abd7eb80ddaa90a128996a06c5eaa5a492a4fd"},{"filename":"alpha/v06/helpers/flow_operator_detector.py","content":"#!/usr/bin/env python3\n\"\"\"flow_operator_detector.py — v0.7.21 flow-based wallet detector.\n\nCaptures two blind spots that v0.7.20.2 misses:\n\n 1. Cross-Alpha trading operator — single EOA running the same\n strategy across many Alpha tokens; low single-token balance, high\n tx frequency. cross_sym detector is stock-based (top-100 holders ×\n multiple Alpha tokens), so a wallet with 0.005% of supply but 2k+\n txs is invisible there.\n\n 2. Single-operator asynchronous wash (P → X → Q on separate txs) —\n two counterparties dominate flow, in ≈ out, but each leg is its\n own tx. wash_infra detector requires atomic_pair_ratio ≥ 0.85\n (both legs in one tx_hash), so the async pattern is invisible.\n\nBoth share the primary signal `tx_from_diversity \u003c 0.05` (one upstream\nsigner drives every interaction with the wallet). Sub-class then splits\non counterparty structure and cross-Alpha activity.\n\nSee `v06/v0721_DESIGN.md` for the full spec, threshold rationale,\nbudget projection, and regression test list.\n\n## Budget\n\nThree batch SQL queries + one Arkham label batch. ~20-25 seconds\nwall-clock and ~30-40 surf credits on top of v0.7.20.2's ~480 baseline.\nTotal report cost stays under $5.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent))\nfrom chain_router import ( # v0.7.20 / .21.7\n transfers_table, get_active_chain, chain_lock,\n is_valid_addr as _chain_is_valid_addr,\n)\n\n# v0.7.21.7: kept for back-compat callers that referenced this constant\n# directly; the SQL-injection guard inside `detect` uses the chain-aware\n# `_chain_is_valid_addr` so Solana base58 candidates pass through.\n_ADDR_RE = re.compile(r\"^0x[0-9a-f]{40}$\")\n_DATE_RE = re.compile(r\"^\\d{4}-\\d{2}-\\d{2}$\")\n\n# Pre-committed thresholds — see v0721_DESIGN.md §Thresholds.\n# Do NOT tune to \"rescue\" a specific case; if a candidate doesn't meet\n# the documented bar, document the miss rather than lowering it.\nMIN_TX_THIS_TOKEN = 200\nMAX_TX_FROM_DIVERSITY = 0.05\nMIN_TOP2_RATIO = 0.80\nMIN_CROSS_ALPHA_TOKENS = 5\nMAX_NET_BALANCE_PCT = 0.5 # filters genuine holders who happen to trade\n\n# Arkham label families that mark a counterparty as routing-class\n# infrastructure (vs a plain EOA). Matched case-insensitive against the\n# label_text and entity_type fields the same way section_cross_sym does.\n_ROUTER_HINTS = (\n \"router\", \"aggregator\", \"swap\", \"1inch\", \"okx\", \"pancake\", \"uniswap\",\n \"kyber\", \"paraswap\", \"0x protocol\", \"matcha\", \"alpha 2.0\",\n)\n_CEX_HINTS = (\"deposit\", \"cex\", \"exchange\", \"hot wallet\", \"binance\", \"kucoin\",\n \"okx exchange\", \"bybit\", \"mexc\", \"xt.com\", \"gate.io\", \"huobi\",\n \"kraken\", \"coinbase\")\n\n\nclass FlowOperatorError(ValueError):\n \"\"\"Raised on invalid input (bad addr / bad date / SQL injection guard).\"\"\"\n\n\ndef _validate_inputs(ca: str, candidates: list[str], listing_date: str) -> list[str]:\n \"\"\"Reject anything that doesn't pass the active chain's address\n format (EVM 0x40-hex or Solana base58 32-44) and a YYYY-MM-DD date.\n\n Defence in depth — every input flows into SQL `IN (…)` clauses and we\n do NOT trust upstream to have validated. v0.7.21.7: chain-aware via\n `chain_router.is_valid_addr`, so Solana CAs no longer get rejected\n here even though Section A accepted them upstream.\n \"\"\"\n is_solana = get_active_chain() == \"solana\"\n ca_norm = ca if is_solana else (ca or \"\").lower()\n if not _chain_is_valid_addr(ca_norm):\n raise FlowOperatorError(f\"invalid ca: {ca!r}\")\n if not _DATE_RE.fullmatch(listing_date or \"\"):\n raise FlowOperatorError(f\"invalid listing_date: {listing_date!r}\")\n clean: list[str] = []\n seen: set[str] = set()\n for a in candidates:\n a_norm = a if is_solana else (a or \"\").lower()\n if not _chain_is_valid_addr(a_norm):\n continue\n if a_norm in seen:\n continue\n seen.add(a_norm)\n clean.append(a_norm)\n return clean\n\n\n_TRANSIENT_HINTS = (\n \"429\", \"too many requests\", \"rate\", \"throttle\", \"timeout\",\n \"service unavailable\", \"503\", \"504\", \"temporarily\",\n)\n\n\ndef _is_transient(text: str) -> bool:\n lower = (text or \"\").lower()\n return any(h in lower for h in _TRANSIENT_HINTS)\n\n\ndef _run_sql(sql: str, max_rows: int = 500,\n max_attempts: int = 4) -> tuple[list[dict], int]:\n \"\"\"Run one surf onchain-sql with exponential backoff retry.\n\n v0.7.21 hot-fix: section_flow_operators runs immediately after\n section_wash_infra, which fires ~1200 surf calls (154 candidates ×\n 8 workers × 4-5 steps). The first PLAY v0.7.21 run captured\n section_flow_operators returning 0 operators and 0 credits while\n direct-call testing returned 1 + 25 — i.e. surf was 429-throttling\n every flow_operator SQL because wash_infra had just exhausted the\n rate budget. Adding retry brings flow_operator's success rate in\n the back-to-back scenario to ~100%.\n\n Returns ([], 0) only after all attempts fail; partial / non-\n transient errors return immediately.\n \"\"\"\n last_err = \"\"\n for attempt in range(max_attempts):\n try:\n proc = subprocess.run(\n [\"surf\", \"onchain-sql\"],\n input=json.dumps({\"sql\": sql, \"max_rows\": max_rows}),\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n timeout=60, check=False,\n )\n except (subprocess.TimeoutExpired, OSError) as e:\n last_err = str(e)\n if _is_transient(last_err) and attempt + 1 \u003c max_attempts:\n import random\n import time as _t\n _t.sleep((2 ** attempt) * (0.5 + random.random()))\n continue\n print(f\"[flow_operator_detector] surf failed: {last_err}\", file=sys.stderr)\n return [], 0\n if proc.returncode != 0:\n last_err = (proc.stderr or \"\")[:300]\n if _is_transient(last_err) and attempt + 1 \u003c max_attempts:\n import random\n import time as _t\n _t.sleep((2 ** attempt) * (0.5 + random.random()))\n continue\n print(\n f\"[flow_operator_detector] surf exit {proc.returncode}: {last_err}\",\n file=sys.stderr,\n )\n return [], 0\n try:\n doc = json.loads(proc.stdout)\n except json.JSONDecodeError:\n return [], 0\n if doc.get(\"error\"):\n err_text = str(doc.get(\"error\"))\n if _is_transient(err_text) and attempt + 1 \u003c max_attempts:\n import random\n import time as _t\n _t.sleep((2 ** attempt) * (0.5 + random.random()))\n continue\n print(\n f\"[flow_operator_detector] surf API error: {err_text}\",\n file=sys.stderr,\n )\n return [], 0\n rows = doc.get(\"data\") or []\n credits = int((doc.get(\"meta\") or {}).get(\"credits_used\") or 0)\n return rows, credits\n print(\n f\"[flow_operator_detector] surf retries exhausted ({max_attempts}): {last_err}\",\n file=sys.stderr,\n )\n return [], 0\n\n\ndef _arkham_classify(label_text: str, entity_type: str, entity_name: str) -> str:\n \"\"\"Categorise a counterparty by Arkham label hints.\n\n Returns one of: 'router', 'cex', 'eoa', 'other'.\n 'other' = labelled but not router/cex (e.g. NFT contract).\n 'eoa' = unlabelled (no Arkham entry) — treat as plain wallet.\n \"\"\"\n text = \" \".join(\n x for x in [label_text or \"\", entity_type or \"\", entity_name or \"\"]\n ).lower()\n if not text.strip():\n return \"eoa\"\n if any(h in text for h in _ROUTER_HINTS):\n return \"router\"\n if any(h in text for h in _CEX_HINTS):\n return \"cex\"\n return \"other\"\n\n\ndef _fetch_arkham_labels(addrs: list[str]) -> dict[str, dict]:\n \"\"\"Batch wallet-labels-batch (max 100 per call). Returns {addr: {...}}.\n \"\"\"\n if not addrs:\n return {}\n out: dict[str, dict] = {}\n # Chunk to 100 (surf cap).\n for i in range(0, len(addrs), 100):\n chunk = addrs[i:i + 100]\n try:\n proc = subprocess.run(\n [\"surf\", \"wallet-labels-batch\",\n \"--addresses\", \",\".join(chunk),\n \"--json\"],\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n timeout=30, check=False,\n )\n if proc.returncode != 0:\n continue\n doc = json.loads(proc.stdout)\n for r in (doc.get(\"data\") or []):\n a = (r.get(\"address\") or \"\").lower()\n if not a:\n continue\n # surf wallet-labels-batch can shape labels in either of\n # two ways depending on the dataset: nested under\n # `label.labels[]` (with confidence) or as a flat list\n # `labels[]` of strings. Handle both — v0.7.21 PLAY case\n # had 0xc8f6b8 in the flat-string form.\n label_blob = r.get(\"label\") or {}\n top_label_text: str | None = None\n if isinstance(label_blob, dict):\n inner = label_blob.get(\"labels\")\n if isinstance(inner, list) and inner:\n # nested with confidence\n try:\n top = max(\n inner,\n key=lambda x: (\n x.get(\"confidence\", 0)\n if isinstance(x, dict) else 0\n ),\n )\n top_label_text = (top or {}).get(\"label\") if isinstance(top, dict) else None\n except (TypeError, ValueError):\n top_label_text = None\n # Flat top-level labels list (string or dict elements).\n flat = r.get(\"labels\")\n if not top_label_text and isinstance(flat, list) and flat:\n first = flat[0]\n top_label_text = first if isinstance(first, str) else (\n first.get(\"label\") if isinstance(first, dict) else None\n )\n out[a] = {\n \"label_text\": top_label_text,\n \"entity_name\": r.get(\"entity_name\") or (\n label_blob.get(\"entity_name\") if isinstance(label_blob, dict) else None\n ),\n \"entity_type\": r.get(\"entity_type\") or (\n label_blob.get(\"entity_type\") if isinstance(label_blob, dict) else None\n ),\n }\n except (subprocess.TimeoutExpired, OSError, json.JSONDecodeError):\n continue\n return out\n\n\ndef detect(\n *,\n ca: str,\n candidate_addrs: list[str],\n listing_date: str,\n total_supply: int | None,\n alpha_token_cas_base: set[str] | None = None,\n alpha_token_cas_bsc: set[str] | None = None,\n alpha_ca_to_sym_base: dict[str, str] | None = None,\n alpha_ca_to_sym_bsc: dict[str, str] | None = None,\n cross_alpha_window_days: int = 60,\n) -> tuple[list[dict], int]:\n \"\"\"v0.7.21 flow-based operator detector.\n\n Args:\n ca: subject token contract address (lower-case 0x40-hex).\n candidate_addrs: union of (wash_infra step-0 candidates) and\n (dump_tracker top-200 DEX sellers). The detector validates\n and dedupes — pass whatever's available.\n listing_date: YYYY-MM-DD, lower bound for the subject-token SQL.\n total_supply: token total supply, used to filter by\n MAX_NET_BALANCE_PCT (genuine holders, not bots).\n alpha_token_cas_base / alpha_token_cas_bsc: Alpha API token CA\n sets per chain. Used to compute `cross_alpha_token_count`\n (how many other Alpha tokens the candidate also touches).\n If None or empty, cross_alpha_token_count stays at 0.\n cross_alpha_window_days: lookback window for cross-Alpha scan.\n Default 60 days matches the design doc threshold.\n\n Returns:\n (operators, credits_used) — operators is a list of dicts (see\n v0721_DESIGN.md §Output schema) sorted by n_tx_this_token DESC.\n credits_used is the surf credit total for this detector run.\n\n Per the design doc, this never raises — surf failures degrade to\n empty operators / 0 credits and a stderr warning.\n \"\"\"\n candidates = _validate_inputs(ca, candidate_addrs, listing_date)\n if not candidates:\n return [], 0\n\n credits_total = 0\n in_list = \",\".join(f\"'{a}'\" for a in candidates)\n\n # ---- batch SQL 1: per-candidate stats on the SUBJECT token ------\n # diversity = count(DISTINCT tx_from) / count(*) per candidate.\n # Combined with n_tx and net flow in a single GROUP BY.\n transfers = transfers_table()\n sql_subject = (\n f\"SELECT addr, \"\n f\" count(DISTINCT tx_from) AS unique_origins, \"\n f\" count(*) AS n_tx, \"\n f\" sumIf(amount, role='sent') AS tok_out, \"\n f\" sumIf(amount, role='recv') AS tok_in \"\n f\"FROM (\"\n f\" SELECT `from` AS addr, amount, tx_from, 'sent' AS role \"\n f\" FROM {transfers} \"\n f\" WHERE contract_address='{ca.lower()}' \"\n f\" AND block_date >= '{listing_date}' \"\n f\" AND `from` IN ({in_list}) \"\n f\" UNION ALL \"\n f\" SELECT `to` AS addr, amount, tx_from, 'recv' AS role \"\n f\" FROM {transfers} \"\n f\" WHERE contract_address='{ca.lower()}' \"\n f\" AND block_date >= '{listing_date}' \"\n f\" AND `to` IN ({in_list}) \"\n f\") \"\n f\"GROUP BY addr\"\n )\n # v0.7.21 hot-fix: surf caps max_rows at 10000. Subject stats SQL\n # returns one row per candidate so len(candidates)+10 is correct but\n # we clamp defensively in case future caller passes >9990 candidates.\n rows1, c1 = _run_sql(sql_subject, max_rows=min(len(candidates) + 10, 10000))\n credits_total += c1\n subject_stats: dict[str, dict] = {}\n for r in rows1:\n a = (r.get(\"addr\") or \"\").lower()\n n_tx = int(r.get(\"n_tx\") or 0)\n u_origin = int(r.get(\"unique_origins\") or 0)\n tok_in = float(r.get(\"tok_in\") or 0)\n tok_out = float(r.get(\"tok_out\") or 0)\n net_balance = tok_in - tok_out\n diversity = (u_origin / n_tx) if n_tx > 0 else 1.0\n net_pct = (net_balance / total_supply * 100) if total_supply else None\n subject_stats[a] = {\n \"n_tx\": n_tx,\n \"tx_from_diversity\": diversity,\n \"tok_in\": tok_in,\n \"tok_out\": tok_out,\n \"net_balance\": net_balance,\n \"net_balance_pct_supply\": net_pct,\n }\n\n # Apply primary gate: single-operator + high enough frequency.\n survivors = [\n a for a, s in subject_stats.items()\n if s[\"n_tx\"] >= MIN_TX_THIS_TOKEN\n and s[\"tx_from_diversity\"] \u003c MAX_TX_FROM_DIVERSITY\n and (s[\"net_balance_pct_supply\"] is None\n or abs(s[\"net_balance_pct_supply\"]) \u003c= MAX_NET_BALANCE_PCT)\n ]\n if not survivors:\n return [], credits_total\n\n surv_list = \",\".join(f\"'{a}'\" for a in survivors)\n\n # ---- batch SQL 2: top-2 counterparty per survivor on this token --\n # For each survivor compute total per-counterparty tx count, then\n # the helper picks top 2 in Python (window functions vary across\n # ClickHouse surf builds; staying portable).\n sql_cp = (\n f\"SELECT addr, counterparty, count(*) AS n \"\n f\"FROM ( \"\n f\" SELECT `from` AS addr, `to` AS counterparty \"\n f\" FROM {transfers} \"\n f\" WHERE contract_address='{ca.lower()}' \"\n f\" AND block_date >= '{listing_date}' \"\n f\" AND `from` IN ({surv_list}) \"\n f\" UNION ALL \"\n f\" SELECT `to` AS addr, `from` AS counterparty \"\n f\" FROM {transfers} \"\n f\" WHERE contract_address='{ca.lower()}' \"\n f\" AND block_date >= '{listing_date}' \"\n f\" AND `to` IN ({surv_list}) \"\n f\") \"\n f\"GROUP BY addr, counterparty\"\n )\n # v0.7.21 hot-fix: surf caps max_rows at 10000. The PLAY case hit\n # this — 62 survivors × 200 = 12500, INVALID_REQUEST, zero rows back,\n # every operator got empty counterparty_top2 and got mis-classified\n # as UNCLASSIFIED_SINGLE_OPERATOR. Cap at the surf limit; in\n # practice a survivor has \u003c 50 unique counterparties on the subject\n # token (most txs go through the same router / pool), so a per-\n # survivor budget of 50 fits well under the cap.\n rows2, c2 = _run_sql(sql_cp, max_rows=min(len(survivors) * 50 + 100, 10000))\n credits_total += c2\n cp_by_addr: dict[str, list[tuple[str, int]]] = {}\n _is_solana = get_active_chain() == \"solana\"\n for r in rows2:\n # Solana base58 is case-sensitive; EVM is case-insensitive.\n a = r.get(\"addr\") or \"\"\n cp = r.get(\"counterparty\") or \"\"\n if not _is_solana:\n a = a.lower()\n cp = cp.lower()\n if not (_chain_is_valid_addr(a) and _chain_is_valid_addr(cp)):\n continue\n cp_by_addr.setdefault(a, []).append((cp, int(r.get(\"n\") or 0)))\n # Pick top-2 per addr, compute ratio\n top2_by_addr: dict[str, dict] = {}\n for a, cps in cp_by_addr.items():\n cps.sort(key=lambda t: t[1], reverse=True)\n total = sum(n for _, n in cps)\n if total \u003c= 0:\n continue\n top2 = cps[:2]\n top2_n = sum(n for _, n in top2)\n top2_by_addr[a] = {\n \"top2\": [{\"addr\": cp, \"n_tx\": n} for cp, n in top2],\n \"top2_ratio\": top2_n / total,\n }\n\n # ---- batch SQL 3: cross-Alpha token count (60d, current chain) ---\n sql_cross = (\n f\"SELECT addr, count(DISTINCT contract_address) AS n_tokens, \"\n f\" count(*) AS n_tx \"\n f\"FROM ( \"\n f\" SELECT `from` AS addr, contract_address \"\n f\" FROM {transfers} \"\n f\" WHERE block_date >= today() - {cross_alpha_window_days} \"\n f\" AND `from` IN ({surv_list}) \"\n f\" UNION ALL \"\n f\" SELECT `to` AS addr, contract_address \"\n f\" FROM {transfers} \"\n f\" WHERE block_date >= today() - {cross_alpha_window_days} \"\n f\" AND `to` IN ({surv_list}) \"\n f\") \"\n f\"GROUP BY addr\"\n )\n rows3, c3 = _run_sql(sql_cross, max_rows=len(survivors) + 10)\n credits_total += c3\n # We can't list every contract_address per addr (too much); the\n # cross_alpha_token_count is an UPPER bound on Alpha matches. To\n # get the EXACT Alpha count we'd need contract_address per row,\n # which blows up the row count. Per design doc, we instead reuse\n # the SQL above for total distinct tokens and add a separate\n # SQL only for the Alpha-CA intersection if alpha sets are given.\n cross_stats: dict[str, dict] = {}\n for r in rows3:\n a = (r.get(\"addr\") or \"\").lower()\n cross_stats[a] = {\n \"cross_token_count_all\": int(r.get(\"n_tokens\") or 0),\n \"n_tx_60d_all_tokens\": int(r.get(\"n_tx\") or 0),\n }\n\n # Optional: scope cross_token_count to Alpha tokens. Two SQL — one\n # for the active chain (the table chain_router currently points at),\n # one for BSC if BSC is NOT the active chain. The Alpha CA sets are\n # chain-specific (a token on Base has a different CA than a wrapper\n # on BSC, even when it's the \"same\" project), so we sum the per-chain\n # counts. PLAY case: active chain Base hits 3 Alpha tokens, BSC adds\n # 28 (the same operator runs on BSC too) → total 31, comfortably\n # above MIN_CROSS_ALPHA_TOKENS=5.\n\n # v0.7.21.2: per-survivor cross-Alpha token breakdown. For every\n # survivor we collect (sym, n_tx, chain) tuples so the report can\n # name *which* Alpha tokens the operator runs on (the design doc\n # called for a sub-table per operator, but v0.7.21 only kept the\n # count — user feedback caught the gap).\n cross_alpha_breakdown: dict[str, list[dict]] = {a: [] for a in survivors}\n\n def _run_alpha_scan(\n alpha_ca_set: set[str],\n ca_to_sym: dict[str, str],\n transfers_for_chain: str,\n chain_label: str,\n ) -> int:\n if not alpha_ca_set:\n return 0\n # Drop the subject token from the scan — we don't want\n # cross_alpha_token_count incrementing because of the report's\n # own token. v0.7.21.7: chain-aware — Solana CAs are case-sensitive,\n # EVM are lowercase. _chain_is_valid_addr handles both per active chain.\n if get_active_chain() == \"solana\":\n clean_set = {a for a in alpha_ca_set\n if _chain_is_valid_addr(a) and a != ca}\n else:\n clean_set = {a.lower() for a in alpha_ca_set\n if _chain_is_valid_addr(a.lower())\n and a.lower() != ca.lower()}\n if not clean_set:\n return 0\n alpha_in = \",\".join(f\"'{a}'\" for a in clean_set)\n sql_alpha = (\n f\"SELECT addr, contract_address AS ca, count(*) AS n_tx \"\n f\"FROM ( \"\n f\" SELECT `from` AS addr, contract_address \"\n f\" FROM {transfers_for_chain} \"\n f\" WHERE block_date >= today() - {cross_alpha_window_days} \"\n f\" AND `from` IN ({surv_list}) \"\n f\" AND contract_address IN ({alpha_in}) \"\n f\" UNION ALL \"\n f\" SELECT `to` AS addr, contract_address \"\n f\" FROM {transfers_for_chain} \"\n f\" WHERE block_date >= today() - {cross_alpha_window_days} \"\n f\" AND `to` IN ({surv_list}) \"\n f\" AND contract_address IN ({alpha_in}) \"\n f\") \"\n f\"GROUP BY addr, ca\"\n )\n # Per design max_rows budget: 62 survivors × ~30 cross-Alpha tokens\n # ≈ 1860 worst case. Clamp at surf's 10K cap defensively.\n rows_alpha, c_alpha = _run_sql(\n sql_alpha,\n max_rows=min(len(survivors) * 200 + 100, 10000),\n )\n for r in rows_alpha:\n a = (r.get(\"addr\") or \"\").lower()\n ca_hit = (r.get(\"ca\") or \"\").lower()\n n_tx = int(r.get(\"n_tx\") or 0)\n if a not in cross_stats or not ca_hit:\n continue\n cross_alpha_breakdown.setdefault(a, []).append({\n \"ca\": ca_hit,\n \"sym\": ca_to_sym.get(ca_hit, \"?\"),\n \"chain\": chain_label,\n \"n_tx\": n_tx,\n })\n return c_alpha\n\n active_chain = get_active_chain()\n if active_chain == \"bsc\":\n # Active chain is already BSC. Merge symbol maps so a CA listed\n # on both chains is still labelled.\n union_set = (alpha_token_cas_bsc or set()) | (alpha_token_cas_base or set())\n union_map = {**(alpha_ca_to_sym_base or {}), **(alpha_ca_to_sym_bsc or {})}\n credits_total += _run_alpha_scan(union_set, union_map, transfers_table(), \"bsc\")\n else:\n # Scan active chain first (PLAY: Base).\n credits_total += _run_alpha_scan(\n alpha_token_cas_base or set(),\n alpha_ca_to_sym_base or {},\n transfers_table(),\n active_chain,\n )\n # Then BSC — chain_lock flips the router for the BSC SQL and\n # restores on exit.\n if alpha_token_cas_bsc:\n try:\n with chain_lock(\"bsc\"):\n credits_total += _run_alpha_scan(\n alpha_token_cas_bsc,\n alpha_ca_to_sym_bsc or {},\n transfers_table(),\n \"bsc\",\n )\n except Exception as e:\n print(\n f\"[flow_operator_detector] BSC cross-Alpha scan failed: {e}\",\n file=sys.stderr,\n )\n\n # v0.7.21.2: aggregate per-op count + FULL sorted breakdown (no cap).\n # User explicitly chose option B (full compact inline) over a top-N\n # cap — the report renders every cross-Alpha token in one\n # `sym(n_tx·chain) · sym(...) · ...` line per operator, so the user\n # never has to open skeleton.json to see the full list. Skeleton\n # grows ~140KB (62 ops × ~30 tokens × 80 bytes/entry) which is\n # well within acceptable bounds.\n for a in survivors:\n tokens = cross_alpha_breakdown.get(a) or []\n tokens.sort(key=lambda t: t[\"n_tx\"], reverse=True)\n # Distinct (chain, ca) — the same CA may appear on both chains\n # (e.g. a bridged wrapper); we keep both rows so a future audit\n # can see the chain split. The count is over distinct (chain, ca).\n cross_stats[a][\"cross_alpha_token_count\"] = len(tokens)\n cross_stats[a][\"cross_alpha_tokens\"] = tokens\n\n # ---- Arkham labels for OPERATORS + top-2 counterparties --------\n # v0.8.0.2 fix (user catch): operators themselves were never labeled\n # — only counterparties. That meant a known router masquerading as\n # an operator survived classification + showed as \"UNCLASSIFIED\n # single operator\", and DEX_ARB_BOT vs ASYNC_WASH couldn't reflect\n # the operator's own arkham class. Merge survivor addresses into\n # the same label-resolution batch (0 extra surf round-trip —\n # _fetch_arkham_labels does one batched call regardless).\n label_addr_set: set[str] = set(survivors)\n for a in survivors:\n for cp in top2_by_addr.get(a, {}).get(\"top2\", []):\n label_addr_set.add(cp[\"addr\"])\n labels = _fetch_arkham_labels(sorted(label_addr_set))\n\n # ---- Assemble operator rows + sub-classification ----------------\n operators = []\n for a in survivors:\n s = subject_stats[a]\n top2 = top2_by_addr.get(a, {})\n cross = cross_stats.get(a, {})\n cp_top2 = top2.get(\"top2\") or []\n # Annotate top-2 with Arkham classification\n annotated_top2 = []\n for cp in cp_top2:\n lbl = labels.get(cp[\"addr\"]) or {}\n cls = _arkham_classify(\n lbl.get(\"label_text\") or \"\",\n lbl.get(\"entity_type\") or \"\",\n lbl.get(\"entity_name\") or \"\",\n )\n annotated_top2.append({\n \"addr\": cp[\"addr\"],\n \"n_tx\": cp[\"n_tx\"],\n \"arkham_class\": cls,\n \"arkham_label\": lbl.get(\"label_text\"),\n \"arkham_entity_name\": lbl.get(\"entity_name\"),\n })\n\n # Sub-classification rules — see v0721_DESIGN.md §sub_class table.\n sub_classes = []\n top2_ratio = top2.get(\"top2_ratio\", 0.0)\n cross_alpha = cross.get(\"cross_alpha_token_count\", 0)\n if top2_ratio >= MIN_TOP2_RATIO and len(annotated_top2) == 2:\n classes = {cp[\"arkham_class\"] for cp in annotated_top2}\n if classes \u003c= {\"router\", \"cex\"} and classes:\n sub_classes.append(\"DEX_ARB_BOT\")\n elif \"eoa\" in classes:\n sub_classes.append(\"ASYNC_WASH\")\n if cross_alpha >= MIN_CROSS_ALPHA_TOKENS:\n sub_classes.append(\"CROSS_ALPHA_OPERATOR\")\n\n if not sub_classes:\n # Survivors that pass diversity but match no sub-class are\n # still informative — flag as UNCLASSIFIED so the report can\n # surface them without forcing a category fit.\n sub_classes.append(\"UNCLASSIFIED_SINGLE_OPERATOR\")\n\n # v0.8.0.2 fix: annotate operator self with arkham label.\n op_lbl = labels.get(a) or {}\n op_arkham_cls = _arkham_classify(\n op_lbl.get(\"label_text\") or \"\",\n op_lbl.get(\"entity_type\") or \"\",\n op_lbl.get(\"entity_name\") or \"\",\n )\n operators.append({\n \"addr\": a,\n \"sub_class\": sub_classes[0], # primary tag\n \"sub_classes_all\": sub_classes, # full set\n # v0.8.0.2 — Arkham label for operator itself (was missing).\n # Caller (render_report.py) surfaces this in the FLOW\n # OPERATORS table column so a router/CEX hot wallet shows\n # the entity name + class instead of \"UNCLASSIFIED\".\n \"arkham_label\": op_lbl.get(\"label_text\"),\n \"arkham_entity_name\": op_lbl.get(\"entity_name\"),\n \"arkham_entity_type\": op_lbl.get(\"entity_type\"),\n \"arkham_classification\": op_arkham_cls,\n \"n_tx_this_token\": s[\"n_tx\"],\n \"tx_from_diversity\": s[\"tx_from_diversity\"],\n \"tok_in_this_token\": s[\"tok_in\"],\n \"tok_out_this_token\": s[\"tok_out\"],\n \"net_balance_this_token\": s[\"net_balance\"],\n \"net_balance_pct_supply\": s[\"net_balance_pct_supply\"],\n \"counterparty_top2_ratio\": top2_ratio,\n \"counterparty_top2\": annotated_top2,\n \"n_tx_60d_all_tokens\": cross.get(\"n_tx_60d_all_tokens\", 0),\n \"cross_token_count_all\": cross.get(\"cross_token_count_all\", 0),\n \"cross_alpha_token_count\": cross_alpha,\n # v0.7.21.2: full per-Alpha-token breakdown — list of\n # {ca, sym, chain, n_tx}, sorted by n_tx DESC. Rendered as\n # one compact inline line per operator so the report covers\n # the whole cross-Alpha activity without pushing the user\n # to skeleton.json. Empty when alpha_ca_to_sym_* not passed.\n \"cross_alpha_tokens\": cross.get(\"cross_alpha_tokens\", []),\n # v0.7.21.1: writable narrative slots so the report shows\n # plain-language analysis per operator (matches the cross_sym\n # whales pattern). LLM fills these with what this wallet IS\n # (identity_narrative) and how to use the information\n # (risk_assessment_narrative). The user reads the report,\n # not the skeleton — every operator needs to stand on its\n # own without the reader hunting for context in JSON.\n \"identity_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n \"risk_assessment_narrative\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n\n operators.sort(key=lambda o: o[\"n_tx_this_token\"], reverse=True)\n return operators, credits_total\n","content_type":"text/x-python; charset=utf-8","language":"python","size":30379,"content_sha256":"3dd0273c00332e786095dd1e4069fb1071d1ba742cc1ab20e3b27fb17a1f533b"},{"filename":"alpha/v06/helpers/funding_source_attribution.py","content":"\"\"\"v0.7.23.1: Funding source attribution for high-value addresses.\n\nReverse approach to \"矿币 / bridge / airdrop distribution modelling\".\n\nStandard rule_11 (deployer → m6 → m4) assumes a single deployer is the\ndistribution authority. Tokens like Humanity Protocol, Aethir-class\nmining tokens, Polyhedra-class bridge tokens, and vesting-airdrop\ntokens violate this — the deployer is a placeholder; real distribution\nflows via bridge mint contracts, vesting contracts, or airdrop reward\ncontracts. Forward-decoding the distribution mechanism is surf-load-\nintensive (cluster hub detection across thousands of mining receivers)\nand depends on an unstable threshold (n_senders ≥ 10 etc.).\n\nReverse approach instead: take the high-value addresses surfaced by\nexisting detectors (wash_infra P/Q, flow_operators op_addrs, rule_11\nm6 receivers, dump_tracker insider_addrs, top_holders top-N) and ask\n\"where did each addr's token come from\": mint vs DEX buy vs CEX\nwithdraw vs P2P transfer. A wallet with 87% mint origin + heavy DEX\ndump activity is operator-mined-then-dumped; 92% DEX buy + heavy dump\nis normal retail PnL-taker. The classification answers the user's\nreal question (\"是不是庄家在出货\") without decoding the mining\nmechanism.\n\nOne batch SQL (group by addr, source) covers up to ~200 addresses\nwithin surf's 30s budget.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport os\nfrom datetime import date, timedelta\nfrom typing import Any\n\nfrom chain_router import (\n is_valid_addr as _chain_is_valid_addr,\n transfers_table,\n dex_trades_table,\n decimals_factor_str,\n)\nfrom window_chunker import ( # v0.9.4\n chunked_dates,\n parallel_run_chunked,\n merge_chunked_rows,\n chunk_summary,\n)\n\n# surf enforces a 365-day window on `bsc_transfers` queries (and the\n# equivalent on other chains). Any wider span is rejected upfront with\n# INVALID_REQUEST. The helper clamps caller-supplied date_floor to today\n# - 365d so a listing-anchored floor like (listing - 365d) doesn't blow\n# up when the token has been listed for more than a year.\n_SURF_MAX_LOOKBACK_DAYS = 365\n\n# v0.9.4 round-2 adversarial review fix #1: surf onchain-sql hard-caps max_rows at\n# 10000 (adversarial review CLO bug report 2026-06-12, confirmed in section_a_scope\n# error parsing comment). Stay safely below the cap for chunk_top_n\n# computations so default pipeline call (`top_n=100` → naive\n# `chunk_top_n = top_n * 100 = 10000` → `max_rows=10010`) doesn't\n# every-chunk-fail with INVALID_REQUEST.\n_SURF_SAFE_MAX_ROWS = 9000\n\n# v0.9.4: same heuristic as rule_11._is_short_window — short windows\n# collapse to a single chunk; long windows split to chunk_days=90.\n# Override via env on tokens where the default proves wrong (timeouts\n# at 200d → lower; surf back to normal → raise toward 400-500d).\n_SHORT_WINDOW_DAYS = int(os.environ.get(\"BINANCE_ALPHA_SHORT_WINDOW_DAYS\", \"300\"))\n\n\ndef _is_short_window(floor: str, ceiling: str | None = None) -> bool:\n floor_d = date.fromisoformat(floor)\n ceiling_d = date.fromisoformat(ceiling) if ceiling else date.today()\n return (ceiling_d - floor_d).days \u003c= _SHORT_WINDOW_DAYS\n\n\ndef _single_chunk_or_chunker_dates(\n floor: str, ceiling: str | None = None, chunk_days: int = 90,\n) -> list[tuple[str, str]]:\n if _is_short_window(floor, ceiling):\n ceiling_concrete = ceiling or date.today().isoformat()\n return [(floor, ceiling_concrete)]\n return chunked_dates(floor, ceiling or date.today().isoformat(), chunk_days=chunk_days)\n\n\ndef _run_chunk_via_surf(sql: str, max_rows: int = 5000, base_timeout: int = 40) -> dict[str, Any]:\n \"\"\"v0.9.4: surf onchain-sql call shaped for parallel_run_chunked. Returns\n {data: [...], _error?: str, meta: {...}}. Threads section_a_scope's retry\n wrapper so 429 / transient surf errors get the same backoff treatment as\n single-shot queries. Unlike rule_11's _run_one_chunk (which writes a\n workdir file), this uses stdin so funding_attribution doesn't need a\n workdir passed in.\n \"\"\"\n from section_a_scope import _run_surf_with_retry\n try:\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": max_rows}),\n base_timeout=base_timeout, max_attempts=4,\n )\n except Exception as e:\n return {\"_error\": f\"{type(e).__name__}: {e}\"[:200], \"data\": []}\n if not doc:\n return {\"_error\": err or \"surf returned no doc\", \"data\": []}\n return {\"data\": doc.get(\"data\") or [], \"meta\": doc.get(\"meta\") or {}}\n\n_DEAD = {\n \"0x0000000000000000000000000000000000000000\",\n \"0x000000000000000000000000000000000000dead\",\n}\n_ZERO_ADDR = \"0x0000000000000000000000000000000000000000\"\n# Unmatchable sentinel for empty IN lists. ClickHouse rejects IN ()\n# and the bare literal NULL has unpredictable semantics with strict\n# comparisons; a literal that no real address can match preserves\n# CASE branch ordering without false positives.\n_IN_SENTINEL = \"('0xffffffffffffffffffffffffffffffffffffffff')\"\n\n\ndef _clean_addrs(addrs: list[str] | None) -> list[str]:\n \"\"\"Dedup + lowercase + validate. Drops zero/burn addrs.\"\"\"\n out: list[str] = []\n seen: set[str] = set()\n for a in (addrs or []):\n if not a:\n continue\n a = a.lower()\n if a in seen or a in _DEAD:\n continue\n if not _chain_is_valid_addr(a):\n continue\n seen.add(a)\n out.append(a)\n return out\n\n\ndef _empty_summary() -> dict[str, int]:\n return {\n \"n_addrs_queried\": 0, \"n_addrs_with_data\": 0,\n \"n_mining_fed\": 0, \"n_dex_fed\": 0,\n \"n_p2p_fed\": 0, \"n_cex_fed\": 0,\n }\n\n\ndef _build_source_sql(\n *,\n ca: str,\n high_value_addrs: list[str],\n dex_pair_addrs: list[str],\n cex_addrs: list[str],\n date_floor: str,\n) -> str:\n \"\"\"Single batch SQL classifying incoming transfers by source.\n\n Each row of the result: (addr, source, sum(amt), count). source ∈\n {mint, dex_buy, cex_withdraw, p2p}. Mint is detected by\n from=0x0; dex_buy by from ∈ known DEX pair set; cex_withdraw by\n from ∈ known CEX custody set; else p2p_transfer.\n\n All address values interpolated into the SQL are forced to lowercase\n here (defensive — attribute_funding's _clean_addrs already does this\n upstream, but the helper is also called directly by tests). The SQL\n never wraps lower() on column values (surf-team anti-pattern: it\n prevents projection-skip optimisation, exactly what slowed SKYAI).\n \"\"\"\n ca_lc = ca.lower()\n high_lc = [a.lower() for a in high_value_addrs]\n dex_lc = [a.lower() for a in dex_pair_addrs]\n cex_lc = [a.lower() for a in cex_addrs]\n high_in = \"(\" + \",\".join(f\"'{a}'\" for a in high_lc) + \")\"\n dex_in = (\n \"(\" + \",\".join(f\"'{a}'\" for a in dex_lc) + \")\"\n if dex_lc else _IN_SENTINEL\n )\n cex_in = (\n \"(\" + \",\".join(f\"'{a}'\" for a in cex_lc) + \")\"\n if cex_lc else _IN_SENTINEL\n )\n sql = (\n \"SELECT \\\"to\\\" AS addr, \"\n \"CASE \"\n f\" WHEN \\\"from\\\" = '{_ZERO_ADDR}' THEN 'mint' \"\n f\" WHEN \\\"from\\\" IN {dex_in} THEN 'dex_buy' \"\n f\" WHEN \\\"from\\\" IN {cex_in} THEN 'cex_withdraw' \"\n \" ELSE 'p2p' END AS source, \"\n f\"sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS amt, \"\n \"count() AS n_tx \"\n f\"FROM {transfers_table()} \"\n f\"WHERE contract_address = '{ca_lc}' \"\n f\"AND \\\"to\\\" IN {high_in} \"\n f\"AND block_date >= '{date_floor}' \"\n \"GROUP BY addr, source\"\n )\n return sql\n\n\ndef attribute_funding(\n *,\n ca: str,\n high_value_addrs: list[str],\n dex_pair_addrs: list[str] | None = None,\n cex_addrs: list[str] | None = None,\n date_floor: str = \"2020-01-01\",\n max_addrs: int = 200,\n) -> dict[str, Any]:\n \"\"\"Classify each high-value address's incoming token by source type.\n\n Args:\n ca: contract address (lowercased internally for EVM).\n high_value_addrs: addresses to classify. Capped to max_addrs.\n dex_pair_addrs: known DEX pair / pool addresses (from\n section_a chain_lp_realtime). Empty list ⇒ no dex_buy\n classification — all non-mint flows fall into p2p.\n cex_addrs: known CEX custody addresses (from identity\n classifier). Empty list ⇒ no cex_withdraw classification.\n date_floor: 'YYYY-MM-DD'. Earliest block_date to scan.\n max_addrs: hard cap on input list (prevents SQL bloat).\n\n Returns: dict with keys:\n attributions: {addr → per-addr classification dict}\n summary: counts by primary source\n _debug: SQL diagnostics\n _error: only present on hard failure\n \"\"\"\n ca = (ca or \"\").lower()\n if not ca or not _chain_is_valid_addr(ca):\n return {\n \"attributions\": {}, \"summary\": _empty_summary(),\n \"_error\": f\"invalid ca: {ca!r}\",\n }\n\n addrs = _clean_addrs(high_value_addrs)\n truncated_n = max(0, len(addrs) - max_addrs)\n addrs = addrs[:max_addrs]\n\n # surf enforces a 365-day block_date window — clamp the caller's\n # floor up if it's older. Forensic callers naturally pass listing-365d\n # which can pre-date today-365d for tokens listed > 1 year ago.\n surf_window_floor = (date.today() - timedelta(days=_SURF_MAX_LOOKBACK_DAYS)).isoformat()\n if date_floor \u003c surf_window_floor:\n date_floor_clamped = surf_window_floor\n else:\n date_floor_clamped = date_floor\n\n if not addrs:\n return {\n \"attributions\": {}, \"summary\": _empty_summary(),\n \"_debug\": {\n \"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor,\n \"n_dex_pairs_known\": len(_clean_addrs(dex_pair_addrs)),\n \"n_cex_known\": len(_clean_addrs(cex_addrs)),\n },\n }\n\n dex_pairs = _clean_addrs(dex_pair_addrs)\n cex_a = _clean_addrs(cex_addrs)\n\n sql = _build_source_sql(\n ca=ca, high_value_addrs=addrs,\n dex_pair_addrs=dex_pairs, cex_addrs=cex_a,\n date_floor=date_floor_clamped,\n )\n\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\n \"sql\": sql,\n # 4 sources × addrs + headroom for orphan rows.\n \"max_rows\": max(len(addrs) * 4 + 100, 500),\n }),\n base_timeout=60,\n max_attempts=4, # v0.7.23 surf-team cooperation pattern\n )\n except Exception as e:\n doc, err = None, f\"exception: {str(e)[:120]}\"\n\n if not doc:\n return {\n \"attributions\": {}, \"summary\": _empty_summary(),\n \"_error\": err or \"surf returned no doc\",\n \"_debug\": {\n \"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor_clamped,\n \"lookup_window_floor_requested\": date_floor,\n \"n_dex_pairs_known\": len(dex_pairs),\n \"n_cex_known\": len(cex_a),\n },\n }\n\n per_addr: dict[str, dict[str, Any]] = {}\n for row in (doc.get(\"data\") or []):\n a = (row.get(\"addr\") or \"\").lower()\n if not a:\n continue\n src = row.get(\"source\") or \"p2p\"\n if src not in {\"mint\", \"dex_buy\", \"cex_withdraw\", \"p2p\"}:\n src = \"p2p\"\n try:\n amt = float(row.get(\"amt\") or 0)\n except (TypeError, ValueError):\n amt = 0.0\n try:\n n_tx = int(row.get(\"n_tx\") or 0)\n except (TypeError, ValueError):\n n_tx = 0\n bucket = per_addr.setdefault(a, {\n \"mint\": 0.0, \"dex_buy\": 0.0, \"cex_withdraw\": 0.0, \"p2p\": 0.0,\n \"n_tx\": {\"mint\": 0, \"dex_buy\": 0, \"cex_withdraw\": 0, \"p2p\": 0},\n })\n bucket[src] += amt\n bucket[\"n_tx\"][src] = bucket[\"n_tx\"].get(src, 0) + n_tx\n\n attributions: dict[str, dict[str, Any]] = {}\n n_mining = n_dex = n_p2p = n_cex = 0\n for a in addrs:\n b = per_addr.get(a) or {\n \"mint\": 0.0, \"dex_buy\": 0.0, \"cex_withdraw\": 0.0, \"p2p\": 0.0,\n \"n_tx\": {\"mint\": 0, \"dex_buy\": 0, \"cex_withdraw\": 0, \"p2p\": 0},\n }\n total = b[\"mint\"] + b[\"dex_buy\"] + b[\"cex_withdraw\"] + b[\"p2p\"]\n if total > 0:\n mint_pct = b[\"mint\"] / total\n dex_pct = b[\"dex_buy\"] / total\n cex_pct = b[\"cex_withdraw\"] / total\n p2p_pct = b[\"p2p\"] / total\n primary = max(\n (\"mint\", \"dex_buy\", \"cex_withdraw\", \"p2p\"),\n key=lambda k: b[k],\n )\n else:\n mint_pct = dex_pct = cex_pct = p2p_pct = None\n primary = None\n is_mining_fed = mint_pct is not None and mint_pct >= 0.50\n attributions[a] = {\n \"mint\": b[\"mint\"], \"dex_buy\": b[\"dex_buy\"],\n \"cex_withdraw\": b[\"cex_withdraw\"], \"p2p\": b[\"p2p\"],\n \"n_tx\": b[\"n_tx\"], \"total\": total,\n \"mint_pct\": mint_pct, \"dex_buy_pct\": dex_pct,\n \"cex_withdraw_pct\": cex_pct, \"p2p_pct\": p2p_pct,\n \"primary_source\": primary,\n \"is_mining_fed\": is_mining_fed,\n }\n if primary == \"mint\":\n n_mining += 1\n elif primary == \"dex_buy\":\n n_dex += 1\n elif primary == \"p2p\":\n n_p2p += 1\n elif primary == \"cex_withdraw\":\n n_cex += 1\n\n summary = {\n \"n_addrs_queried\": len(addrs),\n \"n_addrs_with_data\": sum(1 for v in attributions.values() if v[\"total\"] > 0),\n \"n_mining_fed\": n_mining,\n \"n_dex_fed\": n_dex,\n \"n_p2p_fed\": n_p2p,\n \"n_cex_fed\": n_cex,\n }\n\n return {\n \"attributions\": attributions, \"summary\": summary,\n \"_debug\": {\n \"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor_clamped,\n \"lookup_window_floor_requested\": date_floor,\n \"n_dex_pairs_known\": len(dex_pairs),\n \"n_cex_known\": len(cex_a),\n },\n }\n\n\n# ---------------------------------------------------------------------------\n# v0.7.23.2: mining-fed wallet outflow detector\n# ---------------------------------------------------------------------------\n#\n# attribute_funding() above tells us which high-value addresses received their\n# tokens via mint (= are operators of bridge / mining / vesting contracts).\n# v0.7.23.1 stopped there — the report flagged mining-fed wallets but didn't\n# tell retail what they DID with the tokens. Now we follow up: for each\n# mining-fed address, query its 365d DEX swap-sells (token_sold = ca,\n# tx_from = addr) and its outgoing transfer destinations. That tells the\n# reader \"operator X received Y tokens from mint, dumped Z on DEX for $USD,\n# routed W to destinations D1/D2/D3\". This is the data dump_tracker (a)(b)\n# would have surfaced if rule_11's m6 had been populated; here we recover it\n# from the funding-attribution result instead, fixing the false-zero on\n# mining-token forensic runs.\n\n# v0.7.24a: discover mint authorities — top from-0x0 minters by amount in\n# 365d window. Mint authorities are bridge/staking/airdrop contracts that\n# physically issue new supply on-chain. They are NOT necessarily in the\n# high_value_addrs set that attribute_funding scans (they only receive 0x0\n# transfers, not P2P), so the v0.7.23.x mining_fed_outflows path misses\n# them. Specifically: 0x6aa22cb8 (H bridge contract) minted 132.5B H over\n# 30d and self-DEX-dumped 19.8B (≈ $3M) — never surfaced before.\n_SQL_FIND_MINT_AUTHORITIES = \"\"\"SELECT \"to\" AS authority, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS total_minted, count() AS n_mints FROM {transfers} WHERE contract_address = '{ca_lc}' AND \"from\" = '0x0000000000000000000000000000000000000000' AND block_date >= '{date_floor}' GROUP BY authority ORDER BY total_minted DESC LIMIT {top_n}\"\"\"\n\n_SQL_MINING_FED_DEX_SELL = \"\"\"SELECT tx_from AS addr, count() AS n_swaps, sum(token_sold_amount) AS tokens_sold, sum(if(amount_usd > 0, amount_usd, 0)) AS sold_usd FROM {dex_trades} WHERE token_sold_address = '{ca_lc}' AND tx_from IN {in_list} AND block_date >= '{date_floor}' GROUP BY addr ORDER BY tokens_sold DESC\"\"\"\n\n_SQL_MINING_FED_OUTFLOWS = \"\"\"SELECT \"from\" AS src, \"to\" AS dest, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt, count() AS n_tx FROM {transfers} WHERE contract_address = '{ca_lc}' AND \"from\" IN {in_list} AND \"to\" != '0x0000000000000000000000000000000000000000' AND \"to\" != '0x000000000000000000000000000000000000dead' AND block_date >= '{date_floor}' GROUP BY src, dest ORDER BY amt DESC LIMIT 200\"\"\"\n\n# v0.7.24e.1: merged outflows + balances in 1 UNION ALL SQL. Both query\n# the same transfers partition (contract + block_date filter) so\n# ClickHouse can scan once. Row type marked by `rt` column. Python pivots.\n# Saves 1 surf round-trip + 1 credit per query_mining_fed_outflows call\n# (and v0.7.24a mint_authority_dumps reuses same helper → 2 saves total\n# per pipeline).\n_SQL_MINING_FED_TRANSFERS_MERGED = \"\"\"WITH ins AS (SELECT \"to\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' AND \"to\" IN {in_list} GROUP BY a), outs AS (SELECT \"from\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' AND \"from\" IN {in_list} GROUP BY a) SELECT 'balance' AS rt, r.a AS src, '' AS dest, COALESCE(ins.amt, 0) AS amt, 0 AS n_tx, COALESCE(ins.amt, 0) - COALESCE(outs.amt, 0) AS balance, COALESCE(outs.amt, 0) AS total_out FROM (SELECT arrayJoin({array_list}) AS a) r LEFT JOIN ins ON r.a = ins.a LEFT JOIN outs ON r.a = outs.a UNION ALL SELECT 'outflow' AS rt, \"from\" AS src, \"to\" AS dest, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt, count() AS n_tx, 0 AS balance, 0 AS total_out FROM {transfers} WHERE contract_address = '{ca_lc}' AND \"from\" IN {in_list} AND \"to\" != '0x0000000000000000000000000000000000000000' AND \"to\" != '0x000000000000000000000000000000000000dead' AND block_date >= '{date_floor}' GROUP BY src, dest\"\"\"\n\n# v0.7.23.4: per-addr current balance = sum(in) − sum(out) over the 365d\n# window. Mining-fed wallets fully unwound (balance ≈ 0 after dump) read as\n# \"已 dump 完毕\" stock; wallets still holding read as \"还在累积/未完全出货\".\n_SQL_MINING_FED_BALANCES = \"\"\"WITH ins AS (SELECT \"to\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' AND \"to\" IN {in_list} GROUP BY a), outs AS (SELECT \"from\" AS a, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' AND \"from\" IN {in_list} GROUP BY a) SELECT r.a AS addr, COALESCE(ins.amt, 0) AS total_in, COALESCE(outs.amt, 0) AS total_out, COALESCE(ins.amt, 0) - COALESCE(outs.amt, 0) AS balance FROM (SELECT arrayJoin({array_list}) AS a) r LEFT JOIN ins ON r.a = ins.a LEFT JOIN outs ON r.a = outs.a\"\"\"\n\n\ndef query_mining_fed_outflows(\n *,\n ca: str,\n mining_fed_addrs: list[str],\n date_floor: str = \"2020-01-01\",\n max_addrs: int = 30,\n) -> dict[str, Any]:\n \"\"\"For each mining-fed address, surface DEX dump activity + top\n outflow destinations.\n\n Args:\n ca: contract address.\n mining_fed_addrs: addresses with is_mining_fed=True from\n attribute_funding(). Capped to max_addrs (mining-fed\n addresses are typically \u003c10 per token; cap defends against\n pathological cases).\n date_floor: 'YYYY-MM-DD'. Same surf 365-day window clamp as\n attribute_funding.\n max_addrs: hard cap.\n\n Returns dict:\n per_addr: {\n addr: {\n \"dex_sells\": {\"n_swaps\": int, \"tokens_sold\": float,\n \"sold_usd\": float},\n \"top_destinations\": [\n {\"dest\": addr, \"amt\": float, \"n_tx\": int}, ...\n ],\n \"total_outflow_tokens\": float,\n }\n }\n summary: {\n \"n_addrs_with_dex_sells\": int,\n \"total_dex_sold_tokens\": float,\n \"total_dex_sold_usd\": float,\n \"total_outflow_tokens\": float,\n }\n _debug, _error (on hard failure).\n \"\"\"\n ca = (ca or \"\").lower()\n if not ca or not _chain_is_valid_addr(ca):\n return {\n \"per_addr\": {}, \"summary\": _empty_mfo_summary(),\n \"_error\": f\"invalid ca: {ca!r}\",\n }\n\n addrs = _clean_addrs(mining_fed_addrs)\n truncated_n = max(0, len(addrs) - max_addrs)\n addrs = addrs[:max_addrs]\n if not addrs:\n return {\n \"per_addr\": {}, \"summary\": _empty_mfo_summary(),\n \"_debug\": {\"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor},\n }\n\n # Clamp to surf 365-day window (same logic as attribute_funding).\n surf_window_floor = (date.today() - timedelta(days=_SURF_MAX_LOOKBACK_DAYS)).isoformat()\n date_floor_clamped = surf_window_floor if date_floor \u003c surf_window_floor else date_floor\n\n in_list = \"(\" + \",\".join(f\"'{a}'\" for a in addrs) + \")\"\n array_list = \"[\" + \",\".join(f\"'{a}'\" for a in addrs) + \"]\"\n sql_sell = _SQL_MINING_FED_DEX_SELL.format(\n dex_trades=dex_trades_table(), decimals_factor=decimals_factor_str(), ca_lc=ca,\n in_list=in_list, date_floor=date_floor_clamped,\n )\n # v0.7.24e.1: merged outflows + balances into 1 SQL (UNION ALL)\n sql_transfers_merged = _SQL_MINING_FED_TRANSFERS_MERGED.format(\n transfers=transfers_table(), decimals_factor=decimals_factor_str(), ca_lc=ca,\n in_list=in_list, array_list=array_list,\n date_floor=date_floor_clamped,\n )\n\n try:\n from section_a_scope import _run_surf_with_retry\n doc_sell, err_sell = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql_sell, \"max_rows\": max_addrs + 10}),\n base_timeout=60, max_attempts=4,\n )\n doc_transfers, err_transfers = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql_transfers_merged, \"max_rows\": max_addrs + 210}),\n base_timeout=60, max_attempts=4,\n )\n except Exception as e:\n return {\n \"per_addr\": {}, \"summary\": _empty_mfo_summary(),\n \"_error\": f\"exception: {str(e)[:120]}\",\n \"_debug\": {\"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor_clamped},\n }\n\n if not doc_sell and not doc_transfers:\n return {\n \"per_addr\": {}, \"summary\": _empty_mfo_summary(),\n \"_error\": f\"both queries failed: sell={err_sell}; transfers={err_transfers}\",\n \"_debug\": {\"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor_clamped},\n }\n\n # v0.7.24e.1: split merged result by rt column → simulate doc_bal + doc_out\n # Same per-row schema as before so downstream loops untouched.\n doc_bal = None\n doc_out = None\n if doc_transfers:\n bal_rows = []\n out_rows = []\n for r in (doc_transfers.get(\"data\") or []):\n rt = (r.get(\"rt\") or \"\")\n if rt == \"balance\":\n bal_rows.append({\n \"addr\": r.get(\"src\"),\n \"total_in\": r.get(\"amt\"),\n \"total_out\": r.get(\"total_out\"),\n \"balance\": r.get(\"balance\"),\n })\n elif rt == \"outflow\":\n out_rows.append({\n \"src\": r.get(\"src\"), \"dest\": r.get(\"dest\"),\n \"amt\": r.get(\"amt\"), \"n_tx\": r.get(\"n_tx\"),\n })\n # Sort outflows by amt desc + apply implicit LIMIT 200 (Python-side\n # since UNION ALL can't have per-branch LIMIT without subquery)\n out_rows.sort(key=lambda r: float(r.get(\"amt\") or 0), reverse=True)\n doc_bal = {\"data\": bal_rows}\n doc_out = {\"data\": out_rows[:200]}\n\n per_addr: dict[str, dict[str, Any]] = {a: {\n \"dex_sells\": {\"n_swaps\": 0, \"tokens_sold\": 0.0, \"sold_usd\": 0.0},\n \"top_destinations\": [],\n \"total_outflow_tokens\": 0.0,\n # v0.7.23.4: current balance (in − out over 365d window). For\n # mining-fed wallets that fully unwound, balance ≈ 0 reads as\n # \"dumped clean\"; balance > 0 means operator still sitting on\n # unsold mint.\n \"current_balance\": 0.0,\n \"total_in_365d\": 0.0,\n \"total_out_365d\": 0.0,\n } for a in addrs}\n\n # v0.7.23.4: stitch in per-addr balance from the third query\n if doc_bal:\n for row in (doc_bal.get(\"data\") or []):\n a = (row.get(\"addr\") or \"\").lower()\n if a not in per_addr:\n continue\n try:\n per_addr[a][\"current_balance\"] = float(row.get(\"balance\") or 0)\n per_addr[a][\"total_in_365d\"] = float(row.get(\"total_in\") or 0)\n per_addr[a][\"total_out_365d\"] = float(row.get(\"total_out\") or 0)\n except (TypeError, ValueError):\n pass\n\n if doc_sell:\n for row in (doc_sell.get(\"data\") or []):\n a = (row.get(\"addr\") or \"\").lower()\n if a not in per_addr:\n continue\n per_addr[a][\"dex_sells\"] = {\n \"n_swaps\": int(row.get(\"n_swaps\") or 0),\n \"tokens_sold\": float(row.get(\"tokens_sold\") or 0),\n \"sold_usd\": float(row.get(\"sold_usd\") or 0),\n }\n\n if doc_out:\n # Group outflows by source addr, keep top destinations per source.\n for row in (doc_out.get(\"data\") or []):\n src = (row.get(\"src\") or \"\").lower()\n dest = (row.get(\"dest\") or \"\").lower()\n amt = float(row.get(\"amt\") or 0)\n n_tx = int(row.get(\"n_tx\") or 0)\n if src not in per_addr or amt \u003c= 0:\n continue\n per_addr[src][\"total_outflow_tokens\"] += amt\n per_addr[src][\"top_destinations\"].append({\n \"dest\": dest, \"amt\": amt, \"n_tx\": n_tx,\n })\n # Trim each addr's top destinations to top 5\n # v0.8.2.2 audit HIGH #1 fix: also expose true destination\n # count (before slicing) so fake_mining_detector can gate the\n # `low_destination_count` signal on actual data, not the trimmed\n # top 5. Without this every clamped slice always tripped the\n # signal and real mining distributions would mis-fire.\n for a, v in per_addr.items():\n v[\"top_destinations\"].sort(key=lambda d: d[\"amt\"], reverse=True)\n v[\"n_destinations_total\"] = len(v[\"top_destinations\"])\n v[\"top_destinations\"] = v[\"top_destinations\"][:5]\n\n summary = {\n \"n_addrs_with_dex_sells\": sum(\n 1 for v in per_addr.values() if v[\"dex_sells\"][\"n_swaps\"] > 0\n ),\n \"total_dex_sold_tokens\": sum(\n v[\"dex_sells\"][\"tokens_sold\"] for v in per_addr.values()\n ),\n \"total_dex_sold_usd\": sum(\n v[\"dex_sells\"][\"sold_usd\"] for v in per_addr.values()\n ),\n \"total_outflow_tokens\": sum(\n v[\"total_outflow_tokens\"] for v in per_addr.values()\n ),\n # v0.7.23.4: aggregate current balance across all mining-fed wallets.\n # Reads as \"operator pool still sitting on this much potential dump.\"\n \"total_current_balance\": sum(\n v[\"current_balance\"] for v in per_addr.values()\n ),\n \"n_addrs\": len(per_addr),\n }\n return {\n \"per_addr\": per_addr, \"summary\": summary,\n \"_debug\": {\n \"sql_truncated_addr_n\": truncated_n,\n \"lookup_window_floor\": date_floor_clamped,\n \"lookup_window_floor_requested\": date_floor,\n },\n }\n\n\ndef _empty_mfo_summary() -> dict[str, Any]:\n return {\n \"n_addrs_with_dex_sells\": 0,\n \"total_dex_sold_tokens\": 0.0,\n \"total_dex_sold_usd\": 0.0,\n \"total_outflow_tokens\": 0.0,\n }\n\n\ndef discover_mint_authorities(\n *,\n ca: str,\n date_floor: str = \"2020-01-01\",\n exclude_addrs: list[str] | None = None,\n top_n: int = 10,\n min_pct_supply: float = 0.001,\n total_supply: float | None = None,\n) -> dict[str, Any]:\n \"\"\"v0.7.24a: enumerate top mint authorities (top from-0x0 minters).\n\n Bridge / staking / airdrop / vesting contracts that issue new supply by\n receiving from the zero address. Distinct from \"deployer\" (which the\n project may name as the human EOA placeholder) and distinct from\n \"mining-fed wallets\" (which receive their tokens from mint authorities,\n not from 0x0 directly).\n\n Args:\n ca: contract address.\n date_floor: 'YYYY-MM-DD'. Same surf 365-day window clamp.\n exclude_addrs: deployer + already-known mining-fed addrs to skip\n (we don't want to double-count their dump activity in the\n downstream render).\n top_n: SQL LIMIT on minter list.\n min_pct_supply: drop authorities that mint less than this fraction\n of total_supply (filters noise — small mint sources like\n occasional reward drips).\n total_supply: Alpha-API nominal supply. Used to compute mint_pct\n per authority and to filter via min_pct_supply.\n\n Returns dict:\n authorities: list of {addr, total_minted, mint_pct_supply,\n n_mints, is_excluded: bool}\n summary: {n_authorities, total_minted_aggregate}\n _debug: {sql_floor, exclude_n}\n _error: only on hard failure.\n \"\"\"\n ca = (ca or \"\").lower()\n if not ca or not _chain_is_valid_addr(ca):\n return {\n \"authorities\": [], \"summary\": {\n \"n_authorities\": 0, \"total_minted_aggregate\": 0.0,\n },\n \"_error\": f\"invalid ca: {ca!r}\",\n }\n\n surf_window_floor = (date.today() - timedelta(days=_SURF_MAX_LOOKBACK_DAYS)).isoformat()\n date_floor_clamped = surf_window_floor if date_floor \u003c surf_window_floor else date_floor\n\n exclude_set = {a.lower() for a in _clean_addrs(exclude_addrs)}\n\n sql = _SQL_FIND_MINT_AUTHORITIES.format(\n transfers=transfers_table(), decimals_factor=decimals_factor_str(), ca_lc=ca,\n date_floor=date_floor_clamped, top_n=top_n,\n )\n\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": top_n + 10}),\n base_timeout=60, max_attempts=4,\n )\n except Exception as e:\n return {\n \"authorities\": [], \"summary\": {\n \"n_authorities\": 0, \"total_minted_aggregate\": 0.0,\n },\n \"_error\": f\"exception: {str(e)[:120]}\",\n }\n\n if not doc:\n return {\n \"authorities\": [], \"summary\": {\n \"n_authorities\": 0, \"total_minted_aggregate\": 0.0,\n },\n \"_error\": err or \"surf returned no doc\",\n }\n\n authorities: list[dict[str, Any]] = []\n total_minted_agg = 0.0\n min_amt = (total_supply * min_pct_supply) if total_supply else 0.0\n for row in (doc.get(\"data\") or []):\n addr = (row.get(\"authority\") or \"\").lower()\n if not _chain_is_valid_addr(addr) or addr in _DEAD:\n continue\n try:\n amt = float(row.get(\"total_minted\") or 0)\n n_mints = int(row.get(\"n_mints\") or 0)\n except (TypeError, ValueError):\n continue\n if amt \u003c min_amt:\n continue\n mint_pct = (amt / total_supply * 100) if (total_supply and total_supply > 0) else None\n is_excluded = addr in exclude_set\n authorities.append({\n \"addr\": addr,\n \"total_minted\": amt,\n \"mint_pct_supply\": mint_pct,\n \"n_mints\": n_mints,\n \"is_excluded\": is_excluded,\n })\n if not is_excluded:\n total_minted_agg += amt\n\n return {\n \"authorities\": authorities,\n \"summary\": {\n \"n_authorities\": sum(1 for a in authorities if not a[\"is_excluded\"]),\n \"total_minted_aggregate\": total_minted_agg,\n },\n \"_debug\": {\n \"sql_floor\": date_floor_clamped,\n \"exclude_n\": len(exclude_set),\n \"min_pct_supply_filter\": min_pct_supply,\n },\n }\n\n\n# v0.7.24b: high-throughput dump trace SQL — detect wallets that received\n# a large quantity of tokens but cleared out (balance ≈ 0), via many txs.\n# Catches sss_crypto's @sss_crypto thread addresses (0x47a6e4e1: 79k tx,\n# 30.6M throughput, 0 balance) which all existing detectors miss because:\n# - rule_11: deployer placeholder, m6 empty\n# - wash_infra: pattern is not atomic\n# - flow_operators: 60d window (these dumped 60d ago)\n# - mining_fed: balance is below the mint-fed threshold (they receive\n# via p2p from authorities, not directly from 0x0)\n# - mint_authority: they're receivers, not 0x0-issuers\n# - top_holders: balance ≈ 0 keeps them off top-30\n_SQL_HIGH_THROUGHPUT = \"\"\"WITH ins AS (SELECT \"to\" AS addr, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt_in, count() AS n_in FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' GROUP BY addr), outs AS (SELECT \"from\" AS addr, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt_out, count() AS n_out FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' GROUP BY addr) SELECT ins.addr, ins.amt_in AS total_in, COALESCE(outs.amt_out, 0) AS total_out, ins.amt_in - COALESCE(outs.amt_out, 0) AS balance, ins.n_in + COALESCE(outs.n_out, 0) AS n_tx FROM ins LEFT JOIN outs ON ins.addr = outs.addr WHERE ins.amt_in >= {min_throughput} AND ins.amt_in \u003c= {max_throughput} AND abs(ins.amt_in - COALESCE(outs.amt_out, 0)) \u003c ins.amt_in * {max_balance_frac} AND ins.n_in + COALESCE(outs.n_out, 0) >= {min_n_tx} AND ins.addr != '0x0000000000000000000000000000000000000000' AND ins.addr != '0x000000000000000000000000000000000000dead' ORDER BY ins.amt_in DESC LIMIT {top_n}\"\"\"\n\n# v0.9.4 + audit fixes: chunkable variant. Per-chunk SQL uses BETWEEN\n# (partition pruning) so each chunk only scans ~30d of bsc_transfers. The\n# 2-layer CTE form is preserved per chunk because at 30d window the ins/outs\n# materialization is small enough that ClickHouse handles it inside 30s.\n#\n# Filters that survive per-chunk:\n# - addr != 0x0 / 0xdead (correctness — these are noise)\n# - amt_in >= chunk_min_in (1% floor of global threshold; bounds result\n# size without divisor undercount, see chunk_min_in comment)\n# - amt_in \u003c= max_throughput (adversarial review Fix 2: this filter is MONOTONIC —\n# per-chunk amt_in ≤ global amt_in, so per-chunk > max implies global\n# > max. Safe to pre-filter at SQL. Without this, wash bots with huge\n# per-chunk amt_in fill the LIMIT and push real operators out.)\n#\n# Filters MOVED to Python post-merge (cannot apply per-chunk correctly):\n# - min_throughput (totals only known after merge)\n# - max_balance_frac (balance = sum_in - sum_out, totals)\n# - min_n_tx (count distributes via merge)\n# - ORDER BY amt_in DESC LIMIT top_n (global top)\n#\n# chunk_top_n is a per-chunk safety cap. Caller queries LIMIT chunk_top_n+1\n# and fails loud if exactly chunk_top_n+1 rows returned (adversarial review Fix 2:\n# truncation detection — capped chunk = incomplete merge input, must NOT\n# emit clean empty findings).\n_SQL_HIGH_THROUGHPUT_CHUNK = \"\"\"WITH ins AS (SELECT \"to\" AS addr, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt_in, count() AS n_in FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date BETWEEN '{chunk_floor}' AND '{chunk_ceiling}' GROUP BY addr), outs AS (SELECT \"from\" AS addr, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt_out, count() AS n_out FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date BETWEEN '{chunk_floor}' AND '{chunk_ceiling}' GROUP BY addr) SELECT ins.addr AS addr, ins.amt_in AS total_in, COALESCE(outs.amt_out, 0) AS total_out, ins.n_in AS n_in, COALESCE(outs.n_out, 0) AS n_out FROM ins LEFT JOIN outs ON ins.addr = outs.addr WHERE ins.amt_in >= {chunk_min_in} AND ins.amt_in \u003c= {max_throughput} AND ins.addr != '0x0000000000000000000000000000000000000000' AND ins.addr != '0x000000000000000000000000000000000000dead' ORDER BY ins.amt_in DESC LIMIT {chunk_top_n_plus_1}\"\"\"\n\n\ndef discover_high_throughput_dumpers(\n *,\n ca: str,\n date_floor: str = \"2020-01-01\",\n exclude_addrs: list[str] | None = None,\n min_throughput: float = 1_000_000.0,\n max_throughput: float | None = None,\n max_balance_frac: float = 0.05,\n min_n_tx: int = 1000,\n top_n: int = 50,\n total_supply: float | None = None,\n) -> dict[str, Any]:\n \"\"\"v0.7.24b: enumerate wallets with high-throughput / clean-dump pattern.\n\n Detect operator wallets that have ingested a large quantity of tokens\n over the 365d window but cleared out (balance ≈ 0), via many txs. The\n sss_crypto Twitter forensic thread on H called out exactly this pattern:\n 0x47a6e4e1 received 30.6M, sent 30.5M (residual 0.09M), 79,048 txs over\n 365d. These operators are deliberately invisible to:\n - rule_11 (deployer placeholder model)\n - wash_infra (P→X→Q async, not atomic)\n - flow_operators (60d window misses earlier dump)\n - mining_fed (receive via p2p from authorities, not directly from 0x0)\n - mint_authority (they're receivers, not 0x0-issuers)\n - top_holders (balance ≈ 0 keeps them off the table)\n\n Default thresholds tuned to H/sss_crypto cohort:\n - min_throughput=1M: filters small dust; 0x47a6e4e1 has 30.6M\n - max_balance_frac=0.05: balance within 5% of throughput counts as\n \"cleared out\"; 0x47a6e4e1 has 92k residual on 30.6M = 0.3%\n - min_n_tx=1000: filters one-shot transfers; 0x47a6e4e1 has 79k\n\n Args:\n ca: contract address.\n date_floor: 'YYYY-MM-DD', surf 365d window clamp applied.\n exclude_addrs: addrs already covered by other detectors (mining-fed,\n mint authority, deployer) — don't double-surface them.\n min_throughput: minimum tokens received in window.\n max_balance_frac: fraction of throughput allowed as residual balance.\n min_n_tx: minimum (in_count + out_count) tx count.\n top_n: SQL LIMIT (cap on output rows).\n\n Returns:\n {\n \"dumpers\": list of {addr, total_in, total_out, balance, n_tx,\n throughput_pct_supply, is_excluded},\n \"summary\": {n_dumpers, total_throughput, total_throughput_pct_supply},\n \"_debug\": {date_floor_clamped, min_throughput, ...},\n \"_error\": only on hard failure.\n }\n \"\"\"\n ca = (ca or \"\").lower()\n if not ca or not _chain_is_valid_addr(ca):\n return {\n \"dumpers\": [], \"summary\": {\n \"n_dumpers\": 0, \"total_throughput\": 0.0,\n },\n \"_error\": f\"invalid ca: {ca!r}\",\n }\n\n surf_window_floor = (date.today() - timedelta(days=_SURF_MAX_LOOKBACK_DAYS)).isoformat()\n date_floor_clamped = surf_window_floor if date_floor \u003c surf_window_floor else date_floor\n exclude_set = {a.lower() for a in _clean_addrs(exclude_addrs)}\n\n # v0.7.24b filter: wash bots can churn 100M-8B tokens (1-80% of nominal\n # supply churned multiple times). They are NOT clean-dump operators —\n # they cycle the same liquidity many times. Operator allocation dumps\n # (sss_crypto thread profile: 0x47a6e4e1 = 30M / 0.3% nominal) sit\n # below 5% of nominal supply. Default cap = 5% of nominal supply.\n # Above that, the wallet is almost certainly a wash bot / MM / DEX\n # router, not a one-shot operator wallet liquidating an allocation.\n if max_throughput is None:\n max_throughput = (float(total_supply) * 0.05) if total_supply else 5e17\n\n # v0.9.4 + audit fix 5: chunked path for long windows. Short windows\n # collapse to a single chunk → SQL shape preserved as BETWEEN, identical\n # behavior to v0.9.3 single-shot. Long windows split to 30d chunks\n # (NOT 90d — funding_attribution heavy CTE is per-chunk heavier than\n # rule_11's selective mint queries, so 90d may still hit 30s budget on\n # EVAA-class tokens. 30d × 12-13 chunks is a known-safe budget).\n transfers = transfers_table()\n chunks = _single_chunk_or_chunker_dates(date_floor_clamped, None, chunk_days=30)\n # Per-chunk min_in floor: use 1% of global threshold (NOT divide by\n # N_chunks). Reason: an N_chunks divisor undercounts wallets whose\n # throughput is uneven across chunks. 1% floor (= 10K for 1M threshold)\n # keeps all chunks contributing to the global total. Hard floor 1000\n # to bound result size on partitions with millions of dust addrs.\n chunk_min_in = max(1000.0, float(min_throughput) * 0.01)\n # v0.9.4 round-2 adversarial review Fix 1: cap chunk_top_n below surf 10K max_rows\n # ceiling. Naïve top_n * 100 with default top_n=100 yields 10000 →\n # LIMIT 10001 → max_rows 10010 → every chunk rejected. Stay safe.\n chunk_top_n = min(top_n * 100, _SURF_SAFE_MAX_ROWS - 10)\n\n def _sql_fn(chunk_floor: str, chunk_ceiling: str) -> dict[str, Any]:\n # adversarial review Fix 2: LIMIT chunk_top_n + 1. If response has exactly\n # that many rows, the chunk was capped → not a complete merge\n # input → must propagate as truncation error, not silently merge.\n sql = _SQL_HIGH_THROUGHPUT_CHUNK.format(\n transfers=transfers, ca_lc=ca,\n chunk_floor=chunk_floor, chunk_ceiling=chunk_ceiling,\n chunk_min_in=chunk_min_in, max_throughput=max_throughput,\n chunk_top_n_plus_1=chunk_top_n + 1,\n decimals_factor=decimals_factor_str())\n return _run_chunk_via_surf(sql, max_rows=chunk_top_n + 10, base_timeout=40)\n\n chunk_results = parallel_run_chunked(_sql_fn, chunks)\n errs = [r.get(\"_error\") for r in chunk_results if r.get(\"_error\")]\n rows_per_chunk = [r.get(\"data\") or [] for r in chunk_results]\n\n # adversarial review Fix 2: detect per-chunk truncation. If any chunk returned\n # exactly chunk_top_n + 1 rows, the LIMIT was hit → chunk's contribution\n # to the global merge is incomplete → fail loud.\n truncated_chunks = sum(\n 1 for r in rows_per_chunk if len(r) > chunk_top_n\n )\n\n # adversarial review Fix 1 (CRITICAL): partial chunk error → empty + _error. v0.9.3\n # single-shot semantics were \"either complete data or hard error\" —\n # silently returning partial data from k of N chunks is a NEW silent\n # failure mode where a true operator concentrated in the failing chunk\n # vanishes from the result. Match v0.9.3 strictness: any chunk error\n # → empty result + _error so caller behaves the same as before.\n if errs or truncated_chunks > 0:\n return {\n \"dumpers\": [], \"summary\": {\n \"n_dumpers\": 0, \"total_throughput\": 0.0,\n },\n \"_error\": (\n f\"{len(errs)}/{len(chunks)} chunks errored, \"\n f\"{truncated_chunks}/{len(chunks)} chunks truncated at LIMIT; \"\n f\"first chunk error: {errs[0] if errs else 'none'}\"\n ),\n \"_debug\": {\n \"date_floor_clamped\": date_floor_clamped,\n \"chunks\": chunk_summary(chunks),\n \"chunk_errs\": len(errs),\n \"chunk_truncated\": truncated_chunks,\n },\n }\n\n # Merge by addr — SUM is distributive over the chunk partition.\n # n_in / n_out are per-chunk counts; total n_tx = sum(n_in)+sum(n_out).\n merged = merge_chunked_rows(\n rows_per_chunk, key_field=\"addr\",\n sum_fields=[\"total_in\", \"total_out\", \"n_in\", \"n_out\"],\n )\n\n # Apply Python-side filters (moved from SQL HAVING / WHERE):\n # min_throughput / max_throughput, max_balance_frac, min_n_tx\n # Order by total_in DESC, LIMIT top_n — matches v0.9.3 single-shot\n # ORDER BY ins.amt_in DESC LIMIT top_n semantics.\n dumpers: list[dict[str, Any]] = []\n total_through = 0.0\n for r in merged:\n addr = (r.get(\"addr\") or \"\").lower()\n if not _chain_is_valid_addr(addr) or addr in _DEAD:\n continue\n tin = float(r.get(\"total_in\") or 0)\n tout = float(r.get(\"total_out\") or 0)\n bal = tin - tout\n n_tx = int((r.get(\"n_in\") or 0) + (r.get(\"n_out\") or 0))\n # Python-side filters (parity with v0.9.3 SQL HAVING):\n if tin \u003c min_throughput or tin > max_throughput:\n continue\n if tin > 0 and abs(bal) >= tin * max_balance_frac:\n continue\n if n_tx \u003c min_n_tx:\n continue\n is_excluded = addr in exclude_set\n dumpers.append({\n \"addr\": addr,\n \"total_in\": tin,\n \"total_out\": tout,\n \"balance\": bal,\n \"n_tx\": n_tx,\n \"is_excluded\": is_excluded,\n })\n # Sort + LIMIT (matches v0.9.3 ORDER BY ins.amt_in DESC LIMIT top_n)\n dumpers.sort(key=lambda d: d[\"total_in\"], reverse=True)\n dumpers = dumpers[:top_n]\n for d in dumpers:\n if not d[\"is_excluded\"]:\n total_through += d[\"total_in\"]\n\n return {\n \"dumpers\": dumpers,\n \"summary\": {\n \"n_dumpers\": sum(1 for d in dumpers if not d[\"is_excluded\"]),\n \"total_throughput\": total_through,\n },\n \"_debug\": {\n \"date_floor_clamped\": date_floor_clamped,\n \"min_throughput\": min_throughput,\n \"max_balance_frac\": max_balance_frac,\n \"min_n_tx\": min_n_tx,\n \"exclude_n\": len(exclude_set),\n \"chunks\": chunk_summary(chunks),\n \"chunk_min_in\": chunk_min_in,\n \"chunk_errs\": len(errs),\n },\n }\n\n\n# v0.7.24e: CEX deposit fan-out detector — catch sss_crypto Twitter thread\n# profile (BEAT 2026-05-21: Gate.io 热钱包 → 1 hub → 10 sub-wallets each\n# holding 0.1%+ supply). Pattern: operator pulls allocation from CEX hot\n# wallet, routes through one intermediate hub, splits to N fresh wallets\n# to disperse top100 concentration signal.\n#\n# Two-phase SQL: (1) find hub candidates by fan-out signature, (2) per-hub\n# senders + recipients. Python label filter narrows to true CEX-origin\n# fan-out.\n_SQL_FANOUT_HUB_CANDIDATES = \"\"\"WITH recipient_amounts AS (SELECT \"from\" AS hub, \"to\" AS recipient, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS rec_amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date >= '{date_floor}' AND \"to\" != '0x0000000000000000000000000000000000000000' AND \"to\" != '0x000000000000000000000000000000000000dead' GROUP BY hub, recipient HAVING rec_amt >= {min_per_recipient}) SELECT hub, count() AS n_recipients, sum(rec_amt) AS total_out, min(rec_amt) AS min_per, avg(rec_amt) AS avg_per FROM recipient_amounts GROUP BY hub HAVING n_recipients BETWEEN {min_recipients} AND {max_recipients} AND total_out >= {min_total_out} ORDER BY total_out DESC LIMIT {top_n}\"\"\"\n\n# v0.9.4 + audit fixes: chunkable variant. Per-chunk SQL outputs raw\n# (hub, recipient, rec_amt) pairs — the outer GROUP BY hub + hub-level\n# HAVING happens entirely in Python after the per-(hub, recipient) merge.\n#\n# Per-chunk filter `rec_amt >= chunk_min_rec_amt` uses a 1% floor (NOT\n# N_chunks divisor — see discover_high_throughput_dumpers comment for\n# undercount-avoidance rationale). addr != 0x0 / 0xdead enforced in WHERE\n# rather than HAVING (same effect, cheaper on the ClickHouse plan).\n#\n# LIMIT chunk_top_pairs+1: adversarial review Fix 3 — caller queries with +1 cap and\n# fails loud if exactly chunk_top_pairs+1 rows returned (truncation\n# detected). Without truncation detect, a capped chunk silently feeds\n# incomplete data to the hub merge, where junk pairs can mask a real\n# operator hub.\n_SQL_FANOUT_HUB_CANDIDATES_CHUNK = \"\"\"SELECT \"from\" AS hub, \"to\" AS recipient, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS rec_amt FROM {transfers} WHERE contract_address = '{ca_lc}' AND block_date BETWEEN '{chunk_floor}' AND '{chunk_ceiling}' AND \"to\" != '0x0000000000000000000000000000000000000000' AND \"to\" != '0x000000000000000000000000000000000000dead' GROUP BY hub, recipient HAVING rec_amt >= {chunk_min_rec_amt} ORDER BY rec_amt DESC LIMIT {chunk_top_pairs_plus_1}\"\"\"\n\n# v0.7.24e.1: merge senders + recipients into 1 UNION ALL SQL.\n# Both subqueries use IN {hub_in_list} on the same transfers table /\n# date_floor; ClickHouse scans the partition once for the union. Saves\n# 1 round-trip + 1 surf credit. Output rows tagged by `rt` column\n# ('sender' or 'recipient'), Python pivots by rt.\n# v0.8.1 audit CRITICAL fix: the original UNION ALL query shared\n# one max_rows budget between sender + recipient rows, with no ORDER BY,\n# so a hub with many small recipients could silently truncate (top\n# senders could even be dropped). Split into two queries with explicit\n# ORDER BY amt DESC + sentinel rows so the caller can detect truncation\n# and fail-loud rather than emit a stale net metric.\n_SQL_FANOUT_HUB_SENDERS = \"\"\"SELECT \"to\" AS hub, \"from\" AS counterparty, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt, count() AS n_tx, min(block_time) AS first_tx FROM {transfers} WHERE contract_address = '{ca_lc}' AND \"to\" IN {hub_in_list} AND block_date >= '{date_floor}' GROUP BY hub, counterparty ORDER BY amt DESC\"\"\"\n_SQL_FANOUT_HUB_RECIPIENTS = \"\"\"SELECT \"from\" AS hub, \"to\" AS counterparty, sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor}) AS amt, count() AS n_tx, min(block_time) AS first_tx FROM {transfers} WHERE contract_address = '{ca_lc}' AND \"from\" IN {hub_in_list} AND block_date >= '{date_floor}' AND \"to\" != '0x0000000000000000000000000000000000000000' AND \"to\" != '0x000000000000000000000000000000000000dead' GROUP BY hub, counterparty ORDER BY amt DESC\"\"\"\n\n\ndef discover_cex_fanout_hubs(\n *,\n ca: str,\n date_floor: str = \"2020-01-01\",\n min_recipients: int = 5,\n max_recipients: int = 50,\n min_per_recipient: float = 100_000.0,\n min_total_out: float = 1_000_000.0,\n top_n_hubs: int = 30,\n max_recipients_per_hub_display: int = 20,\n) -> dict[str, Any]:\n \"\"\"v0.7.24e: detect CEX-deposit fan-out hubs.\n\n sss_crypto's BEAT thread (2026-05-21) catalogued the pattern: 11.4M\n BEAT pulled from Gate hot wallet → 1 intermediate hub → 10 sub-wallets\n each holding 0.1%+ supply. Designed to disperse top100 concentration\n signal so retail sees \"well-distributed\" holders. Our existing\n detectors miss it: rule_11 looks at deployer mint origin, mint_authority\n looks at 0x0 transfers, high_throughput needs n_tx ≥ 1000 (these hubs\n have ~11 txs total).\n\n Filter: hub has 5-50 recipients each receiving ≥ 100K tokens AND total\n outflow ≥ 1M tokens. Then per-hub, the TOP sender must be a\n CEX_DEPOSIT or CEX_HOT_WALLET classified address (Arkham label). DEX\n routers / mint contracts excluded by this filter.\n\n Args:\n ca: contract address.\n date_floor: 'YYYY-MM-DD', surf 365d window clamp applied.\n min_recipients/max_recipients: tunable hub fan-out range.\n min_per_recipient: each recipient must receive ≥ this many tokens\n (filters DEX routers' dust transfers to retail buyers).\n min_total_out: hub outflow must be ≥ this total.\n top_n_hubs: SQL LIMIT for hub candidates.\n max_recipients_per_hub_display: cap recipient list per hub in output.\n\n Returns:\n {\n \"hubs\": [{addr, n_recipients, total_out_tokens, cex_source, cex_source_label,\n cex_source_inflow_tokens, top_recipients: [{addr, amt, n_tx}], ...}],\n \"summary\": {\n \"n_confirmed_hubs\": int,\n \"n_candidate_hubs\": int,\n \"total_cex_inflow_tokens\": float,\n \"total_fanout_recipients\": int,\n },\n \"_debug\": {date_floor_clamped, ...},\n \"_error\": only on hard failure.\n }\n \"\"\"\n ca = (ca or \"\").lower()\n if not ca or not _chain_is_valid_addr(ca):\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": f\"invalid ca: {ca!r}\",\n }\n\n surf_window_floor = (date.today() - timedelta(days=_SURF_MAX_LOOKBACK_DAYS)).isoformat()\n date_floor_clamped = surf_window_floor if date_floor \u003c surf_window_floor else date_floor\n\n # v0.9.4 + audit fix 5: Phase 1 chunked. Short windows collapse to\n # single chunk (no regression for new tokens). Long windows split to\n # 30d chunks (heavier than rule_11 per-chunk; 90d may still hit 30s\n # budget on EVAA-class tokens).\n transfers = transfers_table()\n chunks = _single_chunk_or_chunker_dates(date_floor_clamped, None, chunk_days=30)\n # Per-chunk per-(hub, recipient) min: 1% of global threshold.\n chunk_min_rec_amt = max(1000.0, float(min_per_recipient) * 0.01)\n # Per-chunk LIMIT: bounded below surf max_rows cap so default pipeline\n # call (top_n_hubs=30, max_recipients=50 → naïve 30*50*5=7500) stays\n # under 9000 budget. Same _SURF_SAFE_MAX_ROWS guard as high_throughput.\n chunk_top_pairs = min(_SURF_SAFE_MAX_ROWS, top_n_hubs * max_recipients * 5)\n\n def _sql_fn(chunk_floor: str, chunk_ceiling: str) -> dict[str, Any]:\n # adversarial review Fix 3: LIMIT+1 truncation detection.\n sql = _SQL_FANOUT_HUB_CANDIDATES_CHUNK.format(\n transfers=transfers, ca_lc=ca,\n chunk_floor=chunk_floor, chunk_ceiling=chunk_ceiling,\n chunk_min_rec_amt=chunk_min_rec_amt,\n chunk_top_pairs_plus_1=chunk_top_pairs + 1,\n decimals_factor=decimals_factor_str())\n return _run_chunk_via_surf(sql, max_rows=chunk_top_pairs + 10, base_timeout=40)\n\n chunk_results = parallel_run_chunked(_sql_fn, chunks)\n errs_p1 = [r.get(\"_error\") for r in chunk_results if r.get(\"_error\")]\n rows_per_chunk_p1 = [r.get(\"data\") or [] for r in chunk_results]\n # adversarial review Fix 3: detect per-chunk truncation.\n truncated_p1 = sum(1 for r in rows_per_chunk_p1 if len(r) > chunk_top_pairs)\n\n # adversarial review Fix 1 (CRITICAL): partial chunk error / truncation → empty +\n # _error. v0.9.3 single-shot semantics. A capped chunk full of junk\n # pairs would silently mask a real operator hub in Python merge.\n if errs_p1 or truncated_p1 > 0:\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": (\n f\"phase1 {len(errs_p1)}/{len(chunks)} chunks errored, \"\n f\"{truncated_p1}/{len(chunks)} chunks truncated at LIMIT; \"\n f\"first: {errs_p1[0] if errs_p1 else 'none'}\"\n ),\n \"_debug\": {\"date_floor_clamped\": date_floor_clamped,\n \"chunks\": chunk_summary(chunks),\n \"chunk_errs_p1\": len(errs_p1),\n \"chunk_truncated_p1\": truncated_p1},\n }\n\n pair_rows: list[dict[str, Any]] = []\n for r in rows_per_chunk_p1:\n pair_rows.extend(r)\n\n # Python merge: SUM rec_amt by (hub, recipient) across chunks\n pair_totals: dict[tuple[str, str], float] = {}\n for row in pair_rows:\n h = (row.get(\"hub\") or \"\").lower()\n rc = (row.get(\"recipient\") or \"\").lower()\n if not h or not rc:\n continue\n try:\n pair_totals[(h, rc)] = pair_totals.get((h, rc), 0.0) + float(row.get(\"rec_amt\") or 0)\n except (TypeError, ValueError):\n continue\n\n # Apply per-recipient filter (parity with v0.9.3 inner HAVING)\n pair_totals = {k: v for k, v in pair_totals.items() if v >= min_per_recipient}\n\n # Group by hub: count recipients, sum total_out, min/avg per recipient\n hub_aggs: dict[str, dict[str, Any]] = {}\n for (h, rc), amt in pair_totals.items():\n agg = hub_aggs.setdefault(h, {\n \"hub\": h, \"n_recipients\": 0, \"total_out\": 0.0,\n \"min_per\": float(\"inf\"), \"amt_sum\": 0.0,\n })\n agg[\"n_recipients\"] += 1\n agg[\"total_out\"] += amt\n agg[\"amt_sum\"] += amt\n if amt \u003c agg[\"min_per\"]:\n agg[\"min_per\"] = amt\n\n # Apply hub-level filter (parity with v0.9.3 outer HAVING)\n hub_candidates: list[dict[str, Any]] = []\n for h, agg in hub_aggs.items():\n if not (min_recipients \u003c= agg[\"n_recipients\"] \u003c= max_recipients):\n continue\n if agg[\"total_out\"] \u003c min_total_out:\n continue\n hub_candidates.append({\n \"hub\": h, \"n_recipients\": agg[\"n_recipients\"],\n \"total_out\": agg[\"total_out\"],\n \"min_per\": agg[\"min_per\"],\n \"avg_per\": agg[\"amt_sum\"] / max(1, agg[\"n_recipients\"]),\n })\n hub_candidates.sort(key=lambda r: r[\"total_out\"], reverse=True)\n hub_candidates = hub_candidates[:top_n_hubs]\n\n if not hub_candidates:\n return {\n \"hubs\": [], \"summary\": {\n \"n_confirmed_hubs\": 0, \"n_candidate_hubs\": 0,\n \"total_cex_inflow_tokens\": 0.0,\n \"total_fanout_recipients\": 0,\n },\n \"_debug\": {\"date_floor_clamped\": date_floor_clamped,\n \"chunks\": chunk_summary(chunks),\n \"chunk_errs_p1\": len(errs_p1)},\n }\n\n hub_addrs = [(r.get(\"hub\") or \"\").lower() for r in hub_candidates]\n hub_addrs = [a for a in hub_addrs if _chain_is_valid_addr(a)]\n if not hub_addrs:\n return {\"hubs\": [], \"summary\": _empty_fanout_summary(), \"_debug\": {}}\n\n hub_in_list = \"(\" + \",\".join(f\"'{a}'\" for a in hub_addrs) + \")\"\n\n # v0.8.1 audit CRITICAL fix: split Phase 2 into 2 queries to\n # give each its own max_rows budget + explicit ORDER BY for stable\n # truncation. Truncation now detected per-query and surfaced via\n # `_phase2_truncated` so behavior_classifier can refuse to emit a\n # net % claim when the input is incomplete.\n # v0.8.5.0 — cap fix (用户 review 2026-06-11 + adversarial review JCT skeleton dump):\n # - recipients_max_rows 之前 = 30 hubs × 1000 + 5 = 30005, > surf 10K cap\n # 导致 SQL 整个 fail returned 0 rows. 改 9000 (\u003c 10K) 接受 truncation\n # 但至少有数据.\n # - senders_max_rows 之前 = 30 × 30 + 5 = 905, 不撞 cap 但 truncated 因\n # single hub 可能 senders > 30. 提到 9000 上限 (\u003c 10K) + per_hub_cap 300.\n # v0.8.5.4 — 用户 review (2026-06-12 CLO 案例): \"fan-out recipients\n # 钱包一定要统计完整, 因为这些一定是庄家弹药\". 之前 16 candidate hubs ×\n # 300 = 4805 \u003c 9000 cap → CLO recipients 4805/4805 truncated.\n # 直接拉满 surf 10K cap (9000), 不再 multiply by N_hubs.\n SURF_MAX_ROWS_CAP = 9000 # \u003c surf 10K hard limit, 留 1K headroom\n sql_senders = _SQL_FANOUT_HUB_SENDERS.format(\n transfers=transfers_table(), decimals_factor=decimals_factor_str(), ca_lc=ca,\n hub_in_list=hub_in_list, date_floor=date_floor_clamped,\n )\n sql_recipients = _SQL_FANOUT_HUB_RECIPIENTS.format(\n transfers=transfers_table(), decimals_factor=decimals_factor_str(), ca_lc=ca,\n hub_in_list=hub_in_list, date_floor=date_floor_clamped,\n )\n senders_max_rows = SURF_MAX_ROWS_CAP\n recipients_max_rows = SURF_MAX_ROWS_CAP\n\n try:\n doc_senders, err_senders = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql_senders, \"max_rows\": senders_max_rows}),\n base_timeout=60, max_attempts=4,\n )\n except Exception as e:\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": f\"phase2 senders exception: {str(e)[:120]}\",\n }\n # v0.9.4 round-2 adversarial review Fix 2: Phase 2 surf failure must fail loud.\n # Pre-fix: doc_senders=None silently became empty sender_rows → all\n # candidate hubs dropped because no top_sender resolved → returned\n # `hubs: []` with no _error. Caller treats clean empty as \"no hubs\n # found\" instead of \"lookup failed\".\n if not doc_senders:\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": f\"phase2 senders surf failed: {err_senders or 'no doc'}\",\n }\n try:\n doc_recipients, err_recipients = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql_recipients, \"max_rows\": recipients_max_rows}),\n base_timeout=60, max_attempts=4,\n )\n except Exception as e:\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": f\"phase2 recipients exception: {str(e)[:120]}\",\n }\n # Same fail-loud guard for recipients side. Without it, hubs can be\n # confirmed (top_sender resolved CEX) but `net_fanout_tokens_total=0`\n # because all recipient_rows are empty — false \"no fan-out\" verdict.\n if not doc_recipients:\n return {\n \"hubs\": [], \"summary\": _empty_fanout_summary(),\n \"_error\": f\"phase2 recipients surf failed: {err_recipients or 'no doc'}\",\n }\n\n # Truncation detection: if the returned row count equals or exceeds\n # the cap, the result set was likely clipped — net metrics computed\n # from this would silently understate fan-out and must NOT be emitted.\n sender_rows = doc_senders.get(\"data\") or []\n recipient_rows = doc_recipients.get(\"data\") or []\n senders_truncated = len(sender_rows) >= senders_max_rows\n recipients_truncated = len(recipient_rows) >= recipients_max_rows\n\n senders_per_hub: dict[str, list] = {}\n recipients_per_hub: dict[str, list] = {}\n for r in sender_rows:\n h = (r.get(\"hub\") or \"\").lower()\n senders_per_hub.setdefault(h, []).append({\n \"hub\": h,\n \"source\": (r.get(\"counterparty\") or \"\").lower(),\n \"amt\": r.get(\"amt\"),\n \"n_tx\": r.get(\"n_tx\"),\n \"first_tx\": r.get(\"first_tx\"),\n })\n for r in recipient_rows:\n h = (r.get(\"hub\") or \"\").lower()\n recipients_per_hub.setdefault(h, []).append({\n \"hub\": h,\n \"recipient\": (r.get(\"counterparty\") or \"\").lower(),\n \"amt\": r.get(\"amt\"),\n \"n_tx\": r.get(\"n_tx\"),\n \"first_tx\": r.get(\"first_tx\"),\n })\n\n # Determine top sender per hub (most amount in) — we'll resolve_labels on these\n top_senders: dict[str, dict] = {}\n for h, senders in senders_per_hub.items():\n senders_sorted = sorted(senders, key=lambda x: float(x.get(\"amt\") or 0), reverse=True)\n if senders_sorted:\n top_senders[h] = senders_sorted[0]\n\n # v0.8.5.4: 用户 review 2026-06-12 — fan-out recipients 必须完整.\n # 如果 bulk recipients SQL truncated (≥ 9000 rows), 对每个 hub 跑\n # 单独 query 拿 LIMIT 1000 each, override recipients_per_hub. 保证\n # 每个 hub 的 fan-out 出口钱包都被捕获. 成本: confirmed hub count\n # × 1 SQL × ~3 credits ≈ 10-30 extra credits/token (高 fan-out 场景).\n if recipients_truncated:\n import sys as _sys\n print(f\"[fanout] recipients bulk truncated ({len(recipient_rows)}/{recipients_max_rows}), \"\n f\"falling back to per-hub queries for {len(hub_addrs)} hubs\", file=_sys.stderr)\n per_hub_complete: dict[str, list] = {}\n for hub_addr in hub_addrs:\n sql_per_hub = (\n f\"SELECT '{hub_addr}' AS hub, \\\"to\\\" AS counterparty, \"\n f\"sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS amt, \"\n f\"count() AS n_tx, min(block_time) AS first_tx \"\n f\"FROM {transfers_table()} \"\n f\"WHERE contract_address = '{ca}' AND \\\"from\\\" = '{hub_addr}' \"\n f\"AND block_date >= '{date_floor_clamped}' \"\n f\"AND \\\"to\\\" != '0x0000000000000000000000000000000000000000' \"\n f\"AND \\\"to\\\" != '0x000000000000000000000000000000000000dead' \"\n f\"GROUP BY counterparty ORDER BY amt DESC LIMIT 1000\"\n )\n # v0.9.4 round-2 adversarial review Fix 3: bump max_attempts 2 → 4 for parity\n # with all other phase-2 / chunk surf calls (4 = repo retry\n # standard, see _run_surf_with_retry docstring). Per-hub recipient\n # data is load-bearing for `net_fanout_tokens_total` — a single\n # silent failure here previously produced bulk-fallback data\n # silently and dropped the truncation signal.\n per_hub_err: str | None = None\n try:\n d_per, per_hub_err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql_per_hub, \"max_rows\": 1005}),\n base_timeout=30, max_attempts=4,\n )\n if d_per:\n rows = (d_per.get(\"data\") or [])\n per_hub_complete[hub_addr] = [{\n \"hub\": hub_addr,\n \"recipient\": (r.get(\"counterparty\") or \"\").lower(),\n \"amt\": r.get(\"amt\"),\n \"n_tx\": r.get(\"n_tx\"),\n \"first_tx\": r.get(\"first_tx\"),\n } for r in rows]\n else:\n # adversarial review Fix 3: surf failed (None doc). Preserve bulk\n # partial rows for this hub AND set recipients_truncated\n # = True so the downstream net% claim is gated.\n print(f\"[fanout] per-hub surf failed for {hub_addr[:14]}: \"\n f\"{per_hub_err or 'no doc'}\", file=_sys.stderr)\n per_hub_complete[hub_addr] = recipients_per_hub.get(hub_addr, [])\n recipients_truncated = True\n except Exception as e:\n print(f\"[fanout] per-hub query failed for {hub_addr[:14]}: {e}\", file=_sys.stderr)\n # 保留 bulk 部分数据 fallback\n per_hub_complete[hub_addr] = recipients_per_hub.get(hub_addr, [])\n recipients_truncated = True\n # Override recipients_per_hub with complete data\n recipients_per_hub = per_hub_complete\n # adversarial review Fix 3 (revised): truncation flag uses BOTH the bulk\n # truncation signal we just OR'd into recipients_truncated AND\n # the \"any hub has ≥ 1000 recipients\" check from before. Bulk\n # surf failures no longer mask incomplete data as \"complete\".\n any_per_hub_full = any(len(rs) >= 1000 for rs in per_hub_complete.values())\n recipients_truncated = recipients_truncated or any_per_hub_full\n\n # Resolve Arkham labels for top senders\n unique_source_addrs = list({\n (s.get(\"source\") or \"\").lower() for s in top_senders.values()\n if s.get(\"source\") and _chain_is_valid_addr((s.get(\"source\") or \"\").lower())\n })\n source_labels: dict[str, dict] = {}\n if unique_source_addrs:\n try:\n from surf_labels_probe import resolve_labels as _resolve_labels\n source_labels = _resolve_labels(unique_source_addrs)\n except Exception as e:\n import sys as _sys\n print(f\"[fanout] label resolve failed (non-fatal): {e}\", file=_sys.stderr)\n source_labels = {}\n\n # Filter: only confirmed CEX-origin hubs\n CEX_CLS = {\"CEX_DEPOSIT\", \"CEX_HOT_WALLET\"}\n confirmed_hubs: list[dict[str, Any]] = []\n total_cex_inflow = 0.0\n total_fanout_recipients = 0\n\n # v0.8.1 first pass: collect confirmed-hub addr set BEFORE per-hub\n # net computation, so the inter-hub-loopback exclusion uses the full\n # set (not the partial set being built).\n confirmed_hub_addrs: set[str] = set()\n for hub_data in hub_candidates:\n hub = (hub_data.get(\"hub\") or \"\").lower()\n if hub not in top_senders:\n continue\n src = (top_senders[hub].get(\"source\") or \"\").lower()\n src_cls = (source_labels.get(src) or {}).get(\"classification\") or \"UNLABELED\"\n if src_cls in CEX_CLS:\n confirmed_hub_addrs.add(hub)\n\n # v0.8.1: also collect ALL CEX-source addrs (the top-sender into each\n # confirmed hub) so we can identify outflows that loop back to the\n # same CEX brand (e.g. hub → its own Gate Deposit). These do NOT\n # represent retail fan-out — they are funding-source round-trips.\n all_cex_source_addrs: set[str] = set()\n for hub in confirmed_hub_addrs:\n src = (top_senders[hub].get(\"source\") or \"\").lower()\n if src and _chain_is_valid_addr(src):\n all_cex_source_addrs.add(src)\n\n for hub_data in hub_candidates:\n hub = (hub_data.get(\"hub\") or \"\").lower()\n if hub not in top_senders:\n continue\n top_send = top_senders[hub]\n src = (top_send.get(\"source\") or \"\").lower()\n src_label = source_labels.get(src) or {}\n src_cls = src_label.get(\"classification\") or \"UNLABELED\"\n if src_cls not in CEX_CLS:\n continue\n\n # v0.8.1: compute NET fan-out from the trustworthy Phase 2\n # recipient list (not the broken Phase 1 total_out which is\n # SQL-layer inconsistent — see velvet handoff 2026-06-11).\n # Two flavors of net:\n # - net_fanout_tokens (transparency): sums all non-loopback\n # recipients regardless of per-recipient amount.\n # - net_structured_fanout_tokens: sums only recipients\n # receiving ≥ min_per_recipient (matches Phase 1 hub-shape\n # semantic; this is the input A2 size claim should use).\n # Exclusions (same for both):\n # - The hub's own CEX source (round-trip loopback)\n # - All other confirmed CEX-sources (cross-hub loopback)\n # - All other confirmed fanout hubs (inter-hub shuffle)\n all_recipients = recipients_per_hub.get(hub, [])\n net_tokens = 0.0\n net_recipient_addrs: set[str] = set()\n net_structured_tokens = 0.0\n net_structured_recipient_addrs: set[str] = set()\n loopback_tokens = 0.0\n inter_hub_tokens = 0.0\n for r in all_recipients:\n r_addr = (r.get(\"recipient\") or \"\").lower()\n r_amt = float(r.get(\"amt\") or 0.0)\n if not r_addr:\n continue\n if r_addr in all_cex_source_addrs:\n loopback_tokens += r_amt\n continue\n if r_addr in confirmed_hub_addrs and r_addr != hub:\n inter_hub_tokens += r_amt\n continue\n net_tokens += r_amt\n net_recipient_addrs.add(r_addr)\n if r_amt >= min_per_recipient:\n net_structured_tokens += r_amt\n net_structured_recipient_addrs.add(r_addr)\n\n # Display top-N recipients sorted by amount (excluding loopbacks\n # so the table matches the net calc). UNCLASSIFIED amounts\n # (= net) are what shows up in the report.\n display_recipients = sorted(\n (r for r in all_recipients\n if (r.get(\"recipient\") or \"\").lower() in net_recipient_addrs),\n key=lambda x: float(x.get(\"amt\") or 0), reverse=True,\n )[:max_recipients_per_hub_display]\n cex_inflow_amt = float(top_send.get(\"amt\") or 0)\n n_recip = int(hub_data.get(\"n_recipients\") or 0)\n confirmed_hubs.append({\n \"addr\": hub,\n \"n_recipients\": n_recip,\n # v0.8.1 DEPRECATED metric — kept for back-compat / debug.\n # SQL-layer inconsistent vs the Phase 2 recipient detail.\n # Do NOT use for size claims. See net_fanout_tokens below.\n \"total_out_tokens\": float(hub_data.get(\"total_out\") or 0),\n \"min_per_recipient\": float(hub_data.get(\"min_per\") or 0),\n \"avg_per_recipient\": float(hub_data.get(\"avg_per\") or 0),\n # v0.8.1 NEW metrics — derived from Phase 2 recipient detail.\n # net_fanout_tokens = floor on retail-routed amount; this is\n # what feeds fanout_pct_supply in behavior_classifier.\n \"net_fanout_tokens\": net_tokens,\n \"net_fanout_recipients\": len(net_recipient_addrs),\n # v0.8.1 audit MED fix: structured variant matches Phase 1\n # ≥ min_per_recipient gate used to define \"hub shape\".\n # Used by A2 size claim to keep the semantic consistent.\n \"net_structured_fanout_tokens\": net_structured_tokens,\n \"net_structured_recipients\": len(net_structured_recipient_addrs),\n \"loopback_to_cex_tokens\": loopback_tokens,\n \"inter_hub_shuffle_tokens\": inter_hub_tokens,\n # v0.8.1 audit HIGH fix: raw recipient address sets so the\n # summary dedup uses ALL net recipients, not the display top\n # 20 capped slice. Stored with underscore prefix to mark as\n # internal aggregation use (not a stable consumer field).\n \"_net_recipient_addrs_raw\": sorted(net_recipient_addrs),\n \"_net_structured_recipient_addrs_raw\": sorted(net_structured_recipient_addrs),\n \"cex_source\": src,\n \"cex_source_label\": src_label.get(\"label\"),\n \"cex_source_entity\": src_label.get(\"entity_name\"),\n \"cex_source_classification\": src_cls,\n \"cex_source_inflow_tokens\": cex_inflow_amt,\n \"cex_source_n_tx\": int(top_send.get(\"n_tx\") or 0),\n \"top_recipients\": [\n {\n \"addr\": (r.get(\"recipient\") or \"\").lower(),\n \"amt\": float(r.get(\"amt\") or 0),\n \"n_tx\": int(r.get(\"n_tx\") or 0),\n }\n for r in display_recipients\n ],\n })\n total_cex_inflow += cex_inflow_amt\n total_fanout_recipients += n_recip\n\n # v0.8.1: aggregate net metrics across hubs for the report header.\n # net_fanout_tokens_total is the floor on retail-routed amount —\n # this is what should drive A2 fanout_pct_supply, not the broken\n # total_out_tokens.\n net_fanout_tokens_total = sum(\n h.get(\"net_fanout_tokens\") or 0.0 for h in confirmed_hubs\n )\n loopback_to_cex_total = sum(\n h.get(\"loopback_to_cex_tokens\") or 0.0 for h in confirmed_hubs\n )\n inter_hub_shuffle_total = sum(\n h.get(\"inter_hub_shuffle_tokens\") or 0.0 for h in confirmed_hubs\n )\n # v0.8.1 audit HIGH fix: dedupe across hubs using the RAW\n # net recipient set (not display-capped top_recipients). Previously\n # used top_recipients which is sliced to N=20 for display, so the\n # global unique count silently understated.\n all_net_recipients_raw: set[str] = set()\n for h in confirmed_hubs:\n for a in (h.get(\"_net_recipient_addrs_raw\") or []):\n if a:\n all_net_recipients_raw.add(a)\n # Threshold-respecting variant: only recipients each receiving ≥\n # min_per_recipient (matches Phase 1 \"structured fan-out\" semantic).\n # Used by behavior_classifier for A2 size claim.\n net_structured_tokens_total = sum(\n h.get(\"net_structured_fanout_tokens\") or 0.0 for h in confirmed_hubs\n )\n all_net_structured_recipients: set[str] = set()\n for h in confirmed_hubs:\n for a in (h.get(\"_net_structured_recipient_addrs_raw\") or []):\n if a:\n all_net_structured_recipients.add(a)\n\n return {\n \"hubs\": confirmed_hubs,\n \"summary\": {\n \"n_confirmed_hubs\": len(confirmed_hubs),\n \"n_candidate_hubs\": len(hub_candidates),\n \"total_cex_inflow_tokens\": total_cex_inflow,\n \"total_fanout_recipients\": total_fanout_recipients,\n # v0.8.1 — NET metrics (Phase 2 detail-derived).\n #\n # net_fanout_tokens_total: includes ALL net recipients\n # regardless of per-recipient amount (transparency floor).\n # net_structured_fanout_tokens_total: ≥ min_per_recipient\n # only (matches Phase 1 hub-shape semantic; this is the\n # right input for A2 size claim).\n \"net_fanout_tokens_total\": net_fanout_tokens_total,\n \"net_fanout_unique_recipients\": len(all_net_recipients_raw),\n \"net_structured_fanout_tokens_total\": net_structured_tokens_total,\n \"net_structured_unique_recipients\": len(all_net_structured_recipients),\n \"loopback_to_cex_tokens_total\": loopback_to_cex_total,\n \"inter_hub_shuffle_tokens_total\": inter_hub_shuffle_total,\n # v0.8.1 audit CRITICAL fix: truncation propagation.\n # Downstream code (behavior_classifier) MUST refuse to emit\n # net % claims when these are True.\n \"_phase2_senders_truncated\": senders_truncated,\n \"_phase2_recipients_truncated\": recipients_truncated,\n \"_phase2_complete\": not (senders_truncated or recipients_truncated),\n },\n \"_debug\": {\n \"date_floor_clamped\": date_floor_clamped,\n \"min_recipients\": min_recipients,\n \"max_recipients\": max_recipients,\n \"min_per_recipient\": min_per_recipient,\n \"min_total_out\": min_total_out,\n \"senders_max_rows\": senders_max_rows,\n \"recipients_max_rows\": recipients_max_rows,\n \"senders_returned_rows\": len(sender_rows),\n \"recipients_returned_rows\": len(recipient_rows),\n },\n }\n\n\ndef _empty_fanout_summary() -> dict[str, Any]:\n return {\n \"n_confirmed_hubs\": 0,\n \"n_candidate_hubs\": 0,\n \"total_cex_inflow_tokens\": 0.0,\n \"total_fanout_recipients\": 0,\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":77876,"content_sha256":"a30edd55d73823c320acce2d761081a96f8fa3688e5a40a640721e09069006dc"},{"filename":"alpha/v06/helpers/hidden_operator_activity.py","content":"#!/usr/bin/env python3\n\"\"\"hidden_operator_activity.py — query whether the heuristically-flagged\nhidden operator wallets (suspected_operator_reserve / fake_mining_cluster_member,\nemitted by hidden_operator_enricher) have already begun realizing their\nallocation on-chain.\n\n# Why this exists (v0.8.3)\n\nv0.8.2.1 added `hidden_operator_enricher` which appends heuristically-\nidentified operator-controlled wallets to monitoring_wallets[]. Users\nthen import paste.json into Binance Wallet / OKX wallet tracker to\nmonitor the addresses themselves.\n\nBut for the report itself, users wanted: a forensic summary — has the\nhidden operator population already started realizing? If so, how much?\nThis module answers that by running the same (a)/(b) SQL the dump_tracker\nruns against m6 insiders, but with the input set replaced by hidden\noperator addresses pulled from monitoring_wallets[].\n\n# Output\n\n`skeleton[\"hidden_operator_activity\"]` =\n{\n \"n_hidden_wallets_tracked\": int,\n \"confirmed_cex_tokens\": float, # (a) hidden → CEX deposit\n \"confirmed_dex_tokens\": float, # (b) hidden own DEX swaps\n \"confirmed_total_tokens\": float,\n \"confirmed_total_pct_circ\": float | None,\n \"confirmed_est_usd\": float | None, # tokens × median DEX price\n \"n_distinct_cex_destinations\": int, # how many CEX deposit addrs\n \"n_dex_swaps\": int, # how many DEX swap rows\n \"cex_destination_brands\": [str], # list of CEX brand names hit\n \"_error\": str | None, # only on hard SQL failure\n}\n\n# Render impact\n\n`render_report.py` (真实派发段) shows a `🕵️ 隐藏庄家弹药历史动作` row\nconditionally — only when `confirmed_total_tokens > 0`. The wording is\nforensic (\"有 / 无 已观察到的变现\"), not alert-y.\n\n# Surf cost\n\n2 parallel SQL queries per token (reuses dump_tracker's\n`fetch_apparatus_to_cex` and `fetch_apparatus_dex_sold` SQL helpers).\nEstimated +10-30 credits/token (~3-10% bump over baseline). Time\nimpact 0s when run in parallel with the existing dump_tracker queue.\n\n# Caveats\n\n- Reuses the dump_tracker (a)/(b) algorithm exactly — same convention:\n CEX deposit = exit/sell (the off-chain CEX sale is unobservable on\n chain, treated as confirmed sell). DEX swap = directed sell from\n the hidden wallet itself.\n- USD estimate uses the dump_tracker's DEX median price field\n (`dex_twap_usd_per_token`) when available; else None (display as \"—\").\n- Stale historical allocation is NOT double-counted — we query 365d\n activity, no overlap with insider (a)/(b) because the hidden\n operator set is wallet-disjoint from the m6 insider set by\n construction (post-launch detector outputs vs pre-launch deployer\n outflow).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent))\n\n\n_HIDDEN_OPERATOR_ROLE_ENUMS = (\n \"suspected_operator_reserve\",\n \"fake_mining_cluster_member\",\n)\n\n\ndef _collect_hidden_addresses(skel: dict) -> list[str]:\n \"\"\"Extract hidden-operator wallet addresses from skeleton's\n monitoring_wallets[] list.\n\n Returns a sorted list of lowercase 0x-addresses.\n \"\"\"\n addrs: set[str] = set()\n for w in (skel.get(\"monitoring_wallets\") or []):\n if (w.get(\"monitor_role_enum\") or \"\") in _HIDDEN_OPERATOR_ROLE_ENUMS:\n a = (w.get(\"addr_full\") or w.get(\"address\") or \"\").lower()\n if a and a.startswith(\"0x\") and len(a) == 42:\n addrs.add(a)\n return sorted(addrs)\n\n\ndef probe_hidden_operator_activity(\n ca: str,\n skel: dict,\n date_floor: str | None = None,\n) -> dict[str, Any]:\n \"\"\"Probe (a)/(b) realization for the hidden operator wallet set.\n\n Args:\n ca: token contract address.\n skel: skeleton dict (read monitoring_wallets, meta, dump_tracking).\n date_floor: 'YYYY-MM-DD' lower bound. Defaults to dump_tracker's\n date_floor (alpha listing date) for consistency with (a)/(b)\n windows in the report.\n\n Returns the `hidden_operator_activity` dict (see module docstring).\n Errors are reported via `_error` field, never raised.\n \"\"\"\n hidden = _collect_hidden_addresses(skel)\n if not hidden:\n return {\n \"n_hidden_wallets_tracked\": 0,\n \"confirmed_cex_tokens\": 0.0,\n \"confirmed_dex_tokens\": 0.0,\n \"confirmed_total_tokens\": 0.0,\n \"confirmed_total_pct_circ\": None,\n \"confirmed_est_usd\": None,\n \"n_distinct_cex_destinations\": 0,\n \"n_dex_swaps\": 0,\n \"cex_destination_brands\": [],\n }\n\n # Default date_floor mirrors dump_tracker convention (Alpha listing day).\n # v0.9.1: clamp to surf's 364-day safe window so old tokens (>365 day\n # listing age) don't INVALID_REQUEST on bsc_transfers / bsc_dex_trades.\n if not date_floor:\n meta = skel.get(\"meta\") or {}\n date_floor = (\n meta.get(\"alpha_listing_date_utc\")\n or meta.get(\"listing_date\")\n )\n from surf_constraints import surf_safe_date_floor\n date_floor = surf_safe_date_floor(date_floor)\n\n # Reuse dump_tracker SQL helpers.\n try:\n from dump_tracker import fetch_apparatus_to_cex, fetch_apparatus_dex_sold\n except Exception as e:\n return {\n \"n_hidden_wallets_tracked\": len(hidden),\n \"confirmed_cex_tokens\": 0.0,\n \"confirmed_dex_tokens\": 0.0,\n \"confirmed_total_tokens\": 0.0,\n \"confirmed_total_pct_circ\": None,\n \"confirmed_est_usd\": None,\n \"n_distinct_cex_destinations\": 0,\n \"n_dex_swaps\": 0,\n \"cex_destination_brands\": [],\n \"_error\": f\"import dump_tracker helpers failed: {str(e)[:120]}\",\n }\n\n # (a) hidden → CEX deposit\n cex_res: dict[str, Any] = {}\n try:\n cex_res = fetch_apparatus_to_cex(ca, hidden, date_floor) or {}\n except Exception as e:\n cex_res = {\"__ERR\": f\"cex query: {str(e)[:120]}\"}\n\n # (b) hidden own DEX swaps\n dex_res: dict[str, Any] = {}\n try:\n dex_res = fetch_apparatus_dex_sold(ca, hidden, date_floor) or {}\n except Exception as e:\n dex_res = {\"__ERR\": f\"dex query: {str(e)[:120]}\"}\n\n cex_tokens = float(cex_res.get(\"cex_tokens\") or 0)\n dex_tokens = float(dex_res.get(\"dex_sold_tokens\") or 0)\n total_tokens = cex_tokens + dex_tokens\n\n n_swaps = int(dex_res.get(\"n_swaps\") or 0)\n cex_labels = list(cex_res.get(\"cex_labels\") or [])\n\n # Distinct CEX-destination count: labels are sometimes ['Binance',\n # 'Binance', 'OKX'] — dedupe for the report. Brand only, not address.\n cex_brands_dedup = sorted({lbl for lbl in cex_labels if lbl})\n\n # v0.8.3.1 audit MED #2 fix: prefer the hidden population's OWN\n # realized DEX USD (dex_sold_usd) when available. Falls back to m6\n # insider TWAP only when the hidden leg has no on-chain SUM amount_usd\n # available (rare). The render text discloses which source was used.\n hidden_dex_usd = float(dex_res.get(\"dex_sold_usd\") or 0)\n dt = skel.get(\"dump_tracking\") or {}\n twap = dt.get(\"apparatus_dex_twap_usd_per_token\")\n confirmed_est_usd: float | None = None\n est_usd_source: str = \"none\"\n if hidden_dex_usd > 0 and total_tokens > 0:\n # Use realized DEX USD + extrapolate to CEX leg via the hidden\n # population's own implied price (hidden_dex_usd / dex_tokens).\n if dex_tokens > 0:\n hidden_implied_px = hidden_dex_usd / dex_tokens\n confirmed_est_usd = hidden_implied_px * total_tokens\n est_usd_source = \"hidden_own_dex\"\n else:\n confirmed_est_usd = hidden_dex_usd\n est_usd_source = \"hidden_own_dex_only\"\n elif twap and total_tokens > 0:\n try:\n confirmed_est_usd = float(twap) * total_tokens\n est_usd_source = \"m6_insider_twap\"\n except (ValueError, TypeError):\n confirmed_est_usd = None\n\n meta = skel.get(\"meta\") or {}\n circ = meta.get(\"circulating_supply\")\n pct_circ = None\n if circ and total_tokens > 0:\n try:\n pct_circ = total_tokens / float(circ) * 100\n except (ValueError, TypeError, ZeroDivisionError):\n pct_circ = None\n\n out = {\n \"n_hidden_wallets_tracked\": len(hidden),\n \"confirmed_cex_tokens\": cex_tokens,\n \"confirmed_dex_tokens\": dex_tokens,\n \"confirmed_total_tokens\": total_tokens,\n \"confirmed_total_pct_circ\": pct_circ,\n \"confirmed_est_usd\": confirmed_est_usd,\n \"confirmed_est_usd_source\": est_usd_source,\n \"n_distinct_cex_destinations\": len(cex_brands_dedup),\n \"n_dex_swaps\": n_swaps,\n \"cex_destination_brands\": cex_brands_dedup,\n \"date_floor\": date_floor,\n }\n # v0.8.3.1 audit HIGH #1 fix: surface ok=False as _error.\n # fetch_apparatus_to_cex / fetch_apparatus_dex_sold can return\n # {\"ok\": False, \"error\": ...} on failed SQL; silent treat-as-0\n # would let the render emit \"未观察到变现\" when both queries\n # actually failed.\n errs: list[str] = []\n if isinstance(cex_res, dict):\n if cex_res.get(\"__ERR\"):\n errs.append(f\"cex: {cex_res['__ERR']}\")\n elif cex_res.get(\"ok\") is False:\n errs.append(f\"cex: ok=False {cex_res.get('error') or ''}\")\n if isinstance(dex_res, dict):\n if dex_res.get(\"__ERR\"):\n errs.append(f\"dex: {dex_res['__ERR']}\")\n elif dex_res.get(\"ok\") is False:\n errs.append(f\"dex: ok=False {dex_res.get('error') or ''}\")\n err_str = \" | \".join(e for e in errs if e)\n if err_str:\n out[\"_error\"] = err_str\n return out\n\n\n__all__ = [\"probe_hidden_operator_activity\"]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9695,"content_sha256":"392dc8244dd613f9aaf2951583726ff87618769d7f0ca871fed6841e1c88c286"},{"filename":"alpha/v06/helpers/hidden_operator_enricher.py","content":"#!/usr/bin/env python3\n\"\"\"hidden_operator_enricher.py — append heuristically-flagged\n\"hidden operator\" wallets to the monitoring_wallets list.\n\n# Why this exists (v0.8.2)\n\nThe 5-bucket disjoint accounting (operator-controlled vs non-operator\npressure) systematically under-counts operator-side balance when\nArkham labels are sparse. Velvet / COLLECT / JCT review surfaced two\npatterns:\n\n 1. **Suspected operator reserve** — top-100 holder ≥ 10% circulating\n with no Arkham label. Examples:\n - COLLECT `0xf27d6fc930db` 441.9% 流通 no label (mint reserve)\n - COLLECT `0x692253941d1f` 16.8% 流通 no label\n - JCT `0xe0b5ed319707` 36.1% 流通 (= one of the mint\n authorities — its current balance is\n un-distributed supply)\n\n 2. **Fake mining cluster members** — destinations of a mint authority\n classified as `is_fake_mining_cluster` by fake_mining_detector.\n These addresses received 0.5-3% supply each in 1-3 large\n transactions — i.e. they look like operator-controlled allocation\n wallets, not retail miners. Adding them to the watch list lets the\n user track them when they start fanning-out / depositing to CEX /\n selling on DEX.\n\nThis enricher is FORENSIC — it does not alert or push. It only\nappends rows to `skeleton.monitoring_wallets[]` so the downstream\n`annotate_monitoring_wallets` + `monitoring_export` pipeline carries\nthem into `monitoring_paste.json`. The user imports the paste file\ninto Binance / OKX wallet tracker and reads on-chain activity\nmanually.\n\n# Inputs (skeleton fields read)\n\n- `funding_attribution.mint_authority_dumps.per_addr` — for\n fake-mining cluster destinations.\n- `meta.total_supply` and `meta.circulating_supply` — for % calc.\n- `monitoring_wallets[].addr_full` — for de-duplication against\n already-known wallets.\n\nNOTE: This module does NOT call surf. The \"suspected_operator_reserve\"\nheuristic needs a `top_holders_label_classified` field on the skeleton\nthat section_a / dump_tracker would need to emit. That wiring is left\nfor v0.8.3 — see the v0.8.2 PR description. For now this enricher\nhandles fake-mining cluster members only (the highest-confidence\nsignal, derived from already-collected mint_authority_dumps data).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent))\nfrom fake_mining_detector import classify_token_mining_mode\nfrom i18n import t # v0.6.2 i18n\n\n# Threshold for suspected_operator_reserve heuristic.\n# v0.8.4.5: 10% → 3% (Velvet review caught miss — top unclassified at\n# 8% / 7.7% / 5.3% all skipped at 10% threshold. Lowering to 3% catches\n# medium-sized op aliases. Trade-off: more false positives among 3-7%\n# whales who might be legit large retail / VC OTC buyers. Mitigation:\n# emit `_arkham_classification: UNLABELED` + monitoring_paste lets the\n# user vet each one. The forensic value is in capturing the pattern,\n# not making zero-FP claims.\nSUSPECTED_RESERVE_MIN_PCT_CIRCULATING = 3.0\n\n\ndef _norm_addr(a: Any) -> str:\n if not a or not isinstance(a, str):\n return \"\"\n return a.lower().strip()\n\n\ndef enrich_monitoring_with_hidden_operators(skel: dict) -> dict:\n \"\"\"Append heuristically-flagged hidden operator wallets to\n `skel[\"monitoring_wallets\"]` if not already present.\n\n Returns the same skeleton (mutated in place).\n \"\"\"\n meta = skel.get(\"meta\") or {}\n total_supply = float(meta.get(\"total_supply\") or 0) or None\n\n wallets = skel.get(\"monitoring_wallets\") or []\n seen: set[str] = {\n _norm_addr(w.get(\"addr_full\") or w.get(\"address\"))\n for w in wallets\n }\n seen.discard(\"\")\n\n n_seed = max(\n (int(w.get(\"n\") or 0) for w in wallets), default=0\n )\n next_n = n_seed + 1\n\n # ===========================================================\n # Pattern 2: Fake mining cluster members\n # (derived from already-collected mint_authority_dumps data —\n # no extra surf calls needed)\n # ===========================================================\n fa = skel.get(\"funding_attribution\") or {}\n mad = fa.get(\"mint_authority_dumps\") or {}\n mining_mode = classify_token_mining_mode(mad, total_supply=total_supply)\n\n if mining_mode[\"is_fake_mining_distribution\"]:\n per_addr = (mad.get(\"per_addr\") or {})\n for auth_addr, auth_class in (\n mining_mode[\"per_authority_classifications\"].items()\n ):\n if not auth_class.get(\"is_fake_mining_cluster\"):\n continue\n data = per_addr.get(auth_addr) or {}\n for dest in (data.get(\"top_destinations\") or []):\n d_addr = _norm_addr(dest.get(\"dest\"))\n if not d_addr or d_addr in seen:\n continue\n amt = float(dest.get(\"amt\") or 0)\n pct_supply = (amt / total_supply * 100) if total_supply else 0\n # v0.8.2.2 audit HIGH #2 fix: do NOT write\n # historical received amt into balance_tokens. The ranker\n # interprets balance_tokens as current balance and would\n # promote already-emptied recipients to CRITICAL based on\n # historical allocation size. Use None + dedicated\n # _allocation_received_tokens field. monitoring_export's\n # balance-filter will see None and fall through to the\n # role-based keep rule (fake_mining_cluster_member has\n # Tier 1 score so it's exported regardless of balance).\n wallets.append({\n \"n\": next_n,\n \"addr_short\": d_addr[:10],\n \"addr_full\": d_addr,\n \"role\": t(\"sec2.hidden_op_role_fake_mining_cluster\"),\n \"status_emoji\": \"🟡\",\n \"balance_tokens\": None,\n \"recent_activity_72h\": False,\n \"monitor_role_enum\": \"fake_mining_cluster_member\",\n \"_is_enricher_assigned\": True,\n \"_received_from_mint_authority\": auth_addr,\n \"_allocation_received_tokens\": amt,\n \"_allocation_received_pct_supply\": pct_supply,\n \"_arkham_label\": dest.get(\"arkham_label\"),\n \"_arkham_classification\": dest.get(\n \"arkham_classification\"\n ) or \"UNLABELED\",\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n seen.add(d_addr)\n next_n += 1\n\n # ===========================================================\n # Pattern 1: Suspected operator reserve (≥ 10% 流通 + no label)\n # v0.8.4: Iterates meta.chain_lp_realtime.\u003cchain>.top_holders_classified\n # which section_a_scope.py now emits (one classification pass per\n # surf chain probe, no extra cost). Pulls top unclassified holders\n # ≥ 10% circulating + appends to monitoring as\n # `suspected_operator_reserve`.\n # ===========================================================\n circulating = float(meta.get(\"circulating_supply\") or 0) or None\n if circulating and circulating > 0:\n clp = meta.get(\"chain_lp_realtime\") or {}\n for chain_name, chain_data in (clp or {}).items():\n if not isinstance(chain_data, dict):\n continue\n classified = chain_data.get(\"top_holders_classified\") or {}\n if not isinstance(classified, dict) or \"_error\" in classified:\n continue\n unclass = classified.get(\"unclassified\") or {}\n for h in (unclass.get(\"top\") or []):\n addr = _norm_addr(h.get(\"addr\"))\n if not addr or addr in seen:\n continue\n bal = float(h.get(\"balance\") or 0)\n if bal \u003c= 0:\n continue\n pct_circ = bal / circulating * 100\n if pct_circ \u003c SUSPECTED_RESERVE_MIN_PCT_CIRCULATING:\n continue\n wallets.append({\n \"n\": next_n,\n \"addr_short\": addr[:10],\n \"addr_full\": addr,\n \"role\": t(\"sec2.hidden_op_role_suspected_reserve\"),\n \"status_emoji\": \"🟡\",\n # Heuristic-flagged: balance is current (surf\n # token-holders returns current balance). Setting\n # balance_tokens so ranker picks up the size.\n \"balance_tokens\": bal,\n \"recent_activity_72h\": False,\n \"monitor_role_enum\": \"suspected_operator_reserve\",\n \"_is_enricher_assigned\": True,\n \"_suspected_chain\": chain_name,\n \"_pct_circulating_at_detection\": pct_circ,\n \"_arkham_label\": h.get(\"label_text\") or None,\n \"_arkham_classification\": \"UNLABELED\",\n \"alert\": \"\u003cLLM_NARRATIVE_PLACEHOLDER>\",\n })\n seen.add(addr)\n next_n += 1\n\n skel[\"monitoring_wallets\"] = wallets\n return skel\n\n\n__all__ = [\n \"enrich_monitoring_with_hidden_operators\",\n \"SUSPECTED_RESERVE_MIN_PCT_CIRCULATING\",\n]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9341,"content_sha256":"8c796fb0736d319dd63b7ba698d864188d762dfc5a8d7645f57b819e512ff20e"},{"filename":"alpha/v06/helpers/i18n.py","content":"#!/usr/bin/env python3\n\"\"\"i18n.py — single-source-of-truth language loader for v0.6.2.\n\nv0.6.1 had ~221 hardcoded Chinese strings across 13 files. Users running\nthe skill in English contexts (adversarial review / other LLMs / English-speaking analysts)\ngot Chinese reports they couldn't read. v0.6.2 centralizes all user-facing\nstrings into lang/\u003clang>.yaml files.\n\nUsage:\n from helpers.i18n import t, set_lang\n set_lang(\"en\")\n print(t(\"verdict.cn_label.EXIT_IF_HOLDING\")) # → \"Exit if holding\"\n print(t(\"rule_11.wave_title.pre_launch_otc\")) # → \"Wave 1 pre-launch OTC distribution\"\n\nKey paths use dots: `section.\u003cname>.\u003clabel>`. Supports `.format()` interp\non values that contain `{placeholder}` patterns.\n\nLang files live at v06/lang/\u003clang>.yaml. Default lang = \"zh\". Set via\n`set_lang(\"en\")` or env var `BINANCE_ALPHA_LANG=en`.\n\nFail mode: if a key is missing in the requested lang, fall back to \"zh\"\n(the original source). If missing in BOTH, return the key itself as\nvisible-fail (so devs notice the gap).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\n\n_LANG_DIR = Path(__file__).parent.parent / \"lang\"\n_CACHE: dict[str, dict[str, Any]] = {}\n_CURRENT_LANG: str = os.environ.get(\"BINANCE_ALPHA_LANG\", \"zh\")\n\n\ndef _load_lang(lang: str) -> dict[str, Any]:\n \"\"\"Load lang/\u003clang>.json into a flat dotted-key dict. Cached after first load.\n\n v0.6.2 design choice: JSON (stdlib, zero-dep) over YAML (would require\n PyYAML pip-install). YAML's prettier syntax not worth the dep risk for\n static config files.\n \"\"\"\n if lang in _CACHE:\n return _CACHE[lang]\n path = _LANG_DIR / f\"{lang}.json\"\n if not path.exists():\n raise FileNotFoundError(\n f\"i18n lang file missing: {path}. Available: \"\n f\"{sorted(p.stem for p in _LANG_DIR.glob('*.json'))}\"\n )\n raw = json.loads(path.read_text(encoding=\"utf-8\"))\n # Flatten nested dicts → dotted keys\n flat: dict[str, Any] = {}\n _flatten(raw, \"\", flat)\n _CACHE[lang] = flat\n return flat\n\n\ndef _flatten(d: Any, prefix: str, out: dict[str, Any]) -> None:\n \"\"\"Recursively flatten {\"a\": {\"b\": \"x\"}} → {\"a.b\": \"x\"}.\"\"\"\n if isinstance(d, dict):\n for k, v in d.items():\n full = f\"{prefix}.{k}\" if prefix else k\n _flatten(v, full, out)\n else:\n # Leaf (str, list, int, etc.)\n out[prefix] = d\n\n\ndef set_lang(lang: str) -> None:\n \"\"\"Switch active language. Subsequent t() calls use this lang.\"\"\"\n global _CURRENT_LANG\n if lang not in (\"zh\", \"en\"):\n raise ValueError(f\"unsupported lang: {lang!r} (supported: zh, en)\")\n _CURRENT_LANG = lang\n\n\ndef get_lang() -> str:\n \"\"\"Return active language.\"\"\"\n return _CURRENT_LANG\n\n\ndef t(key: str, **kwargs: Any) -> str:\n \"\"\"Translate a key. If key not found in active lang, fall back to zh.\n If missing in both, return the key itself (visible-fail marker).\n\n Optional **kwargs: passed to .format() if the value contains\n `{placeholder}` patterns.\n\n v0.6.2 strict mode (audit MED #4): set\n BINANCE_ALPHA_I18N_STRICT=1 to make missing en keys raise instead of\n silently falling back to zh. Useful for CI to catch key gaps before\n they ship Chinese leaks in English reports.\n \"\"\"\n val = _lookup(key, _CURRENT_LANG)\n if val is None and _CURRENT_LANG != \"zh\":\n strict = os.environ.get(\"BINANCE_ALPHA_I18N_STRICT\", \"\").strip()\n if strict in (\"1\", \"true\", \"yes\"):\n raise KeyError(\n f\"i18n strict mode: key {key!r} missing in {_CURRENT_LANG!r} \"\n f\"(BINANCE_ALPHA_I18N_STRICT=1). Add the key to \"\n f\"lang/{_CURRENT_LANG}.json or unset strict mode.\"\n )\n val = _lookup(key, \"zh\")\n if val is None:\n return f\"[MISSING:{key}]\" # devs notice\n if isinstance(val, str) and kwargs:\n try:\n return val.format(**kwargs)\n except (KeyError, IndexError):\n # Unfilled placeholder — return raw template so dev can see what was missing\n return val\n except (TypeError, ValueError) as e:\n # v0.7.13 (issue #1 Bug 2 class): a kwarg is None (or wrong type)\n # while the template applies a numeric format spec, e.g.\n # \"${trigger:.6f}\".format(trigger=None) → TypeError. The real fix\n # is at the call site (render None → \"—\" before calling t); this is\n # a last-resort net so one stray None can never crash a whole render.\n # adversarial review LOW #2: warn so this masked-bug path stays observable.\n print(f\"[i18n] format fallback for key {key!r}: {type(e).__name__}: {e}\", file=sys.stderr)\n # Strip each None placeholder (incl. its format spec) to a dash so\n # the surviving kwargs still interpolate cleanly.\n dash = _lookup(\"common.none_dash\", _CURRENT_LANG) or _lookup(\"common.none_dash\", \"zh\") or \"—\"\n patched = val\n safe = dict(kwargs)\n for k, v in kwargs.items():\n if v is None:\n patched = re.sub(r\"\\{\" + re.escape(k) + r\"(?::[^}]*)?\\}\", dash, patched)\n safe.pop(k, None)\n try:\n return patched.format(**safe)\n except (KeyError, IndexError, TypeError, ValueError):\n return patched\n return val\n\n\ndef _lookup(key: str, lang: str) -> Any:\n \"\"\"Return the raw value for key in lang, or None if missing.\"\"\"\n try:\n d = _load_lang(lang)\n except FileNotFoundError:\n return None\n return d.get(key)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5670,"content_sha256":"3695690afd6cf0d337ed899da929f7598d66847b9e6813e6ac1dfb88300d0f30"},{"filename":"alpha/v06/helpers/identity_classifier.py","content":"#!/usr/bin/env python3\n\"\"\"identity_classifier.py — Deterministic identity classification for\ncross-sym whale candidates.\n\nv0.7 core. Given a candidate (address + cross_sym_tokens), this module:\n 1. Runs ONE unified `surf onchain-sql` to fetch the candidate's last\n 90 days of transfers across all cross_sym tokens\n 2. Aggregates into an 11-dimensional behavior SIGNATURE (locked, Python-derived)\n 3. Applies a STATIC ordered decision tree to classify identity (5 enums)\n 4. Returns (signature, classification) — both fully locked, NO LLM input\n\nThe LLM only writes `identity_narrative` (explaining WHY this classification)\nand `risk_assessment_narrative` — validator enforces those narratives\ncite at least 2 of the locked signature fields.\n\n5 identity enums (ordered evaluation, first hit wins):\n\n KOL_MANAGER — cross_sym ≥3 + pre_launch_insider ≥2\n ACTIVE_MM — bidirectional LP flow + high tx freq\n ARB_DESK — high CEX-side flow + short hold\n OTC_DESK — single large inflow + low freq\n UNKNOWN_WHALE_HIGH_CROSS_SYM — cross_sym ≥5 but no signature hit\n INSUFFICIENT_SIGNAL — fallback\n\nDesign constraints:\n - 1 surf onchain-sql per candidate (not 5) → cost control\n - 90-day window (partition-narrow → no SQL timeout)\n - All thresholds are MODULE CONSTANTS (auditable, not magic numbers)\n - LLM cannot freelance identity — validator gates narrative\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nfrom typing import Any\nfrom chain_router import transfers_table, dex_trades_table # v0.7.20\n\n# ----- Module constants -----\n\nDEFAULT_WINDOW_DAYS = 30 # v0.7 acceptance fix: 90d query timed out on\n # busy cross-sym whales. 30d still covers ARB\n # (\u003c7d hold) + most ACTIVE_MM cases; KOL_MANAGER\n # detection doesn't depend on signature window.\nSURF_SQL_TIMEOUT_SECS = 35\n\n# Classification thresholds (auditable, not magic in branches)\nKOL_MIN_CROSS_SYM = 3\nKOL_MIN_PRE_LAUNCH_INSIDER = 2\n\nMM_MIN_BIDIR_LP_RATIO = 0.40\nMM_MIN_TX_COUNT_90D = 20 # v0.7 acceptance: scaled from 60→20 since\n # default window dropped 90d→30d. ~3 tx/day\n # remains the MM heuristic intent.\n\nARB_MIN_CEX_FLOW_TOTAL = 0.80 # inflow_from_cex + outflow_to_cex > 0.80\nARB_MAX_AVG_HOLD_DAYS = 7.0\n\nOTC_MIN_LARGEST_INFLOW_PCT = 0.60\nOTC_MAX_TX_COUNT_90D = 10\n\nUNKNOWN_MIN_CROSS_SYM = 5\n\n\n# ----- Public API -----\n\ndef compute_signature(\n addr: str,\n cross_sym_cas: list[str],\n *,\n cex_hot_addrs: set[str],\n dex_pool_addrs: set[str],\n deployer_addrs: set[str],\n insider_addrs: set[str],\n window_days: int = DEFAULT_WINDOW_DAYS,\n) -> dict[str, Any]:\n \"\"\"Compute the 11-dim behavior signature for `addr` by running ONE\n unified surf onchain-sql across the candidate's cross_sym tokens.\n\n Args:\n addr: candidate address (lowercase 0x)\n cross_sym_cas: list of contract addresses to query transfers for\n cex_hot_addrs: set of known CEX hot wallet addrs (for counterparty tagging)\n dex_pool_addrs: set of known DEX main pool addrs\n deployer_addrs: set of known token deployer addrs\n insider_addrs: set of known pre-launch insider receiver addrs\n (from pre_launch_insider_index)\n window_days: lookback window (default 90d)\n\n Returns:\n {\n \"addr\": str,\n \"n_cas_queried\": int,\n \"window_days\": int,\n \"tx_count_90d\": int,\n \"total_inflow_amount\": float,\n \"total_outflow_amount\": float,\n \"inflow_from_cex_pct\": float, # 0.0-1.0\n \"inflow_from_dex_pct\": float,\n \"inflow_from_deployer_pct\": float,\n \"inflow_from_unlabeled_pct\": float,\n \"outflow_to_cex_pct\": float,\n \"outflow_to_dex_pct\": float,\n \"outflow_to_unlabeled_pct\": float,\n \"avg_hold_days\": float, # weighted by amount\n \"single_largest_inflow_pct\": float,\n \"bidirectional_lp_flow_ratio\": float,\n \"_surf_credits_used\": int,\n }\n\n Raises:\n ClassifierError on SQL or aggregation failure\n \"\"\"\n if not cross_sym_cas:\n return _empty_signature(addr, window_days)\n # v0.7.21.7: chain-aware validation. EVM lowercases for SQL match; Solana\n # preserves base58 case (case-sensitive). _addr_ok delegates to chain_router.\n if not _addr_ok(addr):\n raise ClassifierError(f\"invalid addr format: {addr!r}\")\n\n _is_solana = _chain_get_active() == \"solana\"\n addr_norm = addr if _is_solana else addr.lower()\n cas_norm = list(cross_sym_cas) if _is_solana else [c.lower() for c in cross_sym_cas]\n for c in cas_norm:\n if not _addr_ok(c):\n raise ClassifierError(f\"invalid ca in cross_sym_cas: {c!r}\")\n\n rows, credits_used = _query_unified_transfers(addr_norm, cas_norm, window_days)\n sig = _aggregate_signature(\n addr_norm,\n rows,\n cas_norm,\n window_days,\n cex_hot_addrs={(a if _is_solana else a.lower()) for a in cex_hot_addrs},\n dex_pool_addrs={(a if _is_solana else a.lower()) for a in dex_pool_addrs},\n deployer_addrs={(a if _is_solana else a.lower()) for a in deployer_addrs},\n insider_addrs={(a if _is_solana else a.lower()) for a in insider_addrs},\n )\n sig[\"_surf_credits_used\"] = credits_used\n return sig\n\n\ndef classify(\n signature: dict,\n *,\n cross_sym_count: int,\n pre_launch_insider_count: int,\n) -> dict[str, Any]:\n \"\"\"Apply the deterministic decision tree to (signature + context).\n\n Returns:\n {\n \"identity_enum\": str,\n \"confidence\": float (0-1),\n \"evidence_required_fields\": [str, ...] # which locked fields\n # the narrative MUST cite\n }\n\n NO LLM. Pure function of inputs. Same input → same output every time.\n \"\"\"\n s = signature\n\n # Rule 1: KOL_MANAGER\n if (cross_sym_count >= KOL_MIN_CROSS_SYM\n and pre_launch_insider_count >= KOL_MIN_PRE_LAUNCH_INSIDER):\n return {\n \"identity_enum\": \"KOL_MANAGER\",\n \"confidence\": 0.90,\n \"evidence_required_fields\": [\n \"cross_sym_count\",\n \"pre_launch_insider_count\",\n \"pre_launch_insider_tokens\",\n ],\n }\n\n # Rule 2: ACTIVE_MM\n # adversarial review P2b audit fix: use >= so threshold = trigger (was strict >)\n if (s.get(\"bidirectional_lp_flow_ratio\", 0) >= MM_MIN_BIDIR_LP_RATIO\n and s.get(\"tx_count_90d\", 0) >= MM_MIN_TX_COUNT_90D):\n return {\n \"identity_enum\": \"ACTIVE_MM\",\n \"confidence\": 0.85,\n \"evidence_required_fields\": [\n \"bidirectional_lp_flow_ratio\",\n \"tx_count_90d\",\n \"outflow_to_dex_pct\",\n ],\n }\n\n # Rule 3: ARB_DESK\n cex_total = s.get(\"inflow_from_cex_pct\", 0) + s.get(\"outflow_to_cex_pct\", 0)\n if (cex_total >= ARB_MIN_CEX_FLOW_TOTAL\n and 0 \u003c s.get(\"avg_hold_days\", 999) \u003c= ARB_MAX_AVG_HOLD_DAYS):\n return {\n \"identity_enum\": \"ARB_DESK\",\n \"confidence\": 0.80,\n \"evidence_required_fields\": [\n \"inflow_from_cex_pct\",\n \"outflow_to_cex_pct\",\n \"avg_hold_days\",\n ],\n }\n\n # Rule 4: OTC_DESK\n if (s.get(\"single_largest_inflow_pct\", 0) >= OTC_MIN_LARGEST_INFLOW_PCT\n and 0 \u003c s.get(\"tx_count_90d\", 999) \u003c= OTC_MAX_TX_COUNT_90D):\n return {\n \"identity_enum\": \"OTC_DESK\",\n \"confidence\": 0.75,\n \"evidence_required_fields\": [\n \"single_largest_inflow_pct\",\n \"tx_count_90d\",\n ],\n }\n\n # Rule 5: UNKNOWN_WHALE_HIGH_CROSS_SYM\n # v0.7.1: was [cross_sym_count] (only 1 field) but validator requires\n # ≥2 evidence cites — narrative could never satisfy. Add this_token_pct\n # so narrative can cite both \"cross-sym count\" + \"% in this token\".\n if cross_sym_count >= UNKNOWN_MIN_CROSS_SYM:\n return {\n \"identity_enum\": \"UNKNOWN_WHALE_HIGH_CROSS_SYM\",\n \"confidence\": 0.50,\n \"evidence_required_fields\": [\n \"cross_sym_count\",\n \"this_token_pct\",\n \"top_cross_sym_token\",\n ],\n }\n\n # Fallback\n return {\n \"identity_enum\": \"INSUFFICIENT_SIGNAL\",\n \"confidence\": 0.0,\n \"evidence_required_fields\": [],\n }\n\n\n# ----- Errors -----\n\nclass ClassifierError(Exception):\n \"\"\"Raised when classification cannot be completed.\"\"\"\n\n\n# ----- Internals -----\n\ndef _empty_signature(addr: str, window_days: int) -> dict:\n return {\n \"addr\": addr.lower(),\n \"n_cas_queried\": 0,\n \"window_days\": window_days,\n \"tx_count_90d\": 0,\n \"total_inflow_amount\": 0.0,\n \"total_outflow_amount\": 0.0,\n \"inflow_from_cex_pct\": 0.0,\n \"inflow_from_dex_pct\": 0.0,\n \"inflow_from_deployer_pct\": 0.0,\n \"inflow_from_unlabeled_pct\": 0.0,\n \"outflow_to_cex_pct\": 0.0,\n \"outflow_to_dex_pct\": 0.0,\n \"outflow_to_unlabeled_pct\": 0.0,\n \"avg_hold_days\": 0.0,\n \"single_largest_inflow_pct\": 0.0,\n \"bidirectional_lp_flow_ratio\": 0.0,\n \"_surf_credits_used\": 0,\n }\n\n\n# Validator: chain-aware address format check (extra defense for SQL injection).\n# v0.7.21.7: delegated to chain_router so Solana base58 candidates pass through.\n_ADDR_RE = re.compile(r\"^0x[0-9a-f]{40}$\") # back-compat constant\n\nfrom chain_router import ( # noqa: E402\n is_valid_addr as _chain_is_valid_addr,\n get_active_chain as _chain_get_active,\n)\n\n\ndef _addr_ok(addr) -> bool:\n if not isinstance(addr, str) or not addr:\n return False\n if _chain_get_active() == \"solana\":\n return _chain_is_valid_addr(addr)\n return _chain_is_valid_addr(addr.lower())\n\n\ndef _query_unified_transfers(addr: str, cas: list[str], window_days: int) -> tuple[list[dict], int]:\n \"\"\"ONE surf onchain-sql call. Returns list of transfer dicts.\n\n Filters: addr is from OR to, contract is one of cas, block_date in last\n window_days. Narrow date partition keeps query under timeout.\n\n SQL injection defense: addr + cas validated by regex BEFORE format string,\n so no user input flows into the SQL beyond validated 0x[a-f0-9]{40}.\n \"\"\"\n if not _addr_ok(addr):\n raise ClassifierError(f\"addr failed sanitization: {addr!r}\")\n for c in cas:\n if not _addr_ok(c):\n raise ClassifierError(f\"ca failed sanitization: {c!r}\")\n\n # Build IN list as quoted literals (validated above)\n cas_in = \",\".join(f\"'{c}'\" for c in cas)\n # v0.7.21.7: chain-aware case handling. Solana addresses are case-sensitive\n # base58 — wrapping in lower() would produce no matches.\n # v0.7.23 (surf-team 2026-06-09 reply #1): on BSC the EVM SQL must NOT\n # call lower() on contract_address / from / to in the WHERE clause.\n # The table is sorted by contract_address and has projections on\n # from/to; lower() forces a full table scan. addr + cas have already\n # been validated by `_addr_ok` (the regex `^0x[a-f0-9]{40} hertzflow — Skillopedia requires\n # lowercase), so the literals are guaranteed lowercase already —\n # querying the raw columns is correct and uses the index.\n if _chain_get_active() == \"solana\":\n sql = (\n f\"SELECT \\\"from\\\" AS from_addr, \\\"to\\\" AS to_addr, \"\n f\"contract_address AS ca, amount, block_time \"\n f\"FROM {transfers_table()} \"\n f\"WHERE (\\\"from\\\" = '{addr}' OR \\\"to\\\" = '{addr}') \"\n f\"AND contract_address IN ({cas_in}) \"\n f\"AND block_date >= today() - {int(window_days)} \"\n f\"ORDER BY block_time\"\n )\n else:\n sql = (\n f\"SELECT \\\"from\\\" AS from_addr, \\\"to\\\" AS to_addr, \"\n f\"contract_address AS ca, amount, block_time \"\n f\"FROM {transfers_table()} \"\n f\"WHERE (\\\"from\\\" = '{addr}' OR \\\"to\\\" = '{addr}') \"\n f\"AND contract_address IN ({cas_in}) \"\n f\"AND block_date >= today() - {int(window_days)} \"\n f\"ORDER BY block_time\"\n )\n\n body = json.dumps({\"sql\": sql, \"max_rows\": 5000})\n try:\n result = subprocess.run(\n [\"surf\", \"onchain-sql\"],\n input=body,\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n timeout=SURF_SQL_TIMEOUT_SECS,\n check=False,\n )\n except (subprocess.TimeoutExpired, OSError) as e:\n raise ClassifierError(f\"surf onchain-sql call failed: {e}\") from e\n\n if result.returncode != 0:\n raise ClassifierError(\n f\"surf onchain-sql exit {result.returncode}: {result.stderr[:300]}\"\n )\n\n try:\n doc = json.loads(result.stdout)\n except json.JSONDecodeError as e:\n raise ClassifierError(f\"surf onchain-sql returned non-JSON: {e}\") from e\n\n if doc.get(\"error\"):\n raise ClassifierError(\n f\"surf onchain-sql API error: {doc['error'].get('message')}\"\n )\n\n # v0.7.2: capture surf credits used from response meta. Used by\n # section_cross_sym to aggregate total cost into report footer.\n credits = int((doc.get(\"meta\") or {}).get(\"credits_used\") or 0)\n return doc.get(\"data\") or [], credits\n\n\ndef _aggregate_signature(\n addr: str,\n rows: list[dict],\n cas: list[str],\n window_days: int,\n *,\n cex_hot_addrs: set[str],\n dex_pool_addrs: set[str],\n deployer_addrs: set[str],\n insider_addrs: set[str],\n) -> dict:\n \"\"\"Aggregate transfer rows into the 11-dim signature.\n\n Sub-pipeline:\n 1. Partition rows into inflow / outflow based on addr position\n 2. Tag each counterparty as CEX / DEX / deployer / unlabeled\n (insider tag is reserved for downstream — KOL signal is at\n pre_launch_insider count level, not flow level)\n 3. Compute aggregates\n \"\"\"\n sig = _empty_signature(addr, window_days)\n sig[\"n_cas_queried\"] = len(cas)\n\n if not rows:\n return sig\n\n inflow_total = 0.0\n outflow_total = 0.0\n inflow_by_class = {\"cex\": 0.0, \"dex\": 0.0, \"deployer\": 0.0, \"unlabeled\": 0.0}\n outflow_by_class = {\"cex\": 0.0, \"dex\": 0.0, \"deployer\": 0.0, \"unlabeled\": 0.0}\n inflow_amounts = [] # for largest-inflow %\n lp_inflow = 0.0 # for bidirectional LP ratio\n lp_outflow = 0.0\n # avg_hold_days definition (v0.7.0): simple first-inflow-to-first-outflow\n # global span. Not amount-weighted (adversarial review P2b audit clarified). This is\n # crude but bounded; matches the ARB_DESK heuristic \"round-trip within\n # 7 days\" sufficiently well. v0.7.1+ may refine to FIFO-matched holds.\n inflow_times = [] # list of (amount, block_time) for in\n outflow_times = [] # list of (amount, block_time) for out\n n_valid_tx = 0 # adversarial review P2b audit fix: count only rows we actually\n # processed (not raw row count which includes malformed)\n\n for r in rows:\n from_a = (r.get(\"from_addr\") or \"\").lower()\n to_a = (r.get(\"to_addr\") or \"\").lower()\n try:\n amt = float(r.get(\"amount\") or 0)\n except (ValueError, TypeError):\n continue\n bt = r.get(\"block_time\")\n try:\n bt_int = int(bt) if bt else 0\n except (ValueError, TypeError):\n bt_int = 0\n\n if to_a == addr:\n # inbound to candidate\n n_valid_tx += 1\n inflow_total += amt\n inflow_amounts.append(amt)\n inflow_times.append((amt, bt_int))\n cls = _classify_counterparty(\n from_a, cex_hot_addrs, dex_pool_addrs, deployer_addrs\n )\n inflow_by_class[cls] += amt\n if cls == \"dex\":\n lp_inflow += amt\n elif from_a == addr:\n # outbound from candidate\n n_valid_tx += 1\n outflow_total += amt\n outflow_times.append((amt, bt_int))\n cls = _classify_counterparty(\n to_a, cex_hot_addrs, dex_pool_addrs, deployer_addrs\n )\n outflow_by_class[cls] += amt\n if cls == \"dex\":\n lp_outflow += amt\n\n sig[\"tx_count_90d\"] = n_valid_tx\n sig[\"total_inflow_amount\"] = inflow_total\n sig[\"total_outflow_amount\"] = outflow_total\n\n if inflow_total > 0:\n for k in inflow_by_class:\n sig[f\"inflow_from_{k}_pct\"] = inflow_by_class[k] / inflow_total\n if outflow_total > 0:\n for k in outflow_by_class:\n sig[f\"outflow_to_{k}_pct\"] = outflow_by_class[k] / outflow_total\n\n if inflow_amounts and inflow_total > 0:\n sig[\"single_largest_inflow_pct\"] = max(inflow_amounts) / inflow_total\n\n # Bidirectional LP ratio: min(LP_in, LP_out) / max(LP_in, LP_out)\n lp_max = max(lp_inflow, lp_outflow)\n if lp_max > 0:\n sig[\"bidirectional_lp_flow_ratio\"] = min(lp_inflow, lp_outflow) / lp_max\n\n # Average hold days: amount-weighted span from first inflow to first outflow.\n # If no outflow at all → 0 (handled by branch).\n if inflow_times and outflow_times:\n first_in_ts = min(t for _, t in inflow_times if t > 0) if any(t > 0 for _, t in inflow_times) else 0\n first_out_ts = min(t for _, t in outflow_times if t > 0) if any(t > 0 for _, t in outflow_times) else 0\n if first_in_ts > 0 and first_out_ts > first_in_ts:\n sig[\"avg_hold_days\"] = (first_out_ts - first_in_ts) / 86400.0\n\n return sig\n\n\ndef _classify_counterparty(\n addr: str,\n cex_hot: set[str],\n dex: set[str],\n deployer: set[str],\n) -> str:\n \"\"\"Single counterparty → class string ('cex' | 'dex' | 'deployer' | 'unlabeled').\"\"\"\n if addr in cex_hot:\n return \"cex\"\n if addr in dex:\n return \"dex\"\n if addr in deployer:\n return \"deployer\"\n return \"unlabeled\"\n\n\n# ----- CLI for manual testing -----\n\nif __name__ == \"__main__\":\n import argparse\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"--addr\", required=True)\n ap.add_argument(\"--cas\", nargs=\"+\", required=True, help=\"Cross-sym CAs to query\")\n ap.add_argument(\"--cex-hot\", nargs=\"*\", default=[])\n ap.add_argument(\"--dex\", nargs=\"*\", default=[])\n ap.add_argument(\"--deployer\", nargs=\"*\", default=[])\n ap.add_argument(\"--insider\", nargs=\"*\", default=[])\n ap.add_argument(\"--cross-sym-count\", type=int, default=0)\n ap.add_argument(\"--pre-launch-insider-count\", type=int, default=0)\n ap.add_argument(\"--window-days\", type=int, default=DEFAULT_WINDOW_DAYS)\n args = ap.parse_args()\n\n sig = compute_signature(\n args.addr,\n args.cas,\n cex_hot_addrs=set(args.cex_hot),\n dex_pool_addrs=set(args.dex),\n deployer_addrs=set(args.deployer),\n insider_addrs=set(args.insider),\n window_days=args.window_days,\n )\n cls = classify(\n sig,\n cross_sym_count=args.cross_sym_count,\n pre_launch_insider_count=args.pre_launch_insider_count,\n )\n print(json.dumps({\"signature\": sig, \"classification\": cls}, indent=2))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19415,"content_sha256":"3ff41ca6b50000a2a228149a1a8d5a9797210a64080f4ce86f7964bf5c2c42fc"},{"filename":"alpha/v06/helpers/monitoring_export.py","content":"#!/usr/bin/env python3\n\"\"\"monitoring_export.py — emit monitoring_wallets for Binance Wallet +\nOKX Wallet bulk import via paste.\n\nbeta.16 final shape (2026-05-24, user实测 CSV reject + Windows mojibake):\n- Binance Wallet + OKX wallet BOTH accept paste of `[{address, name,\n emoji}]` JSON. PASTE ROUTE ONLY — file upload abandoned.\n- Binance Wallet CSV file upload: tested, format rejected.\n- OKX wallet: no file upload option at all (paste only).\n- GMGN bulk import: all probe formats rejected (beta.13/14).\n\nWindows encoding fix (beta.16): paste.json emitted with\nensure_ascii=True so the file is pure ASCII bytes. Avoids the cp1252\nmojibake user reported in beta.15 (Windows viewer displays UTF-8 中文\n+ emoji as garbage Latin-1 chars; Ctrl+A Ctrl+C copies the mojibake\ninto Binance/OKX → garbage names imported). Binance/OKX JSON parsers\nun-escape `\\\\uXXXX` sequences automatically; end-user sees correct\n中文 + emoji in their tracker UI.\n\n## Output files (per pipeline run, 2 files)\n\nFor a CA → `out_dir/monitoring/`:\n\n- `monitoring_wallets_full.json` — canonical full schema (含 label /\n role / severity / alert / source_sym 完整字段). For own scripts /\n analytics. Not for tracker import.\n\n- `monitoring_paste.json` — `[{address, name, emoji}]` paste format for\n Binance Wallet + OKX wallet. Pure ASCII bytes (Windows-safe).\n\n## Usage\n\nONLY route — paste (both Binance Wallet + OKX):\n 1. Open `monitoring_paste.json` in any text viewer\n (file is pure ASCII, no encoding concerns)\n 2. Ctrl+A select all, Ctrl+C copy\n 3. Open Binance Wallet OR OKX wallet bulk import → paste in text area\n 4. Confirm/save — platform un-escapes \\\\uXXXX → 中文 + emoji rendered\n\n## Label format (cross-tracker safe)\n\n`\u003cSYM>-\u003cROLE>-\u003caddr5>` (e.g. \"ZEST-Deployer-3a6dc\"). ≤25 chars to clear\nall known tracker char limits without truncation. Grep-stable.\n\nROLE enum maps to short tokens:\n- Rule 11 Deployer → Deployer\n- Rule 11 Full Dumper → Dumper\n- Rule 11 Partial Dumper → PDumper\n- Rule 11 Quiet wallet → Quiet\n- DEX main LP → LP\n- Recent 72h anomaly → Anomaly\n\nv0.6 (2026-05-24, Phase B.5 restoration.)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport csv\nimport json\nimport os\nimport re\nimport sys\nfrom io import StringIO\nfrom pathlib import Path\nfrom typing import Any\n\nfrom i18n import t # v0.6.2 i18n\n\n\n# Role enum → short token for label assembly.\n# Order matters: more specific match first.\n# 中文 → 中文 short token (beta.9 — user request: 钱包标注跟报告语言一致).\n# Pipeline now emits Chinese role strings; tokens stay Chinese for GMGN\n# tag matching. Order: most-specific first.\n# v0.6.2 i18n: the LEFT side is the matching needle against pipeline /\n# render-emitted role strings (always carries Chinese/English regardless\n# of active lang, so it stays a literal). The RIGHT side is now an i18n\n# KEY — _role_token() resolves it via t() at lookup time so the emitted\n# short token follows the active lang (e.g. 庄家 vs Operator), as the\n# write_all() docstring documents.\n_ROLE_TOKEN_MAP = [\n # 中文 role strings from forensic_pipeline._build_monitoring_wallets.\n # v0.7.15 (派发/分发 术语统一): rule_11 链上转账场景一律用 \"分发\" 系列;\n # 旧 lang/zh.json 输出的 \"派发\" 系列仍保留映射,让 monitoring 能识别由\n # 上版本 render 出来的报告 — 不破老报告的钱包标注。\n (\"项目方部署钱包\", \"role.DEPLOYER.short\"),\n (\"项目方钱包\", \"role.DEPLOYER.short\"),\n # NEW (v0.7.15 — 链上\"分发\"系列). adversarial review LOW: more-specific BEFORE\n # general so a \"分发中内幕钱包\" wallet name is tagged via its own entry,\n # not the looser \"分发中钱包\" prefix.\n (\"已分完内幕钱包\", \"role.RULE11_FULL.short\"),\n (\"分发中内幕钱包\", \"role.RULE11_PARTIAL.short\"),\n (\"分发中钱包\", \"role.RULE11_PARTIAL.short\"),\n # LEGACY (pre-v0.7.15 — 旧 \"派发\" 系列, 仅用于老报告匹配); same ordering.\n (\"已派完内幕钱包\", \"mon.token_full_dumper_legacy\"),\n (\"派发中内幕钱包\", \"mon.token_partial_legacy\"),\n (\"派发中钱包\", \"mon.token_partial_legacy\"),\n (\"潜伏钱包\", \"role.RULE11_QUIET.short\"),\n (\"庄家中转地\", \"mon.token_operator\"),\n (\"庄家中转\", \"mon.token_operator\"),\n (\"跨币大户\", \"mon.token_cross_sym\"),\n (\"散户接收\", \"mon.token_retail\"),\n (\"近 72h 异常大单参与方\", \"role.ANOMALY.short\"),\n (\"异常大单参与方\", \"role.ANOMALY.short\"),\n (\"DEX 主池\", \"mon.token_lp\"),\n # English legacy (in case some path emits English) — fallback. Resolve\n # to the SAME i18n keys so the emitted token follows active lang too.\n (\"Cross-token whale\", \"mon.token_cross_sym\"),\n (\"Operator relay\", \"mon.token_operator\"),\n (\"operator relay\", \"mon.token_operator\"),\n (\"Deployer\", \"role.DEPLOYER.short\"),\n (\"Full Dumper\", \"mon.token_full_dumper_legacy\"),\n (\"Partial Dumper\", \"mon.token_partial_legacy\"),\n (\"Partial dumper\", \"mon.token_partial_legacy\"),\n (\"Quiet\", \"role.RULE11_QUIET.short\"),\n (\"DEX\", \"mon.token_lp\"),\n (\"LP\", \"mon.token_lp\"),\n (\"72h\", \"role.ANOMALY.short\"),\n (\"anomaly\", \"role.ANOMALY.short\"),\n (\"Anomaly\", \"role.ANOMALY.short\"),\n]\n\n\ndef _role_token(role_str: str) -> str:\n \"\"\"Map a free-form role string to a short token for label assembly.\n\n v0.6.2 i18n: _ROLE_TOKEN_MAP right side is an i18n key; resolve it via\n t() so the emitted token follows the active lang.\n \"\"\"\n for pattern, token_key in _ROLE_TOKEN_MAP:\n if pattern in role_str:\n return t(token_key)\n # Default: take first word, capitalize, truncate to 8 chars\n head = re.split(r\"[\\s\\(]\", role_str.strip(), maxsplit=1)[0]\n return (head or \"Other\")[:8]\n\n\ndef _build_label(symbol: str, role: str, address: str, max_len: int = 25) -> str:\n \"\"\"Assemble \u003cSYM>-\u003cROLE>-\u003caddr5> label, clipped to max_len.\"\"\"\n sym = (symbol or \"UNKNOWN\")[:6].upper()\n token = _role_token(role)\n addr5 = (address or \"\").lower().lstrip(\"0x\")[:5]\n label = f\"{sym}-{token}-{addr5}\"\n return label[:max_len]\n\n\ndef _severity_from_status(status_emoji: str) -> str:\n \"\"\"Map status emoji to severity tier.\"\"\"\n if status_emoji == \"🔴\":\n return \"critical\"\n if status_emoji == \"🟠\":\n return \"high\"\n if status_emoji == \"🟣\":\n # v0.7.3 cross-token mega-wallet — high priority forward-tracking\n # target (holds significant supply AND multiple Alpha tokens).\n return \"high\"\n if status_emoji == \"🟡\":\n return \"watch\"\n if status_emoji == \"🟢\":\n return \"resolved\"\n return \"watch\"\n\n\n_PLACEHOLDER = \"\u003cLLM_NARRATIVE_PLACEHOLDER>\"\n\n# CSV formula injection neutralizer (adversarial review beta.3 audit HIGH 2).\n# Cells starting with these chars can execute formulas / exfiltration\n# payloads when the CSV is opened in Excel / Google Sheets / OKX / Binance\n# Web3 tracker importers (most of which use spreadsheet-style parsers).\n# Per OWASP CSV injection guidance, prefix with single-quote to neutralize.\n_CSV_DANGEROUS_PREFIX = (\"=\", \"+\", \"-\", \"@\", \"\\t\", \"\\r\", \"\\n\")\n\n\ndef _sanitize_csv_cell(value: Any) -> str:\n \"\"\"Neutralize formula injection + control chars. Idempotent on safe input.\n\n adversarial review beta.4 8th audit MED: prior version only checked s[0]. Payloads\n like ' =CMD(...)' or '\\\\t=CMD(...)' (whitespace-prefixed) bypassed\n because the first char was whitespace (not dangerous), even though\n downstream Excel/Sheets trim leading whitespace before formula eval.\n\n Now: also check the first non-whitespace char. If THAT is a formula\n sigil, neutralize by prepending `'`.\n \"\"\"\n if value is None:\n return \"\"\n s = str(value)\n if not s:\n return s\n # Check first non-whitespace char for formula sigil.\n # str.lstrip() with no arg strips all Unicode whitespace, matching\n # how spreadsheets normalize.\n first_non_ws = s.lstrip()\n if first_non_ws and first_non_ws[0] in _CSV_DANGEROUS_PREFIX:\n s = \"'\" + s # OWASP-recommended neutralizer\n # Also strip embedded CR/LF that would break the row even with\n # csv.writer quoting (some downstream parsers don't follow RFC 4180).\n return s.replace(\"\\r\", \" \").replace(\"\\n\", \" \")\n\n\ndef _is_windows_short_path(p: Path) -> bool:\n \"\"\"Detect Windows 8.3 short filename (NTFS legacy alias).\n\n Strict 8.3 grammar (adversarial review beta.6 9th-audit HIGH): 1-6 chars + `~N` (N=1-9)\n + optional `.` + 0-3 chars, all chars excluding reserved set\n `\\\\/:*?\"\u003c>|`. Loose regex `^.+~[1-9]...` previously matched any\n filename ending in `~1.tar` (Linux backup convention) — converted\n cosmetic false-positive into security risk.\n\n v0.6.0-beta.6 fix: User's Windows %TEMP% resolves to `TESTUS~1` etc.\n Path.resolve() expands to long name → realpath-vs-absolute check\n triggered false-positive.\n\n Used ONLY as exception for realpath-mismatch comparison, NEVER as\n a fast path that skips symlink + ancestor + .. checks (adversarial review\n beta.6 9th-audit CRITICAL).\n \"\"\"\n if os.name != \"nt\":\n return False\n # Strict 8.3: 1-6 valid filename chars, ~ , digit 1-9, optional\n # 0-3 char extension. Excludes NTFS reserved chars in any segment.\n pattern = re.compile(r\"^[^\\\\/:*?\\\"\u003c>|]{1,6}~[1-9](\\.[^\\\\/:*?\\\"\u003c>|]{0,3})?$\")\n for part in p.absolute().parts:\n if pattern.match(part):\n return True\n return False\n\n\ndef _paths_equal_normalized(a: Path, b: Path) -> bool:\n \"\"\"Compare two paths after platform-aware normalization.\n\n v0.7.19.2: was `str(real) != str(absolute)` raw-string comparison,\n which on Windows wrongly flagged perfectly-canonical paths as\n \"realpath differs from absolute\" because of drive-letter case\n (`C:\\\\` vs `c:\\\\`) or separator differences (`/` vs `\\\\`).\n `os.path.normcase` lower-cases + flips `/` → `\\\\` on Windows\n (no-op on POSIX); `os.path.normpath` collapses `..` / `.` / dup\n separators. With both, equivalent paths compare equal regardless\n of cosmetic spelling. POSIX behavior is unchanged because\n normcase is identity on POSIX and normpath is structural-only.\n\n Reported by adversarial review BSC postmortem (2026-05-29): COLLECT run on\n Windows tripped the realpath-vs-absolute guard 3× in a row, each\n time at the same step but with cosmetically-different path\n spellings of the same on-disk directory.\n \"\"\"\n a_norm = os.path.normcase(os.path.normpath(str(a)))\n b_norm = os.path.normcase(os.path.normpath(str(b)))\n return a_norm == b_norm\n\n\ndef _is_macos_system_tmp_symlink(p: Path) -> bool:\n \"\"\"macOS system symlinks: `/tmp` → `/private/tmp`, `/var` → `/private/var`,\n `/etc` → `/private/etc`. Recognize so we don't reject platform default\n temp locations (tempfile.TemporaryDirectory uses /var/folders on macOS).\n\n Beta.9 (cross-LLM audit fix): broadened from /tmp-only to all\n 3 platform-system symlinks. Without /var, Python tempfile dirs got\n rejected by the ancestor walker (every macOS /var/folders/.../tmpXXX\n path has /var as a symlinked ancestor).\n \"\"\"\n if os.name != \"posix\":\n return False\n abs_str = str(p.absolute()).rstrip(\"/\")\n try:\n real_str = str(p.resolve(strict=False)).rstrip(\"/\")\n except (OSError, RuntimeError):\n return False\n for sysdir in (\"/tmp\", \"/var\", \"/etc\"):\n private = f\"/private{sysdir}\"\n if abs_str == sysdir and real_str == private:\n return True\n if abs_str.startswith(f\"{sysdir}/\") and real_str.startswith(f\"{private}/\"):\n # Verify suffix matches (no other redirect happening)\n if abs_str[len(sysdir):] == real_str[len(private):]:\n return True\n return False\n\n\ndef _walk_ancestors_for_symlinks(p: Path) -> None:\n \"\"\"Walk every existing ancestor of `p` from root down; raise ValueError\n if any ancestor is a user-controlled symlink.\n\n adversarial review beta.4 8th audit HIGH 1: a guard that only checks `p.is_symlink()`\n misses the case where `p` doesn't exist but an ancestor IS a symlink.\n mkdir(parents=True) on `/tmp/link_to_etc/newdir` happily creates\n `newdir` under whatever `link_to_etc` points to.\n\n macOS /tmp → /private/tmp is recognized as a system symlink (not\n user-controlled) and allowed.\n \"\"\"\n # Walk parents from root downward (deepest ancestor last)\n abs_p = p.absolute()\n parts = abs_p.parts\n cursor = Path(parts[0])\n for part in parts[1:]:\n cursor = cursor / part\n if not cursor.exists():\n return # rest of path doesn't exist; mkdir(parents=True) will create it from here\n if cursor.is_symlink():\n # macOS /tmp exception\n if _is_macos_system_tmp_symlink(cursor):\n continue\n raise ValueError(\n f\"monitoring_export: refused out_dir={str(p)!r} — \"\n f\"ancestor {str(cursor)!r} is a symlink \"\n f\"(realpath {str(cursor.resolve(strict=False))!r}). \"\n f\"mkdir(parents=True) would follow the link, leaking writes outside lexical path.\"\n )\n\n\ndef _guard_output_dir(out_dir: Path) -> Path:\n \"\"\"Path-traversal + symlink guard on the monitoring/ output directory.\n\n adversarial review beta.3 audit HIGH 1: render_report._guard_out_path only checks\n the report.md file path. monitoring_export was called with out_p.parent\n or args.out's parent — neither re-validated.\n\n adversarial review beta.4 8th audit HIGH 1: also walk every ancestor for symlinks\n (defeats the `/tmp/link/newdir` ancestor-redirect bypass where the\n target itself doesn't exist yet).\n\n Refuse:\n - \"..\" anywhere in the path string\n - NUL bytes\n - POSIX backslash (Windows backslash legitimate, gated by os.name)\n - `out_dir` itself a symlink (except macOS /tmp system symlink)\n - ANY existing ancestor a user-controlled symlink (beta.4 new check)\n - `out_dir` exists + realpath escapes lexical absolute path\n \"\"\"\n raw = str(out_dir)\n refuse_backslash = (os.name != \"nt\") and (\"\\\\\" in raw)\n if \"..\" in raw or refuse_backslash or \"\\x00\" in raw:\n raise ValueError(\n f\"monitoring_export: refused out_dir={raw!r} — contains '..', \"\n f\"POSIX-illegal '\\\\\\\\', or NUL byte.\"\n )\n p = Path(out_dir)\n\n # macOS-system-symlink exception applies FIRST (covers /tmp itself + subdirs).\n # Still walks ancestors above /tmp to detect attacker symlinks higher.\n macos_system = _is_macos_system_tmp_symlink(p)\n\n # Windows 8.3 short path is an NTFS alias, NOT a symlink. The ONLY\n # check it interferes with is `realpath != absolute` (resolve expands\n # TESTUS~1 → Testuser). All other guards run unconditionally.\n # adversarial review beta.6 9th-audit CRITICAL: previous version had this as a\n # full fast-path bypass, skipping symlink + ancestor + .. checks.\n win_8_3 = _is_windows_short_path(p)\n\n # ALWAYS check: out_dir itself a symlink (except macOS /tmp).\n if not macos_system and p.is_symlink():\n raise ValueError(\n f\"monitoring_export: refused out_dir={raw!r} — itself a symlink.\"\n )\n\n # ALWAYS walk ancestors for symlinks (catches attacker-planted symlinks\n # in /home/user/legitimate_dir → /etc style).\n _walk_ancestors_for_symlinks(p)\n\n # Realpath-vs-absolute check: skip on macOS /tmp + Windows 8.3\n # (false-positive sources), otherwise enforce. v0.7.19.2: compare\n # via _paths_equal_normalized to defuse Windows drive-letter case\n # / separator cosmetic differences that the raw `str(real) !=\n # str(absolute)` check flagged as symlinks.\n if not macos_system and not win_8_3 and p.exists() and p.is_dir():\n try:\n real = p.resolve(strict=True)\n if not _paths_equal_normalized(real, p.absolute()):\n raise ValueError(\n f\"monitoring_export: refused out_dir={raw!r} — \"\n f\"realpath {real!r} differs from absolute, \"\n f\"likely user-controlled symlink.\"\n )\n except FileNotFoundError:\n pass # parent not yet created\n return p\n\n\ndef _is_wallet_active_for_monitoring(w: dict) -> bool:\n \"\"\"beta.12 filter: drop wallets that are 'done' (post-distribution\n archaeology, not active monitoring targets).\n\n User feedback 2026-05-24: \"已分完 + 0 balance\" wallets are info-only,\n not monitoring targets. Monitoring should only emit wallets that\n are (a) still holding meaningful supply, or (b) recently active in\n high-value flows.\n\n KEEP rules (any one):\n - status_emoji=🔴 (critical: OPERATOR_RELAY / 潜伏钱包 with big balance)\n - status_emoji=🟠 (high: 分发中)\n - recent_activity_72h=True (近 72h 异常大单参与方)\n - balance_tokens > 0 (still holds something)\n\n DROP:\n - status_emoji=🟢 (resolved/已分完 with 0 balance)\n - 项目方部署钱包 with balance 0 + no recent_activity (分完空仓)\n \"\"\"\n emoji = w.get(\"status_emoji\", \"\")\n role = w.get(\"role\", \"\")\n balance = w.get(\"balance_tokens\")\n recent = bool(w.get(\"recent_activity_72h\"))\n\n # v1.2.7: trust the monitoring_ranker first (adversarial review HIGH). The ranker already\n # demoted sold-out (已分完) + dummy-decoy (潜伏 持0) wallets to NOT_TRACKED, and\n # promoted real forward targets to HIGH/CRITICAL. A HIGH/CRITICAL wallet stays\n # in the paste even at 0 balance — the DEPLOYER / mint authority / bridge can\n # act (re-fund / mint) without holding, and dropping them would lose the single\n # most important address to watch. NOT_TRACKED is dropped outright.\n level = w.get(\"monitor_level\")\n if level in (\"CRITICAL\", \"HIGH\"):\n return True\n if level == \"NOT_TRACKED\":\n return False\n\n # Fallback for NORMAL / un-ranked (legacy) skeletons:\n # KEEP critical / high / cross-sym regardless of balance\n if emoji in (\"🔴\", \"🟠\", \"🟣\"):\n return True\n # KEEP recent-activity wallets regardless\n if recent:\n return True\n # KEEP wallets with a non-trivial balance. v1.2.7: floor at 1 whole token —\n # the old `> 0` let float-rounding dust (1e-10 tokens, even tiny negatives on\n # the wrong side) count as \"still holding\", which leaked sold-out wallets into\n # the paste at HIGH (product spec 2026-06-29). Sub-1-token is dust, not a holding.\n if balance is not None and balance >= 1.0:\n return True\n # DROP everything else (resolved / 0-or-dust-balance with no recent activity)\n return False\n\n\n# v0.7.27 sort/filter constants for monitor_level — used by build_canonical\n# + to_paste_json so paste output reflects monitoring_ranker priority.\n_LEVEL_RANK = {\"CRITICAL\": 0, \"HIGH\": 1, \"NORMAL\": 2, \"NOT_TRACKED\": 3, \"\": 4}\n\n# v0.7.27 monitor_level → paste emoji. Distinct from severity emoji to avoid\n# overloading; both are kept on canonical entries (severity is legacy field\n# from anomaly status), monitor_level is the new actionable priority.\n_LEVEL_EMOJI = {\n \"CRITICAL\": \"🚨\",\n \"HIGH\": \"🔥\",\n \"NORMAL\": \"👀\",\n \"NOT_TRACKED\": \"💤\",\n}\n\n\ndef build_canonical(\n symbol: str,\n chain: str,\n contract_address: str,\n monitoring_wallets: list[dict],\n) -> list[dict]:\n \"\"\"Produce the canonical address list for export.\n\n Each entry: {address, chain, label, role, severity, alert,\n source_sym, source_contract, addr_short}\n\n `alert` field falls back to empty string when still\n \u003cLLM_NARRATIVE_PLACEHOLDER> (pipeline-time emit, before LLM fill).\n render_report.py re-emits after fill with real alert text.\n\n Beta.12: filters out 已分完 wallets with 0 balance (info-only,\n not actively dangerous). See _is_wallet_active_for_monitoring().\n \"\"\"\n # v0.7.21.7: chain-aware address validation. Solana base58 (32-44 chars,\n # case-sensitive) bypasses the old EVM-only `startswith(\"0x\") + len 42`\n # gate which silently dropped every Solana wallet from monitoring.\n from chain_router import is_valid_addr as _chain_is_valid_addr, get_active_chain as _chain_get_active\n _is_sol = _chain_get_active() == \"solana\"\n rows = []\n for w in monitoring_wallets or []:\n raw = w.get(\"addr_full\") or \"\"\n addr = raw if _is_sol else raw.lower()\n if not addr or not _chain_is_valid_addr(addr):\n continue\n if not _is_wallet_active_for_monitoring(w):\n continue # skip 已分完 + 0 balance wallets\n role = w.get(\"role\") or \"Other\"\n alert = w.get(\"alert\", \"\")\n if alert == _PLACEHOLDER:\n alert = \"\" # mask placeholder; render_report re-emits post-fill\n rows.append({\n \"address\": addr,\n \"chain\": chain.lower(),\n \"label\": _build_label(symbol, role, addr),\n \"role\": role,\n \"severity\": _severity_from_status(w.get(\"status_emoji\", \"🟡\")),\n \"alert\": alert,\n \"source_sym\": symbol,\n \"source_contract\": contract_address if _is_sol else contract_address.lower(),\n \"addr_short\": w.get(\"addr_short\", addr[:10]),\n # v0.7.27 monitoring_ranker fields (deterministic Python).\n # Default to NORMAL if monitoring_ranker didn't annotate (legacy\n # skeleton without Round 10) so paste export still works.\n \"monitor_level\": w.get(\"monitor_level\") or \"NORMAL\",\n \"monitor_score\": w.get(\"monitor_score\"),\n \"monitor_role_enum\": w.get(\"monitor_role_enum\") or \"other\",\n \"monitor_reason\": w.get(\"monitor_reason\") or \"\",\n \"source_behaviors\": w.get(\"source_behaviors\") or [],\n \"trigger_summary\": w.get(\"trigger_summary\") or \"\",\n })\n # v0.7.27 sort by monitor_level (CRITICAL → HIGH → NORMAL → NOT_TRACKED).\n # Stable secondary sort by role + address for deterministic output.\n rows.sort(key=lambda r: (_LEVEL_RANK.get(r.get(\"monitor_level\"), 4),\n r.get(\"role\") or \"\", r.get(\"address\") or \"\"))\n return rows\n\n\n_SEVERITY_EMOJI = {\n \"critical\": \"🔴\",\n \"high\": \"🟠\",\n \"watch\": \"🟡\",\n \"resolved\": \"🟢\",\n}\n\n\ndef to_paste_json(canonical: list[dict]) -> str:\n \"\"\"Paste-format `[{address, name, emoji}]` JSON for Binance Wallet +\n OKX wallet bulk import (paste route only — neither supports JSON\n file upload).\n\n Beta.16 fix (2026-05-24): use ensure_ascii=True so output is pure\n ASCII bytes (中文 → `\\\\u5e84\\\\u5bb6` escapes, emoji → surrogate pair\n escapes). User reported beta.15 paste.json display 乱码 on Windows\n viewer:\n \"ZEST-庄家-e30a0\" → \"ZEST-åºå®¶-e30a0\" (UTF-8 read as cp1252)\n \"🔴\" → \"ð´\"\n The bytes on disk WERE correct UTF-8 (we use write_bytes(... encode\n utf-8)), but Windows default cp1252 viewer mis-decoded → user's\n Ctrl+A Ctrl+C copied the mojibake → pasted garbage into Binance/OKX.\n Pure ASCII bytes are encoding-invariant — any Windows viewer shows\n the same bytes, clipboard preserves them, Binance/OKX JSON parser\n unescapes `\\\\u5e84\\\\u5bb6` → 庄家 on import. End-user sees correct\n 中文 in the tracker.\n\n Empirical (pre-mojibake) confirmation:\n - Binance Wallet bulk import → paste → ✅ works\n - OKX wallet bulk import → paste → ✅ works (paste only, no upload)\n \"\"\"\n # v0.7.27: skip NOT_TRACKED entries entirely (neutral infra — DEX\n # routers / public CEX hot wallets / DEX liquidity pools — they generate\n # constant noise that drowns out real signal in retail's tracker UI, and\n # wallet apps already surface CEX/LP state natively). Output sorted by\n # monitor_level via\n # build_canonical, so the first entries are always CRITICAL → HIGH.\n # Per-entry emoji uses monitor_level (🚨/🔥/👀) when available,\n # otherwise falls back to severity emoji for legacy compat.\n out = []\n for r in canonical:\n level = r.get(\"monitor_level\") or \"\"\n if level == \"NOT_TRACKED\":\n continue\n emoji = _LEVEL_EMOJI.get(level) or _SEVERITY_EMOJI.get(r.get(\"severity\"), \"🟡\")\n out.append({\n \"address\": r[\"address\"],\n \"name\": r[\"label\"],\n \"emoji\": emoji,\n # v0.7.27 paste schema additions — Binance Wallet / OKX paste\n # parsers ignore unknown fields (verified empirically beta.16),\n # so adding these doesn't break import.\n \"level\": level or \"NORMAL\",\n \"reason\": r.get(\"monitor_reason\") or \"\",\n })\n return json.dumps(out, ensure_ascii=True, indent=2)\n\n\n# Beta.16: to_unified_csv() removed. User empirical 2026-05-24:\n# - Binance Wallet file upload of monitoring.csv → \"format wrong\" rejected\n# - OKX wallet has no file-upload option at all (paste only)\n# CSV was a guess that didn't pan out. Both platforms only accept the\n# paste route via monitoring_paste.json. _sanitize_csv_cell helper kept\n# below for any future CSV emit needs (unused as of beta.16).\n\n\n# NOTE: to_binance_web3_csv / to_okx_csv removed in beta.11 — these CSVs\n# were based on incorrect assumption Binance Wallet + OKX wallet have no\n# bulk import. Both DO support bulk import via paste of GMGN-style JSON\n# (see to_paste_json above). User实测 confirmed paste works on both. The\n# CSV files were never importable; they were \"manual-entry reference\n# lists\" pretending to be useful but report.md monitoring table already\n# served that role with bscscan links.\n#\n# _sanitize_csv_cell kept as standalone utility — currently unused inside\n# this module but retained for any future CSV-emit needs (e.g., DeBank).\n\n\ndef write_all(\n *,\n symbol: str,\n chain: str,\n contract_address: str,\n monitoring_wallets: list[dict],\n out_dir: Path,\n lang: str = \"zh\",\n) -> dict[str, Any]:\n \"\"\"Write all 4 files to `out_dir/monitoring/`. Returns paths + counts.\n\n Path traversal + symlink guards applied (adversarial review beta.3 audit HIGH 1).\n Callers MUST NOT pass attacker-controlled out_dir; we re-validate here\n as defense in depth.\n\n v0.6.2: `lang` param (\"zh\" | \"en\") controls localization of wallet label\n short tokens (e.g. \"庄家\" vs \"Operator\"). Default zh for backward compat.\n \"\"\"\n # v0.6.2: switch to caller's lang for any t() calls during build_canonical /\n # to_paste_json. Loader is cached so this is a free-cost dict assignment.\n try:\n from i18n import set_lang\n set_lang(lang)\n except ImportError:\n # i18n module absent — silently fall back to whatever default was loaded.\n pass\n out_dir_guarded = _guard_output_dir(Path(out_dir))\n monitoring_dir = out_dir_guarded / \"monitoring\"\n # Pre-mkdir: refuse if monitoring/ exists as a symlink.\n if monitoring_dir.exists() and monitoring_dir.is_symlink():\n raise ValueError(\n f\"monitoring_export: refused — monitoring/ under \"\n f\"{out_dir!r} is itself a symlink.\"\n )\n monitoring_dir.mkdir(parents=True, exist_ok=True)\n\n # adversarial review beta.4 8th audit HIGH 2 (TOCTOU mitigation): immediately after\n # mkdir, re-resolve the path and verify it didn't end up somewhere\n # unexpected via a race during creation. Race window between resolve\n # and file write still exists in principle, but attacker would need to\n # swap paths within microseconds — much narrower than no check at all.\n #\n # Beta.14 fix: also exempt Windows 8.3 short path (parity with the\n # pre-mkdir guard at line 318). Symptom: adversarial review Windows env with tmp\n # under `C:\\Users\\TESTUS~1\\...` triggered post-mkdir TOCTOU here\n # because resolve() expands `TESTUS~1` → `cross-LLM tester`. The 8.3 alias\n # is NTFS metadata, not a user-controlled symlink, so it must be\n # exempted from this realpath-vs-absolute check (just like pre-mkdir).\n if not _is_macos_system_tmp_symlink(monitoring_dir) and not _is_windows_short_path(monitoring_dir):\n post_real = monitoring_dir.resolve(strict=True)\n post_abs = monitoring_dir.absolute()\n # v0.7.19.2: normalize before comparison so a cosmetic Windows\n # path-case / separator difference cannot masquerade as a\n # mid-mkdir attacker-swap. The actual TOCTOU guard is the\n # `is_symlink()` check immediately below — that one stays\n # strict (a symlink is a symlink regardless of casing).\n if not _paths_equal_normalized(post_real, post_abs):\n raise ValueError(\n f\"monitoring_export: post-mkdir TOCTOU detection — \"\n f\"resolved {post_real!r} differs from absolute \"\n f\"{post_abs!r}. Refusing all writes.\"\n )\n if monitoring_dir.is_symlink():\n raise ValueError(\n f\"monitoring_export: post-mkdir TOCTOU detection — \"\n f\"monitoring/ became a symlink. Refusing.\"\n )\n\n canonical = build_canonical(symbol, chain, contract_address, monitoring_wallets)\n\n files = {\n # Canonical: own scripts / analytics. Beta.17: ensure_ascii=True\n # (was False) — user 2026-05-24 reported same Windows cp1252\n # mojibake here as on paste.json: viewer showed \"ZEST-åºå®¶...\"\n # for \"ZEST-庄家...\". Consistency with paste.json: both emit pure\n # ASCII bytes, encoding-invariant across Windows viewers.\n # json.loads transparently un-escapes \\uXXXX for analytical\n # readers (Python / JS / jq) — no consumer behavior change.\n # Beta.18 (v0.6.1): renamed from monitoring_wallets.json →\n # monitoring_wallets_full.json. Pair with monitoring_paste.json\n # forms a clear paste-vs-full naming convention: paste = 粘贴版\n # (3 字段精简), full = 完整版 (含 role / severity / alert / etc).\n \"monitoring_wallets_full.json\": json.dumps(\n {\n \"schema_version\": \"0.6.1\",\n \"source_symbol\": symbol,\n \"source_contract\": contract_address.lower(),\n \"source_chain\": chain.lower(),\n \"n_wallets\": len(canonical),\n \"addresses\": canonical,\n },\n ensure_ascii=True, indent=2,\n ),\n # Paste route — Binance Wallet + OKX wallet text-paste bulk import.\n # Empirically verified working since beta.6 era. Beta.16 switches\n # to ensure_ascii=True (pure ASCII bytes) so Windows cp1252\n # viewers don't mojibake the 中文 + emoji content before user\n # Ctrl+A Ctrl+C copies it. JSON parsers on Binance/OKX un-escape\n # \\uXXXX sequences automatically — end-user sees 中文 + emoji\n # correctly in the imported wallet list.\n #\n # CSV file-upload route dropped in beta.16: user empirical 2026-05-24\n # found Binance Wallet rejects unified CSV format, and OKX has no\n # file-upload option at all (paste-only platform).\n \"monitoring_paste.json\": to_paste_json(canonical),\n }\n\n written = {}\n for fname, content in files.items():\n path = monitoring_dir / fname\n # All emitted files are JSON, written as plain UTF-8 bytes.\n # paste.json uses ensure_ascii=True so the bytes are also pure\n # ASCII — clipboard / Windows viewer safe.\n path.write_bytes(content.encode(\"utf-8\"))\n written[fname] = str(path)\n\n return {\n \"dir\": str(monitoring_dir),\n \"n_wallets\": len(canonical),\n \"files\": written,\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":31654,"content_sha256":"0a698567fcc80559da9c3e9d8d88cdfe6c51a77486db1f9eb9f9ac3535106f40"},{"filename":"alpha/v06/helpers/monitoring_ranker.py","content":"\"\"\"v0.7.27 — Deterministic monitoring address ranker.\n\nTranslates the pipeline-built monitoring_wallets[] (free-text role,\nemoji-status) into 4-tier monitor levels (CRITICAL / HIGH / NORMAL /\nNOT_TRACKED) using a deterministic Python score formula. retail then\npastes monitoring_paste.json into Binance Wallet / OKX without being\ndrowned by 50-100 same-weight entries.\n\nArchitecture (per ChatGPT v0.8 spec):\n raw_detectors → behavior_classifier → monitoring_ranker → renderer\n\nThe ranker is the *only* place that decides \"what does retail track\".\nbehavior_classifier supplies the source_behaviors set per address so\nscore can apply behavior linkage bonuses without extra surf SQL.\n\nOutput shape per monitoring_wallets[i] (added in-place):\n {\n ... existing fields (address, role, status_emoji, etc.) ...,\n \"monitor_level\": str, # CRITICAL / HIGH / NORMAL / NOT_TRACKED\n \"monitor_score\": int, # raw score (negative = penalty short-circuit)\n \"monitor_reason\": str, # 1-line zh: why this level\n \"monitor_role_enum\": str, # canonical role (e.g. mint_authority)\n \"source_behaviors\": [str], # behavior IDs that contributed (A1-D2)\n \"trigger_summary\": str, # what to watch for\n }\n\nScore formula (calibration: ChatGPT v0.8 spec section 4.3 — derived from\noperator behavior heuristic. NOT empirically calibrated yet; v0.8 expanded\ncalibration will validate on n ≥ 30 tokens):\n\n score = ROLE_BASE_SCORE[role]\n + balance_pct_bonus (>=1% supply +2, >=0.1% +1)\n + flow_pct_365d_bonus (>=1% supply +2, >=0.1% +1)\n + recency_bonus (72h +3 / 7d +2 / 60d +1)\n + behavior_linkage (A2/A3/C1 +3, C2/D1 +2, B1 +1)\n\n if router/aggregator OR public CEX hot wallet OR dex_pool: score = -999\n (NOT_TRACKED hard — neutral infra; wallet apps already surface CEX/LP natively)\n\n Level: score >= 8 → CRITICAL\n score >= 5 → HIGH\n score >= 2 → NORMAL\n else → NOT_TRACKED\n\nCONTEXT B ONLY (per M47): same context as behavior_classifier. Do NOT\napply to liquid trad market addresses without re-calibration — e.g.\n\"insider hold ≥ 1%\" trigger would false-flag legit VC / treasury\npositions on BTC/ETH.\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom i18n import t # v0.6.2 i18n\n\n# ----------------------------------------------------------------------\n# Role base scores. Higher = more \"this address moving is signal\".\n# Negative = NOT_TRACKED hard (infra / public CEX — they always move,\n# never carries information about operator intent).\n# ----------------------------------------------------------------------\nROLE_BASE_SCORE = {\n # Tier 1: any movement is operator-level signal\n \"mint_authority\": 6,\n \"bridge_contract\": 6,\n \"cex_fanout_hub\": 6,\n # v0.8.2: heuristically-flagged large unlabeled holders likely to be\n # project reserve / operator warehouse / VC OTC / MM cold storage.\n # Velvet/COLLECT/JCT test: these are the addresses outside m6/A2/LP\n # that hold ≥10% circulating + zero Arkham label. Any movement (especially\n # fanout to multiple EOAs / deposit to CEX / direct DEX sell) is a\n # high-confidence operator-action signal.\n \"suspected_operator_reserve\": 6,\n # v0.8.2: addresses receiving from a fake-mining mint authority cluster\n # (per fake_mining_detector). They look like miners on the surface but\n # are operator-controlled EOAs that received 0.5-3% supply each in\n # 1-3 lump transactions. Treat as operator allocation, not retail mining.\n \"fake_mining_cluster_member\": 6,\n\n # Tier 2: confirmed/observed actor with clear forensic role\n \"direct_dumper\": 5,\n \"deployer\": 5,\n \"treasury_vesting\": 4,\n \"high_throughput_operator\": 4,\n \"fanout_recipient\": 4,\n \"anomaly_participant\": 4,\n \"cross_alpha_active_operator\": 4,\n\n # Tier 3: secondary signal, watch but not primary\n \"cex_deposit_destination\": 3,\n \"cross_alpha_inactive_whale\": 2,\n \"unknown_top_holder\": 2,\n\n # NOT_TRACKED hard\n \"router_aggregator\": -999,\n \"public_cex_hot_wallet\": -999,\n # dex_pool (product spec 2026-06-29): a DEX liquidity pool is neutral, token-specific\n # infra — NOT a project/insider wallet to \"catch dumping\". The wallet apps the\n # paste imports into (Binance Wallet / OKX) ALREADY surface this token's LP /\n # pool state natively, so re-listing the pool address in the paste is redundant\n # noise. Keep it correctly classified in the report (m6 badge / waves / the\n # relay bucket of 真实派发), but drop it from the import paste like CEX/routers.\n \"dex_pool\": -999,\n\n # Fallback\n \"other\": 1,\n}\n\n# ----------------------------------------------------------------------\n# Free-text role string → canonical role enum.\n# Order matters (longest/most-specific match first). Pipeline-emitted\n# strings come from forensic_pipeline._build_monitoring_wallets + the\n# v0.7.21 fan-out / v0.7.24 mint-authority / high-throughput additions.\n# ----------------------------------------------------------------------\n_ROLE_STRING_MAP: list[tuple[str, str]] = [\n # Existing pipeline emissions (from _build_monitoring_wallets)\n (\"项目方部署钱包\", \"deployer\"),\n (\"项目方钱包\", \"deployer\"),\n (\"已分完内幕钱包\", \"deployer\"),\n (\"分发中内幕钱包\", \"deployer\"),\n (\"分发中钱包\", \"deployer\"),\n (\"潜伏钱包\", \"deployer\"),\n # Short-form (v0.7.27 fix: zest fixture uses \"已分完钱包\" without 内幕,\n # match shorter prefix). Place AFTER longer variants so \"已分完内幕钱包\"\n # still wins on real pipeline strings.\n (\"已分完\", \"deployer\"),\n (\"分发中\", \"deployer\"),\n (\"庄家中转地\", \"deployer\"),\n # Legacy 派发 wordings — keep mapping so old skeletons stay consistent.\n (\"已派完内幕钱包\", \"deployer\"),\n (\"派发中内幕钱包\", \"deployer\"),\n (\"派发中钱包\", \"deployer\"),\n (\"已派完\", \"deployer\"),\n (\"派发中\", \"deployer\"),\n (\"近 72h 异常大单参与方\", \"anomaly_participant\"),\n (\"近期异常活动钱包\", \"anomaly_participant\"),\n (\"跨币大户\", \"cross_alpha_inactive_whale\"),\n (\"DEX 主池\", \"dex_pool\"),\n (\"DEX pool\", \"dex_pool\"),\n # v0.7.24 mint authority + fan-out additions\n (\"mint authority\", \"mint_authority\"),\n (\"Mint authority\", \"mint_authority\"),\n (\"CEX fan-out hub\", \"cex_fanout_hub\"),\n (\"fan-out hub\", \"cex_fanout_hub\"),\n (\"fan-out recipient\", \"fanout_recipient\"),\n (\"fanout recipient\", \"fanout_recipient\"),\n (\"high-throughput operator\", \"high_throughput_operator\"),\n (\"high throughput operator\", \"high_throughput_operator\"),\n (\"operator\", \"anomaly_participant\"),\n # PLAY/JCT flow_operator names\n (\"LP\", \"dex_pool\"),\n]\n\n# Regex to extract \"持 XX.XX% 总供应\" / \"(持 N%)\" patterns from 工作流-emitted\n# role/alert strings when no separate balance_tokens field is present.\n# v0.7.27 zest fixture case: \"庄家中转地 (持 85.40% 总供应)\" carries the 85.4\n# in the role string itself; without parsing it we lose CRITICAL signal.\nimport re as _re\n_PCT_FROM_ROLE = _re.compile(r\"持\\s*(\\d+\\.?\\d*)\\s*%\")\n\n# ----------------------------------------------------------------------\n# Arkham label → role override. If Arkham label says DEX router or\n# CEX hot wallet, override pipeline role assignment.\n# ----------------------------------------------------------------------\n_ARKHAM_LABEL_OVERRIDES = {\n # Routers / aggregators — never useful to track\n \"1inch\": \"router_aggregator\",\n \"PancakeSwap\": \"router_aggregator\",\n \"OKX DEX\": \"router_aggregator\",\n \"Uniswap\": \"router_aggregator\",\n \"OpenOcean\": \"router_aggregator\",\n # Public CEX hot wallets — don't track (will dwarf all signals)\n \"Binance\": \"public_cex_hot_wallet\",\n \"Bybit\": \"public_cex_hot_wallet\",\n \"OKX\": \"public_cex_hot_wallet\",\n \"Bitget\": \"public_cex_hot_wallet\",\n}\n\n_ARKHAM_CLASSIFICATION_OVERRIDES = {\n \"DEX_ROUTER\": \"router_aggregator\",\n \"DEX_POOL\": \"dex_pool\",\n \"DEX_AGGREGATOR\": \"router_aggregator\",\n \"CEX_HOT_WALLET\": \"public_cex_hot_wallet\",\n}\n\n\ndef _canonical_role(w: dict, public_cex_addrs: set[str]) -> str:\n \"\"\"Map a monitoring_wallet entry's free-text role + Arkham label to\n a canonical role enum. Arkham label takes precedence on overrides\n (routers / public CEX) since pipeline role strings can mis-flag\n these as 'operator'.\n\n v0.7.27.1 adversarial review HIGH fix: cross-reference `public_cex_addrs` set\n (built from funding_attribution.cex_fanout_hubs[].cex_source*) so\n Bitget / KuCoin / etc hot wallets get NOT_TRACKED even when the\n pipeline-built monitoring_wallets entry doesn't carry the Arkham\n classification field (e.g. the JCT 0x1ab4973a Bitget hot wallet\n case).\"\"\"\n addr_lower = (w.get(\"addr_full\") or w.get(\"address\") or \"\").lower()\n if addr_lower and addr_lower in public_cex_addrs:\n return \"public_cex_hot_wallet\"\n # v0.8.2: pre-set monitor_role_enum (from hidden_operator_enricher)\n # takes precedence over re-canonicalization. The enricher categorizes\n # forensically-derived roles that cannot be inferred from free-text\n # role / Arkham label alone.\n # v0.8.2.2 audit LOW #6 fix: use `_is_enricher_assigned=True`\n # flag instead of a hardcoded enum allowlist, so future enricher roles\n # don't silently get re-canonicalized to \"other\".\n preset = (w.get(\"monitor_role_enum\") or \"\").strip()\n if w.get(\"_is_enricher_assigned\") and preset and preset in ROLE_BASE_SCORE:\n return preset\n # 1) Arkham classification (most reliable)\n arkham_cls = (w.get(\"arkham_classification\") or \"\").strip()\n if arkham_cls in _ARKHAM_CLASSIFICATION_OVERRIDES:\n return _ARKHAM_CLASSIFICATION_OVERRIDES[arkham_cls]\n # 2) Arkham entity name (string match — case-sensitive on common entities)\n arkham_entity = (w.get(\"arkham_entity_name\") or \"\").strip()\n for entity, role in _ARKHAM_LABEL_OVERRIDES.items():\n if entity.lower() in arkham_entity.lower():\n return role\n # 3) Pipeline free-text role string\n role_str = (w.get(\"role\") or \"\").strip()\n for needle, role_enum in _ROLE_STRING_MAP:\n if needle in role_str:\n return role_enum\n return \"other\"\n\n\ndef _public_cex_address_set(skel: dict) -> set[str]:\n \"\"\"v0.7.27.1 adversarial review HIGH fix: build address set of known public CEX\n hot wallets / deposit addresses from funding_attribution. These are\n pre-classified by surf Arkham labels at the detector stage. By\n cross-referencing this set against monitoring_wallets[].addr_full\n we catch CEX addresses the pipeline emit didn't label.\n \"\"\"\n out: set[str] = set()\n fa = skel.get(\"funding_attribution\") or {}\n hubs = ((fa.get(\"cex_fanout_hubs\") or {}).get(\"hubs\") or [])\n for h in hubs:\n addr = (h.get(\"cex_source\") or \"\").lower()\n cls = (h.get(\"cex_source_classification\") or \"\").upper()\n if addr and cls in (\"CEX_HOT_WALLET\", \"CEX_DEPOSIT\"):\n out.add(addr)\n # destination_label_summary often carries top CEX destinations too.\n dest = (fa.get(\"destination_label_summary\") or {})\n for cls_key in (\"CEX_HOT_WALLET\", \"CEX_DEPOSIT\"):\n for d in (dest.get(cls_key) or []):\n addr = (d.get(\"addr\") or d.get(\"address\") or \"\").lower()\n if addr:\n out.add(addr)\n return out\n\n\ndef _balance_pct_supply(w: dict, total_supply: float | int | None) -> float:\n \"\"\"Get balance % of total supply from monitoring_wallet entry.\n\n Two paths: explicit `balance_tokens` / `balance` numeric field\n (preferred), or regex-extracted \"持 XX.XX% 总供应\" from the role\n string (工作流 emits this format on 顶 持币人 / 庄家中转地\n wallets when the address holds significant supply but the entry\n doesn't carry a numeric balance field). Tolerates missing — 0.\"\"\"\n bal = w.get(\"balance_tokens\") or w.get(\"balance\") or 0\n if total_supply and total_supply > 0 and bal:\n return (bal / total_supply) * 100\n # Fallback: parse \"持 XX% 总供应\" from role string.\n role_str = w.get(\"role\") or \"\"\n m = _PCT_FROM_ROLE.search(role_str)\n if m:\n try:\n return float(m.group(1))\n except (TypeError, ValueError):\n pass\n return 0.0\n\n\ndef _flow_pct_supply(w: dict, total_supply: float | int | None) -> float:\n \"\"\"Get 365d throughput flow % of total supply. Currently uses\n pipeline-emitted `total_in_tokens` / `total_in` field if present.\"\"\"\n flow = w.get(\"total_in_tokens\") or w.get(\"total_in\") or 0\n if total_supply and total_supply > 0:\n return (flow / total_supply) * 100\n return 0.0\n\n\ndef _active_recency(w: dict) -> tuple[bool, bool, bool]:\n \"\"\"Return (active_72h, active_7d, active_60d). Conservative: if no\n recency hint present, returns all False (no recency bonus).\n\n Pipeline-emitted markers we can use:\n - `last_active_days_ago`: int, from cross_sym or anomaly section\n - `recent_anomaly_participant`: bool from anomaly tagging\n - status_emoji ∈ {🔴 → active, 🟠 → high but not necessarily 72h}\n \"\"\"\n days = w.get(\"last_active_days_ago\")\n if isinstance(days, (int, float)):\n return (days \u003c= 3, days \u003c= 7, days \u003c= 60)\n # Heuristic: anomaly participants are by definition 72h-active\n role_str = (w.get(\"role\") or \"\").strip()\n if \"72h\" in role_str or \"近期\" in role_str:\n return (True, True, True)\n return (False, False, False)\n\n\ndef _behavior_linkage_set(skel: dict) -> dict[str, set[str]]:\n \"\"\"Extract per-behavior address sets from skeleton for set-membership\n linkage checks. This is the v0.7.27 implementation of ChatGPT's spec\n `linked_to_X` semantics — we read addresses from each detector's\n output directly rather than via SQL trace (saves surf cost).\n\n v0.7.27.1 adversarial review HIGH fix: C1 must be INSIDER dumpers (intersection\n of dump_tracker top sellers AND lineage.m6 insider tree), not the\n full top_seller_addrs list which includes wash bots and unrelated\n high-frequency sellers. B1 uses top_seller_addrs as-is (it really\n IS \"top DEX sellers when wash_dominated\", which is the wash bot\n population), no longer copies the contaminated C1 set.\n \"\"\"\n sets: dict[str, set[str]] = {\n \"A2\": set(), # CEX fan-out hubs + recipients\n \"A3\": set(), # mint authorities\n \"B1\": set(), # wash bot top DEX sellers (when wash_dominated)\n \"C1\": set(), # INSIDER dumpers — m6 ∩ top_seller_addrs\n \"C2\": set(), # high-throughput operators\n \"D1\": set(), # cross-alpha whales\n }\n fa = skel.get(\"funding_attribution\") or {}\n\n # A2: CEX fan-out (hubs + recipients)\n hubs = ((fa.get(\"cex_fanout_hubs\") or {}).get(\"hubs\") or [])\n for h in hubs:\n addr = (h.get(\"addr\") or \"\").lower()\n if addr:\n sets[\"A2\"].add(addr)\n for r in (h.get(\"top_recipients\") or []):\n ra = (r.get(\"addr\") or \"\").lower()\n if ra:\n sets[\"A2\"].add(ra)\n\n # A3: mint authorities (only non-excluded)\n auths = ((fa.get(\"mint_authorities\") or {}).get(\"authorities\") or [])\n for a in auths:\n if a.get(\"is_excluded\"):\n continue\n addr = (a.get(\"addr\") or \"\").lower()\n if addr:\n sets[\"A3\"].add(addr)\n\n # Build the m6 insider address set for C1 intersection.\n m6_addrs: set[str] = set()\n for r in ((skel.get(\"lineage\") or {}).get(\"m6\") or {}).get(\"rows\") or []:\n addr = (r.get(\"address\") or r.get(\"addr\") or \"\").lower()\n if addr:\n m6_addrs.add(addr)\n\n # B1 + C1 from dump_tracker.top_seller_addrs.\n dt = skel.get(\"dump_tracking\") or {}\n top_sellers = set()\n for addr in (dt.get(\"top_seller_addrs\") or []):\n if isinstance(addr, str) and addr:\n top_sellers.add(addr.lower())\n\n # C1: INSIDER dumpers = m6 ∩ top_sellers. If m6 is empty (mining /\n # bridge token), C1 set is empty — A3 / C2 / etc cover those tokens.\n if m6_addrs:\n sets[\"C1\"] = top_sellers & m6_addrs\n # else: leave C1 empty rather than mass-tagging top sellers.\n\n # B1: wash bot top sellers — ONLY when wash_dominated. Use top_sellers\n # directly (acknowledging it's \"top sellers in a wash-dominated token\"\n # which is the wash bot population by construction). NO LONGER copies\n # C1's contaminated set.\n if dt.get(\"wash_dominated\"):\n sets[\"B1\"] = top_sellers\n\n # C2: high-throughput operators (only non-excluded)\n htd = ((fa.get(\"high_throughput_dumpers\") or {}).get(\"dumpers\") or [])\n for h in htd:\n if h.get(\"is_excluded\"):\n continue\n addr = (h.get(\"addr\") or \"\").lower()\n if addr:\n sets[\"C2\"].add(addr)\n\n # D1: cross_sym whales\n for w in ((skel.get(\"cross_sym\") or {}).get(\"whales\") or []):\n addr = (w.get(\"address\") or \"\").lower()\n if addr:\n sets[\"D1\"].add(addr)\n\n return sets\n\n\ndef _score(\n role_enum: str,\n balance_pct: float,\n flow_pct: float,\n active_72h: bool,\n active_7d: bool,\n active_60d: bool,\n source_behaviors: list[str],\n) -> int:\n \"\"\"Apply the v0.7.27 deterministic score formula.\"\"\"\n base = ROLE_BASE_SCORE.get(role_enum, 1)\n if base \u003c 0:\n return base # NOT_TRACKED hard penalty short-circuits\n\n s = base\n\n # Balance bonus — v0.7.27 self-fix: added >=10% / >=50% tiers so a\n # single wallet holding 85% supply lands CRITICAL on balance alone\n # (deployer base 5 + balance 5 = 10 → CRITICAL). Without these tiers\n # the score capped at base+2 and even pure operator concentration\n # only made HIGH.\n if balance_pct >= 50.0:\n s += 5\n elif balance_pct >= 10.0:\n s += 3\n elif balance_pct >= 1.0:\n s += 2\n elif balance_pct >= 0.1:\n s += 1\n\n # 365d throughput bonus — same expanded tiering.\n if flow_pct >= 50.0:\n s += 5\n elif flow_pct >= 10.0:\n s += 3\n elif flow_pct >= 1.0:\n s += 2\n elif flow_pct >= 0.1:\n s += 1\n\n # Recency bonus (priority: 72h > 7d > 60d, single tier)\n if active_72h:\n s += 3\n elif active_7d:\n s += 2\n elif active_60d:\n s += 1\n\n # Behavior linkage\n linkage_bonus = {\n \"A2\": 3, \"A3\": 3, \"C1\": 3,\n \"C2\": 2, \"D1\": 2,\n \"B1\": 1,\n }\n for bid in source_behaviors:\n s += linkage_bonus.get(bid, 0)\n\n return s\n\n\ndef _level_from_score(score: int) -> str:\n if score \u003c 0:\n return \"NOT_TRACKED\"\n if score >= 8:\n return \"CRITICAL\"\n if score >= 5:\n return \"HIGH\"\n if score >= 2:\n return \"NORMAL\"\n return \"NOT_TRACKED\"\n\n\n# v0.7.27.1 adversarial review HIGH fix: rephrase to avoid banned words (持有/卖出/派/\n# 进出/卖家/减仓/加仓). All 链上侦测-neutral facts about chain state.\n# v0.6.2 i18n: values are i18n keys, resolved via t() at lookup time (after\n# the pipeline / caller has set the active lang).\n_REASON_ZH = {\n \"mint_authority\": \"mon.reason_mint_authority\",\n \"bridge_contract\": \"mon.reason_bridge_contract\",\n \"cex_fanout_hub\": \"mon.reason_cex_fanout_hub\",\n # v0.8.2 新加 (v0.8.2.2 audit MED #4 fix: 去掉 bare English\n # `mint` / `allocation` / implementation name `fake_mining_detector`,\n # 改用中文术语).\n \"suspected_operator_reserve\": \"mon.reason_suspected_operator_reserve\",\n \"fake_mining_cluster_member\": \"mon.reason_fake_mining_cluster_member\",\n \"direct_dumper\": \"mon.reason_direct_dumper\",\n \"deployer\": \"mon.reason_deployer\",\n \"treasury_vesting\": \"mon.reason_treasury_vesting\",\n \"high_throughput_operator\": \"mon.reason_high_throughput_operator\",\n \"fanout_recipient\": \"mon.reason_fanout_recipient\",\n \"anomaly_participant\": \"mon.reason_anomaly_participant\",\n \"cross_alpha_active_operator\": \"mon.reason_cross_alpha_active_operator\",\n \"cross_alpha_inactive_whale\": \"mon.reason_cross_alpha_inactive_whale\",\n \"cex_deposit_destination\": \"mon.reason_cex_deposit_destination\",\n \"unknown_top_holder\": \"mon.reason_unknown_top_holder\",\n \"dex_pool\": \"mon.reason_dex_pool\",\n \"router_aggregator\": \"mon.reason_router_aggregator\",\n \"public_cex_hot_wallet\": \"mon.reason_public_cex_hot_wallet\",\n \"other\": \"mon.reason_other\",\n}\n\n# v0.8.2: trigger_summary per role — forensic descriptors (NOT alerts).\n# This skill is forensic — it does not push notifications. The role text\n# explains what type of operator-side wallet the address is, and what\n# kind of chain behavior would be relevant when the user manually checks\n# it. The user reads chain activity in their own wallet tracker\n# (Binance / OKX bulk import via paste.json), then cross-references\n# with the 真实派发段 of the same report to estimate USD realized.\n# v0.6.2 i18n: values are i18n keys, resolved via t() at lookup time.\n_TRIGGER_ZH = {\n \"suspected_operator_reserve\": \"mon.trigger_suspected_operator_reserve\",\n \"fake_mining_cluster_member\": \"mon.trigger_fake_mining_cluster_member\",\n}\n\n\ndef _is_sold_out(w: dict) -> bool:\n \"\"\"v1.2.7 (product spec 2026-06-29, option B): an insider wallet that has NOTHING left\n to dump going forward must NOT be a tracked monitoring target (neither 🔥 HIGH\n in the report nor present in the import paste). Two cases:\n\n 1. 已分完 / 已派完 (dumped ~100%): sold out — drop unconditionally, regardless\n of any dust / float-rounding sliver of balance left.\n 2. 潜伏 / 分发中 insider holding \u003c 1 token: an emptied or DUMMY-DECOY address.\n Operators spray 0-value address-poisoning transfers to look-alike-prefix\n addresses (e.g. 0x219f3a7a mirroring the real 0x219f074e); rule_11 picks\n them up as \"receivers\", and since they never dumped (dumped_pct==0) they\n got labelled \"潜伏钱包 (持 0 tokens)\" and scored HIGH. They hold nothing —\n not a forward holder. Real latent holders (balance ≥ 1) are kept.\n\n This is a skill-level fix: the deterministic scorer kept such wallets HIGH via\n the 365-day throughput bonus (a wallet that flowed 50%+ of supply historically\n scored +5 even on a 0 balance), and the paste filter's `balance > 0` check let\n float-dust (1e-10 tokens) sneak them back in. The real forward targets are\n wallets that STILL HOLD a meaningful balance (e.g. genuine quiet insiders).\"\"\"\n role = w.get(\"role\") or \"\"\n if (\"已分完\" in role) or (\"已派完\" in role):\n return True\n # decoy rule: only when the balance is a KNOWN current numeric — a None\n # (failed balance enrichment) must NOT be assumed empty (adversarial review MED).\n bal = w.get(\"balance_tokens\")\n if ((\"潜伏\" in role or \"分发中\" in role or \"派发中\" in role)\n and bal is not None and bal \u003c 1.0):\n return True\n return False\n\n\ndef annotate_monitoring_wallets(skel: dict) -> list[dict]:\n \"\"\"Top-level entry. Reads skeleton.monitoring_wallets[] and the\n behavior linkage sets, returns the SAME list with each entry\n annotated in-place. Caller can also call this on filled.json\n post-render to refresh.\n\n Returns: the annotated list (also mutates entries in place).\n \"\"\"\n wallets = skel.get(\"monitoring_wallets\") or []\n if not wallets:\n return wallets\n\n behavior_sets = _behavior_linkage_set(skel)\n public_cex_addrs = _public_cex_address_set(skel)\n total_supply = (skel.get(\"meta\") or {}).get(\"total_supply\")\n\n for w in wallets:\n # 1) Canonical role\n role_enum = _canonical_role(w, public_cex_addrs)\n # 2) Pull features\n balance_pct = _balance_pct_supply(w, total_supply)\n flow_pct = _flow_pct_supply(w, total_supply)\n a72, a7, a60 = _active_recency(w)\n # 3) Behavior linkage from address sets\n addr_lower = (w.get(\"addr_full\") or w.get(\"address\") or \"\").lower()\n source_behaviors = [\n bid for bid, addrs in behavior_sets.items() if addr_lower in addrs\n ]\n # 4) Score → level\n score = _score(role_enum, balance_pct, flow_pct, a72, a7, a60, source_behaviors)\n level = _level_from_score(score)\n # v1.2.7: a sold-out (已分完, dumped ~100%) wallet is post-mortem — force\n # NOT_TRACKED so it is neither HIGH in the report nor in the import paste\n # (to_paste_json skips NOT_TRACKED), regardless of its historical-flow\n # score or any dust balance. product spec 2026-06-29 option B.\n if _is_sold_out(w):\n score = -1\n level = \"NOT_TRACKED\"\n # 5) Reason + trigger summary\n reason = t(_REASON_ZH.get(role_enum, _REASON_ZH[\"other\"]))\n # v0.8.2: heuristic-derived hidden operator roles get their own\n # neutral trigger descriptor; other roles fall back to the\n # level-based descriptor.\n if role_enum in _TRIGGER_ZH:\n trigger = t(_TRIGGER_ZH[role_enum])\n elif level == \"CRITICAL\":\n trigger = t(\"mon.trigger_critical\", behaviors=\",\".join(source_behaviors) or t(\"mon.behaviors_none\"))\n elif level == \"HIGH\":\n # v0.8.0.3: use ≥ instead of > so md_cell HTML-escape doesn't\n # convert to > in render output (\"DEX 路由\" is the\n # translation regression — restored to \"DEX 路由\" once).\n trigger = t(\"mon.trigger_high\")\n elif level == \"NORMAL\":\n trigger = t(\"mon.trigger_normal\")\n else:\n trigger = t(\"mon.trigger_not_tracked\")\n\n # 6) Annotate\n w[\"monitor_level\"] = level\n w[\"monitor_score\"] = score\n w[\"monitor_role_enum\"] = role_enum\n w[\"monitor_reason\"] = reason\n w[\"source_behaviors\"] = source_behaviors\n w[\"trigger_summary\"] = trigger\n\n return wallets\n\n\ndef summary_by_level(wallets: list[dict]) -> dict[str, int]:\n \"\"\"Return tally of {level: count} across the annotated list.\"\"\"\n out = {\"CRITICAL\": 0, \"HIGH\": 0, \"NORMAL\": 0, \"NOT_TRACKED\": 0}\n for w in wallets or []:\n lvl = w.get(\"monitor_level\") or \"NOT_TRACKED\"\n out[lvl] = out.get(lvl, 0) + 1\n return out\n","content_type":"text/x-python; charset=utf-8","language":"python","size":26162,"content_sha256":"0de827a9a706316bf3ee579312d9193f8637dca0cb452380fdc115bf258bf4b0"},{"filename":"alpha/v06/helpers/parallel_surf.py","content":"#!/usr/bin/env python3\n\"\"\"parallel_surf.py — fire N independent surf onchain-sql queries in parallel.\n\nUsage as a CLI:\n python3 parallel_surf.py q1.json q2.json q3.json -> r1.json r2.json r3.json\n (output files share the input filename with .out.json suffix instead of .json)\n\nUsage as a module:\n from parallel_surf import run_parallel\n results = run_parallel(['q1.json', 'q2.json', 'q3.json'])\n # results = {'q1.json': parsed_json, 'q2.json': parsed_json, ...}\n\nEach query JSON file follows surf's input format:\n {\"max_rows\": N, \"sql\": \"SELECT ...\"}\n\nThe helper spawns N concurrent `surf onchain-sql @\u003cfile>` subprocesses. The\nshell-layer parallelism is sufficient because each surf invocation is a\nself-contained HTTP round-trip; surf does not share state across processes.\n\nRecommended max concurrency: 8. Surf's server-side per-account concurrency\nlimit kicks in past that, returning 429-style throttling. The helper does\nNOT enforce this — caller chooses how many JSON files to pass.\n\nCost: each query independently bills credits; parallel does not save credits,\nonly wall clock. For BABYSHARK forensic ~10 queries: sequential ~5 min,\nparallel-8 ~40 sec. Cost identical either way.\n\nv0.5.7 — wall-clock optimization.\nv0.7.13 — per-query transient-error retry with jittered backoff (issue #1 Bug 1).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport concurrent.futures as _cf\nimport json\nimport random\nimport re\nimport subprocess\nimport sys\nimport time\nfrom datetime import date, timedelta\nimport os\nfrom pathlib import Path\n\n# v0.9.9 CRITICAL deadlock fix (user report 2026-06-17): hard per-attempt\n# subprocess timeout for `surf onchain-sql`. Without it, a hung surf CLI\n# (stuck network connection) blocks subprocess.run forever → ThreadPool\n# worker never returns → pipeline deadlocks at futex_wait, CPU 0%. Chunks\n# are sized to fit surf's 30s budget; 90s gives headroom for a slow-but-\n# progressing query while still catching a truly stuck one. Override via\n# env for debugging against a slow link.\n_SURF_SUBPROCESS_TIMEOUT = int(os.environ.get(\"BINANCE_ALPHA_SURF_SUBPROCESS_TIMEOUT\", \"90\"))\n\n# v0.9.1: surf 365-day window guard. Large tables enforce a 365-day\n# block_date window. Any SQL bumping the limit returns INVALID_REQUEST.\n# These tables are subject to the rule (per surf error responses observed\n# 2026-06-14 SIREN forensic):\n_SURF_LARGE_TABLES = (\n \"bsc_transfers\", \"bsc_dex_trades\",\n \"ethereum_transfers\", \"ethereum_dex_trades\",\n \"arbitrum_transfers\", \"arbitrum_dex_trades\",\n \"base_transfers\", \"base_dex_trades\",\n \"polygon_transfers\", \"polygon_dex_trades\",\n \"optimism_transfers\", \"optimism_dex_trades\",\n)\n_BLOCK_DATE_RE = re.compile(\n r\"\"\"block_date\\s*>=\\s*['\"]?(\\d{4}-\\d{2}-\\d{2})['\"]?\"\"\",\n re.IGNORECASE,\n)\n# Match `today() - \u003cN>` / `today()- \u003cN>` — these are surf-safe by construction\n# as long as N \u003c= 365. We require N \u003c= 364 to be conservative (matches the\n# surf_constraints helper).\n_TODAY_MINUS_RE = re.compile(\n r\"\"\"block_date\\s*>=\\s*today\\(\\)\\s*-\\s*(\\d+)\"\"\",\n re.IGNORECASE,\n)\n\n\ndef _check_surf_365_day_window(payload: str, input_path: str) -> dict | None:\n \"\"\"Pre-flight check: refuse SQL that violates surf's 365-day window on\n large transfer / dex_trades tables. Returns an error dict if violated,\n None if safe.\n\n Implementation: parse JSON payload to get the SQL string, scan for\n `block_date >=` against any `_SURF_LARGE_TABLES` table, and verify\n the floor date is within 364 days of today. The check is permissive:\n `today() - N` forms with N \u003c= 364 are accepted; only literal\n `'YYYY-MM-DD'` dates that are >364 days back trigger the refusal.\n \"\"\"\n try:\n body = json.loads(payload)\n sql = body.get(\"sql\") or \"\"\n except (json.JSONDecodeError, AttributeError):\n return None # Not a JSON-body call; let the surf server validate.\n if not sql:\n return None\n sql_lower = sql.lower()\n # If the SQL doesn't reference any of the large tables, no enforcement.\n if not any(t in sql_lower for t in _SURF_LARGE_TABLES):\n return None\n safe_floor = date.today() - timedelta(days=364)\n # Check literal `block_date >= 'YYYY-MM-DD'` patterns.\n for m in _BLOCK_DATE_RE.finditer(sql):\n try:\n d = date.fromisoformat(m.group(1))\n except ValueError:\n continue\n if d \u003c safe_floor:\n return {\n \"error\": {\n \"code\": \"INVALID_REQUEST_GUARD\",\n \"message\": (\n f\"surf 365-day window guard (v0.9.1): SQL in \"\n f\"{input_path} uses block_date >= '{m.group(1)}' \"\n f\"against a large surf table, but {m.group(1)} is \"\n f\"older than surf's 364-day safe floor \"\n f\"({safe_floor.isoformat()}). Caller MUST wrap \"\n f\"date_floor / listing_date with \"\n f\"helpers.surf_constraints.surf_safe_date_floor() \"\n f\"before constructing the query. Pre-flight refused \"\n f\"to avoid the silent-loss-of-data bug (SIREN \"\n f\"2026-06-14: dump_tracker $0 vs on-chain $45.7M).\"\n ),\n }\n }\n # `today() - N` forms — only accept N \u003c= 364.\n for m in _TODAY_MINUS_RE.finditer(sql):\n try:\n n = int(m.group(1))\n except ValueError:\n continue\n if n > 364:\n return {\n \"error\": {\n \"code\": \"INVALID_REQUEST_GUARD\",\n \"message\": (\n f\"surf 365-day window guard (v0.9.1): SQL in \"\n f\"{input_path} uses block_date >= today() - {n} \"\n f\"against a large surf table, but {n} exceeds the \"\n f\"364-day safe floor. Reduce the window or split \"\n f\"the query.\"\n ),\n }\n }\n return None\n\n\n# adversarial review MEDIUM #2: a request/validation error (bad SQL, unknown column) is\n# PERMANENT — retrying it 4× just wastes time and multiplies load across the 8\n# parallel workers. Fast-fail those; everything else (429 / 5xx / timeout /\n# truncated-JSON-under-load) is treated as transient and retried. Note the\n# @file-vs-stdin INVALID_REQUEST is already resolved by the stdin-pipe\n# fallback within a single attempt, so a residual INVALID_REQUEST here is\n# a real one.\n_PERMANENT_ERROR_MARKERS = (\n \"invalid_request\", \"syntax\", \"unknown identifier\", \"does not exist\",\n \"doesn't exist\", \"illegal\", \"cannot parse\", \"missing column\",\n \"unknown table\", \"type mismatch\",\n)\n\n\ndef _is_transient_error(resp: dict) -> bool:\n \"\"\"True if an error response is worth retrying (transient), False if it is\n a permanent request/schema error that will never succeed on retry.\"\"\"\n err = resp.get(\"error\", {})\n blob = (str(err.get(\"code\", \"\")) + \" \" + str(err.get(\"message\", \"\"))).lower()\n return not any(m in blob for m in _PERMANENT_ERROR_MARKERS)\n\n\ndef _run_one(input_path: str, max_attempts: int = 4) -> tuple[str, dict, float]:\n \"\"\"Run a single surf query with transient-error retry. Returns\n (input_path, parsed_response, elapsed_s).\n\n v0.5.9 (adversarial review BABYSHARK 2026-05-23 field test):\n Some surf CLI versions accept `surf onchain-sql @\u003cfile>`, others require\n `surf onchain-sql --json` with the JSON body via STDIN. the review's Win11\n surf version rejected the @file form, returning INVALID_REQUEST despite\n the helper claiming success. Fix: try @file first; if it returns an API\n error containing 'INVALID_REQUEST' / 'Send a JSON object' / 'not a JSON\n object' (the surf CLI's complaint about @file syntax), fall back to\n stdin-piped JSON via `--json`.\n\n v0.7.13 (issue #1 Bug 1): added exponential-backoff retry on TRANSIENT\n surf failures (429 Too Many Requests / subprocess non-zero exit / 5xx /\n JSON-decode / top-level surf \"error\"). Before this, a single 429 surfaced\n as a hard {\"error\": ...}; on rule_11's sub-dumper balance backfill that\n silently dropped the backfill and left `dumped_pct=None`, which then\n crashed section_alloc / forensic_pipeline. run_parallel fans out up to 8\n of these concurrently, so a burst of 429s (\"drop a CA\" concurrent runs)\n is exactly the failure mode — backoff carries jitter so the 8 workers do\n NOT retry in lockstep and re-trigger the rate limit. Mirrors\n section_a_scope._run_surf_with_retry (4 attempts, ~1/3/7s).\n \"\"\"\n payload = Path(input_path).read_text(encoding=\"utf-8\")\n last_resp: dict = {\"error\": {\"code\": \"NO_ATTEMPT\", \"message\": \"no attempt made\"}}\n last_elapsed = 0.0\n\n # v0.9.1: pre-flight 365-day window guard. surf enforces a 365-day\n # block_date window on bsc_transfers / bsc_dex_trades (and the analogous\n # ethereum_/arbitrum_/base_/polygon_/optimism_ tables). Any SQL that\n # bumps that limit returns INVALID_REQUEST and is silently consumed by\n # the section. SIREN 2026-06-14 lost dump_tracker entirely this way\n # ($0 confirmed_sellout vs. EmberCN-observed $45.7M on-chain). Guard\n # scans the payload for `block_date >= '\u003cYYYY-MM-DD>'` against a large\n # table and refuses to run if the date is >364 days old, returning a\n # clear error pointing the caller at helpers.surf_constraints.\n _guard_err = _check_surf_365_day_window(payload, input_path)\n if _guard_err is not None:\n return (input_path, _guard_err, 0.0)\n\n # v0.9.4: track attempts + wall time for credit accounting. Even failed\n # retry attempts get billed (timeout retry storms must be visible).\n _attempts_billed = 0\n _seconds_total = 0.0\n for attempt in range(1, max_attempts + 1):\n _attempts_billed += 1\n t0 = time.perf_counter()\n # v0.9.9 CRITICAL (deadlock fix, user report 2026-06-17 CA\n # 0x5dbde81f...): subprocess.run had NO timeout. When the surf CLI\n # hangs on a stuck network connection (connected but server never\n # responds), subprocess.run blocks FOREVER → the ThreadPoolExecutor\n # worker never returns → run_parallel's as_completed waits forever\n # → the whole pipeline deadlocks at futex_wait, CPU 0%, no network.\n # rule_11's chunked mint lookup (run_parallel → _run_one) was the\n # exact hang site (46+ min). section_a_scope._run_surf_with_retry\n # already had a timeout — this path was the gap. Hard cap each\n # attempt; TimeoutExpired is treated as a transient error so the\n # existing retry/backoff handles it, then fails loud after\n # max_attempts instead of hanging.\n try:\n # Attempt 1: @file form (older surf CLI / current alpha.40)\n proc = subprocess.run(\n [\"surf\", \"onchain-sql\", f\"@{input_path}\"],\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n check=False, timeout=_SURF_SUBPROCESS_TIMEOUT,\n )\n except subprocess.TimeoutExpired:\n elapsed = time.perf_counter() - t0\n _seconds_total += elapsed\n resp = {\"error\": {\"code\": \"SURF_TIMEOUT\",\n \"message\": f\"surf onchain-sql hung > {_SURF_SUBPROCESS_TIMEOUT}s (killed)\"}}\n last_resp, last_elapsed = resp, elapsed\n if attempt \u003c max_attempts:\n time.sleep(min(2 ** attempt - 1, 7) + random.uniform(0, 0.5 * attempt))\n continue\n _record_credit(None, _seconds_total, _attempts_billed, success=False)\n return (input_path, resp, elapsed)\n elapsed = time.perf_counter() - t0\n\n # Detect \"surf CLI doesn't grok @file\" — these markers come from surf's\n # own server-side validator response when the @file syntax is not parsed\n # client-side and the literal \"@/path\" string is sent as the SQL.\n output = proc.stdout + proc.stderr\n surf_cli_mismatch = (\n \"INVALID_REQUEST\" in output and \"Send a JSON object\" in output\n ) or \"not a JSON object\" in output\n\n if proc.returncode != 0 or surf_cli_mismatch:\n # v0.8.7.2: fall back to stdin-piped JSON, NO --json flag. surf\n # CLI's --json is now a global output-format flag (alias for\n # `-o json`) and is rejected by onchain-sql when the API spec\n # cache is stale (manifests as \"unknown flag: --json\", see\n # ESPORTS report 2026-06-12). Default output is already JSON,\n # so --json is redundant — dropping it removes the spec-cache\n # dependency entirely.\n t0 = time.perf_counter()\n try:\n proc = subprocess.run(\n [\"surf\", \"onchain-sql\"],\n input=payload,\n capture_output=True, text=True, encoding=\"utf-8\", errors=\"replace\",\n check=False, timeout=_SURF_SUBPROCESS_TIMEOUT,\n )\n except subprocess.TimeoutExpired:\n elapsed = time.perf_counter() - t0\n _seconds_total += elapsed\n resp = {\"error\": {\"code\": \"SURF_TIMEOUT\",\n \"message\": f\"surf onchain-sql (stdin) hung > {_SURF_SUBPROCESS_TIMEOUT}s (killed)\"}}\n last_resp, last_elapsed = resp, elapsed\n if attempt \u003c max_attempts:\n time.sleep(min(2 ** attempt - 1, 7) + random.uniform(0, 0.5 * attempt))\n continue\n _record_credit(None, _seconds_total, _attempts_billed, success=False)\n return (input_path, resp, elapsed)\n elapsed = time.perf_counter() - t0\n\n _seconds_total += elapsed\n if proc.returncode != 0:\n resp: dict = {\"error\": {\"code\": \"SURF_EXIT\", \"message\": proc.stderr.strip() or proc.stdout.strip()[:500]}}\n else:\n try:\n resp = json.loads(proc.stdout)\n except json.JSONDecodeError as e:\n resp = {\"error\": {\"code\": \"PARSE_ERROR\", \"message\": str(e), \"raw\": proc.stdout[:500]}}\n\n # Success = a response that is not a dict carrying an \"error\" key.\n if not (isinstance(resp, dict) and \"error\" in resp):\n _record_credit(resp, _seconds_total, _attempts_billed, success=True)\n return (input_path, resp, elapsed)\n\n # Permanent request/schema error → fail fast (no point retrying).\n if not _is_transient_error(resp):\n _record_credit(None, _seconds_total, _attempts_billed, success=False)\n return (input_path, resp, elapsed)\n\n # Transient — back off (with jitter) and retry.\n last_resp, last_elapsed = resp, elapsed\n if attempt \u003c max_attempts:\n time.sleep(min(2 ** attempt - 1, 7) + random.uniform(0, 0.5 * attempt))\n\n _record_credit(None, _seconds_total, _attempts_billed, success=False)\n return (input_path, last_resp, last_elapsed)\n\n\ndef _record_credit(resp: dict | None, seconds: float, attempts: int, success: bool) -> None:\n \"\"\"v0.9.4: report this query's surf usage to the section_a_scope\n credit accumulator. Lazy import to avoid circular import (section_a_scope\n imports run_parallel from here). Silently no-ops if section_a_scope can't\n be imported — credit accounting must NEVER break the pipeline.\"\"\"\n try:\n from section_a_scope import _surf_credit_add # lazy: break cycle\n except Exception:\n return\n credits = 0.0\n if resp:\n try:\n credits = float((resp.get(\"meta\") or {}).get(\"credits_used\") or 0)\n except (TypeError, ValueError, AttributeError):\n credits = 0.0\n try:\n _surf_credit_add(\n credits=credits, seconds=seconds,\n attempts=attempts, success=success,\n )\n except Exception:\n pass\n\n\ndef run_parallel(input_paths: list[str], max_workers: int = 8) -> dict[str, dict]:\n \"\"\"Run all input queries in parallel. Returns {input_path: response_json}.\n\n max_workers caps concurrency. Surf's server side throttles past 8 — keep\n default unless you've verified your account tier supports more.\n \"\"\"\n results: dict[str, dict] = {}\n timings: dict[str, float] = {}\n with _cf.ThreadPoolExecutor(max_workers=max_workers) as ex:\n futures = {ex.submit(_run_one, p): p for p in input_paths}\n for fut in _cf.as_completed(futures):\n path, resp, elapsed = fut.result()\n results[path] = resp\n timings[path] = elapsed\n return results, timings # type: ignore[return-value]\n\n\ndef main() -> int:\n p = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n p.add_argument(\"inputs\", nargs=\"+\", help=\"Query JSON files (surf onchain-sql input format)\")\n p.add_argument(\n \"--max-workers\", type=int, default=8,\n help=\"Max concurrent surf calls (default 8; surf throttles above this)\",\n )\n p.add_argument(\n \"--out-suffix\", default=\".out.json\",\n help=\"Suffix for output files. Default: .out.json (so q1.json -> q1.out.json)\",\n )\n p.add_argument(\n \"--quiet\", action=\"store_true\",\n help=\"Suppress per-query timing summary on stderr.\",\n )\n args = p.parse_args()\n\n # Validate inputs exist before launching anything.\n missing = [p for p in args.inputs if not Path(p).is_file()]\n if missing:\n print(f\"REFUSED: input files not found: {missing}\", file=sys.stderr)\n return 1\n\n t0 = time.perf_counter()\n results, timings = run_parallel(args.inputs, max_workers=args.max_workers)\n total = time.perf_counter() - t0\n\n # Write each result to \u003cinput>.out.json\n for in_path, resp in results.items():\n out_path = Path(in_path).with_suffix(args.out_suffix) if args.out_suffix.startswith(\".\") else Path(in_path).parent / (Path(in_path).stem + args.out_suffix)\n # If suffix doesn't start with `.`, treat as append.\n if not args.out_suffix.startswith(\".\"):\n out_path = Path(in_path).with_name(Path(in_path).stem + args.out_suffix)\n out_path.write_text(json.dumps(resp, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n\n if not args.quiet:\n # v0.5.9 (adversarial review Win11 feedback): print success summary to STDOUT, not\n # stderr. PowerShell renders stderr as red NativeCommandError record\n # even when exit code is 0, which looked like a failure to operators.\n # Stdout is the right channel for success; only ACTUAL errors go to\n # stderr.\n print(f\"OK: {len(results)} queries done in {total:.1f}s \"\n f\"(max worker concurrency = {args.max_workers})\")\n for path, secs in sorted(timings.items(), key=lambda x: -x[1]):\n err = results[path].get(\"error\")\n tag = \"ERR\" if err else \"OK \"\n credits = results[path].get(\"meta\", {}).get(\"credits_used\", \"?\")\n line = f\" [{tag}] {secs:5.1f}s credits={credits} {path}\"\n if err:\n print(line, file=sys.stderr)\n else:\n print(line)\n\n # Exit non-zero if ANY query failed.\n any_err = any(\"error\" in r for r in results.values())\n return 1 if any_err else 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19370,"content_sha256":"6e78ac3aa930e44961d0a1f91a6c0a4d1584acca699ba8f92ef9d8a2a12e57e8"},{"filename":"alpha/v06/helpers/pre_launch_insider_index.py","content":"#!/usr/bin/env python3\n\"\"\"pre_launch_insider_index.py — Append-only m6 reverse map across all\ntokens this user has run forensic reports on.\n\nv0.7 cross-sym foundation. Every time forensic_pipeline.py completes\nrule_11_backward_trace for a token, the pipeline appends that token's\nm6_rows (pre-launch deployer outflow receivers) into this index. Over\ntime, builds a universal \"addr → list of (token, dumped_pct,\nreceived_amount)\" map.\n\nThis index is **zero additional cost per report** — it piggy-backs on\ndata the pipeline already computes. Lookup is O(1) for \"was this wallet\na pre-launch insider in any other token I've reported on?\"\n\nThis is the KOL_MANAGER detection backbone: if a wallet shows up as a\npre-launch receiver in ≥2 different tokens, it's almost certainly\neither a KOL manager / OTC desk / advisor allocation.\n\nDesign constraints:\n - User-local: `~/.binance-alpha-data/pre_launch_insider_index.json`\n - Append-only: idempotent re-runs don't duplicate entries\n - Atomic write (uuid-suffixed tmp + os.replace, same pattern as cross_sym_registry)\n - Cross-process safe (lock + unique tmp)\n - Schema validation on load (reject malformed cache)\n - NO surf calls — pure local data accumulation\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport sys\nimport time\nimport uuid\nfrom pathlib import Path\nfrom typing import Any\n\n# ----- Module constants -----\n\nDEFAULT_INDEX_DIR = Path.home() / \".binance-alpha-data\"\nDEFAULT_INDEX_PATH = DEFAULT_INDEX_DIR / \"pre_launch_insider_index.json\"\nDEFAULT_LOCK_PATH = DEFAULT_INDEX_DIR / \"pre_launch_insider_index.lock\"\nDEFAULT_LOCK_TIMEOUT_SECS = 30\nDEFAULT_LOCK_STALE_SECS = 300\nDEFAULT_SCHEMA_VERSION = \"0.7.0\"\n\n\nclass IndexError_(Exception):\n \"\"\"Raised when the index cannot be loaded or updated.\"\"\"\n\n\n# ----- Public API -----\n\ndef append_from_report(\n ca: str,\n sym: str,\n m6_rows: list[dict],\n *,\n index_path: Path = DEFAULT_INDEX_PATH,\n lock_path: Path = DEFAULT_LOCK_PATH,\n) -> dict[str, int]:\n \"\"\"Append a token's m6_rows (Rule 11 pre-launch receivers) into the index.\n\n Idempotent: re-running on the same (ca, addr) pair overwrites the\n entry rather than duplicating. This means re-running a report on the\n same token updates the dumped_pct / current_balance fields to latest.\n\n Args:\n ca: contract address (lowercase 0x-prefixed)\n sym: token symbol (e.g. \"TOKEN\")\n m6_rows: list of {addr, dumped_pct, received_from_deployer, current_balance, ...}\n\n Returns:\n {\"n_new_addrs\": int, \"n_updated_addrs\": int, \"total_addrs_indexed\": int}\n \"\"\"\n ca_lower = ca.lower()\n if not (ca_lower.startswith(\"0x\") and len(ca_lower) == 42):\n raise IndexError_(f\"invalid ca: {ca!r}\")\n\n # Acquire cross-process lock to avoid concurrent writers clobbering\n lock_owned = _acquire_lock(lock_path)\n try:\n if not lock_owned:\n raise IndexError_(\n \"could not acquire pre_launch_insider_index lock (another process holding too long)\"\n )\n\n existing = _load_index(index_path) or {\n \"_schema_version\": DEFAULT_SCHEMA_VERSION,\n \"snapshot_ts\": int(time.time()),\n \"tokens\": {},\n \"reverse_index\": {},\n }\n\n # Compute delta vs previous record for this CA (idempotent overwrite)\n existing_for_ca = existing[\"tokens\"].get(ca_lower, {}).get(\"addrs\", {})\n new_addrs = {}\n for r in m6_rows:\n addr = (r.get(\"addr\") or \"\").lower()\n if not (addr.startswith(\"0x\") and len(addr) == 42):\n continue\n new_addrs[addr] = {\n \"dumped_pct\": float(r.get(\"dumped_pct\", 0)),\n \"received_from_deployer\": float(r.get(\"received_from_deployer\", 0)),\n \"current_balance\": float(r.get(\"current_balance\") or 0),\n }\n\n n_new = sum(1 for a in new_addrs if a not in existing_for_ca)\n n_updated = sum(1 for a in new_addrs if a in existing_for_ca)\n\n # Update tokens map\n existing[\"tokens\"][ca_lower] = {\n \"sym\": sym.upper(),\n \"ca\": ca_lower,\n \"last_updated_ts\": int(time.time()),\n \"addrs\": new_addrs,\n }\n\n # Rebuild reverse_index from scratch each time (idempotent + simple).\n # Cost: O(N_tokens * avg_addrs_per_token), trivially fast for any\n # realistic N. The alternative (incremental update) is error-prone\n # because removing a previously-indexed token's entries from the\n # reverse_index requires careful bookkeeping.\n reverse: dict[str, list[dict]] = {}\n for ca2, tok_data in existing[\"tokens\"].items():\n sym2 = tok_data.get(\"sym\", \"\")\n for addr2, info in tok_data.get(\"addrs\", {}).items():\n reverse.setdefault(addr2, []).append({\n \"sym\": sym2,\n \"ca\": ca2,\n \"dumped_pct\": info.get(\"dumped_pct\", 0),\n \"received_from_deployer\": info.get(\"received_from_deployer\", 0),\n \"current_balance\": info.get(\"current_balance\", 0),\n })\n\n # Sort each addr's appearances by (received_from_deployer DESC,\n # dumped_pct DESC) for stable ordering even with equal allocations.\n # adversarial review P1b audit tie-break recommendation.\n for addr in reverse:\n reverse[addr].sort(\n key=lambda e: (e[\"received_from_deployer\"], e[\"dumped_pct\"]),\n reverse=True,\n )\n\n existing[\"reverse_index\"] = reverse\n existing[\"snapshot_ts\"] = int(time.time())\n\n _save_index(index_path, existing)\n\n return {\n \"n_new_addrs\": n_new,\n \"n_updated_addrs\": n_updated,\n \"total_addrs_indexed\": len(reverse),\n }\n finally:\n if lock_owned:\n _release_lock(lock_path)\n\n\ndef lookup(\n addr: str,\n *,\n index_path: Path = DEFAULT_INDEX_PATH,\n) -> list[dict]:\n \"\"\"O(1) reverse lookup. Returns list of token entries where this\n address appears as a pre-launch receiver.\n\n Each entry: {sym, ca, dumped_pct, received_from_deployer, current_balance}\n Returns [] if not in any indexed token.\n\n This is a hot path during forensic reports — cache the index in\n caller if doing many lookups.\n \"\"\"\n idx = _load_index(index_path)\n if not idx:\n return []\n return list(idx.get(\"reverse_index\", {}).get(addr.lower(), []))\n\n\ndef load_full_index(\n *,\n index_path: Path = DEFAULT_INDEX_PATH,\n) -> dict[str, Any]:\n \"\"\"Load the full index. Used by callers doing many lookups (batch\n classification) — avoid re-reading the file per lookup.\"\"\"\n return _load_index(index_path) or {\n \"_schema_version\": DEFAULT_SCHEMA_VERSION,\n \"snapshot_ts\": 0,\n \"tokens\": {},\n \"reverse_index\": {},\n }\n\n\n# ----- Internals -----\n\ndef _load_index(path: Path) -> dict | None:\n \"\"\"Load index with schema validation. Returns None on any failure.\"\"\"\n if not path.exists():\n return None\n try:\n data = json.loads(path.read_text(encoding=\"utf-8\"))\n except (json.JSONDecodeError, OSError):\n return None\n if not isinstance(data, dict):\n return None\n if not isinstance(data.get(\"tokens\"), dict):\n return None\n if not isinstance(data.get(\"reverse_index\"), dict):\n return None\n return data\n\n\ndef _save_index(path: Path, data: dict) -> None:\n \"\"\"Atomic write with uuid-suffixed temp (no concurrent-writer clobber).\"\"\"\n path.parent.mkdir(parents=True, exist_ok=True)\n tmp = path.with_suffix(f\"{path.suffix}.{uuid.uuid4().hex[:8]}.tmp\")\n try:\n tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n os.replace(tmp, path)\n finally:\n if tmp.exists():\n try:\n tmp.unlink()\n except OSError:\n pass\n\n\ndef _acquire_lock(lock_path: Path) -> bool:\n \"\"\"Same primitive as cross_sym_registry — O_EXCL create, stale-clean.\"\"\"\n lock_path.parent.mkdir(parents=True, exist_ok=True)\n deadline = time.time() + DEFAULT_LOCK_TIMEOUT_SECS\n payload = f\"{os.getpid()}:{int(time.time())}\\n\".encode()\n while True:\n try:\n fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)\n os.write(fd, payload)\n os.close(fd)\n return True\n except FileExistsError:\n pass\n try:\n mtime = lock_path.stat().st_mtime\n if time.time() - mtime > DEFAULT_LOCK_STALE_SECS:\n try:\n lock_path.unlink()\n except OSError:\n pass\n continue\n except OSError:\n pass\n if time.time() >= deadline:\n return False\n time.sleep(0.5)\n\n\ndef _release_lock(lock_path: Path) -> None:\n try:\n lock_path.unlink()\n except OSError:\n pass\n\n\n# ----- CLI -----\n\nif __name__ == \"__main__\":\n import argparse\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n sub = ap.add_subparsers(dest=\"cmd\", required=True)\n p_lookup = sub.add_parser(\"lookup\", help=\"Look up an address\")\n p_lookup.add_argument(\"addr\")\n p_stats = sub.add_parser(\"stats\", help=\"Show index statistics\")\n p_dump = sub.add_parser(\"dump\", help=\"Dump full index as JSON\")\n args = ap.parse_args()\n\n if args.cmd == \"lookup\":\n hits = lookup(args.addr)\n print(json.dumps({\n \"address\": args.addr.lower(),\n \"n_hits\": len(hits),\n \"tokens\": hits,\n }, ensure_ascii=False, indent=2))\n elif args.cmd == \"stats\":\n idx = load_full_index()\n print(json.dumps({\n \"snapshot_ts\": idx.get(\"snapshot_ts\"),\n \"n_tokens_indexed\": len(idx.get(\"tokens\", {})),\n \"n_unique_addrs\": len(idx.get(\"reverse_index\", {})),\n \"tokens\": [\n {\"sym\": t.get(\"sym\"), \"ca\": t.get(\"ca\"), \"n_addrs\": len(t.get(\"addrs\", {}))}\n for t in idx.get(\"tokens\", {}).values()\n ],\n }, indent=2))\n elif args.cmd == \"dump\":\n idx = load_full_index()\n print(json.dumps(idx, ensure_ascii=False, indent=2))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10285,"content_sha256":"864428950bc70cac9734e2fe9e85fa6fb892dbc642850dd51cb3100de636df38"},{"filename":"alpha/v06/helpers/primary_sale_attribution.py","content":"\"\"\"primary_sale_attribution.py — v1.2.0 (CAP 2026-06-27).\n\nIdentify a token's PRIMARY-SALE distribution pools (CCA / IDO / launchpad /\nairdrop redemption) on-chain, and attribute how much of each pool was captured\nby insider / operator-related wallets vs dispersed to genuine public.\n\nWhy this exists (CAP)\n=====================\nCAP ran a Uniswap \"Continuous Clearing Auction\" (CCA, 4.5% of supply) + a\nPancakeSwap IDO. An independent analyst (@ShillSeals) flagged on-chain that\n\"~50-55% of the 4% CCA pool was taken by a small number of associated wallets\".\nThe single-chain / chip-bucket report shows the AGGREGATE result (operator vs\nnon-operator) but never the EVENT-level attribution. This helper reconstructs it\npurely on-chain so the report prints \"CCA pool 166M (X% of circulating), top-5\nwallets 52% — insider-concentrated\" without a human digging through social.\n\nVerified on CAP: the distribution contract 0x9999b7e3… (vanity, operator-funded\nby the project Safe) spread 166M to 319 wallets with top-5 = 52.1% — exactly\nreproducing the @ShillSeals finding.\n\nMechanism (on-chain core — 100% automatic, no user input)\n=========================================================\n1. DETECT distribution pools: addresses that SENT the token to >= MIN_RECIPIENTS\n distinct wallets in the listing window, are CONTRACTS, are operator-FUNDED\n (received from the operator/genesis/Safe seed), and are NOT a labelled DEX\n pool/router (those are secondary trading, not a primary sale).\n2. ATTRIBUTE per pool: tokens distributed (% of circulating), recipient count,\n top-N concentration (a fair public sale is dispersed; high top-N% = insider /\n whitelist capture), and overlap with the operator set.\n\nSocial enrichment (best-effort — NAMES the pool, may be fuzzy)\n=============================================================\n`enrich_with_social` runs a surf social-post search for the token's public-sale\nevents (\"{symbol} CCA / IDO / auction / launchpad\") to LABEL a detected pool as\n\"Uniswap CCA\" / \"PancakeSwap IDO\" etc. and surface any announced allocation %.\nNaming can mis-fire on common tickers — it is advisory, flagged, and never\nchanges the on-chain numbers.\n\nCost: gated. The single DETECT query is cheap; per-pool analysis (1 recipient\nquery + 1-2 label chunks) only runs for the 1-3 pools found. Tokens with no\nhigh-fan-out operator-funded contract → 1 query, ~0 added cost.\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport re as _re\nimport sys\nfrom datetime import date, datetime, timedelta, timezone\nfrom pathlib import Path\nfrom typing import Any\n\n_THIS_DIR = Path(__file__).parent\nif str(_THIS_DIR) not in sys.path:\n sys.path.insert(0, str(_THIS_DIR))\n\n# tunables (pre-registered)\nMIN_RECIPIENTS = 20 # fan-out floor to count as a distribution pool\nMIN_POOL_PCT_CIRC = 1.0 # ignore pools distributing \u003c 1% of circulating\nTOP_LABEL_K = 40 # resolve labels for the top-K recipients per pool\nMAX_POOLS = 5 # analyse at most the top-N fan-out pools\n# Product gate (product spec): primary-sale attribution is only meaningful for a\n# FRESH listing — once the TGE is more than a month old the CCA/IDO chips have\n# dispersed/been sold and the analysis adds no forensic value. Skip entirely for\n# older listings. (Also keeps every query inside surf's lookback by construction.)\nMAX_TGE_AGE_DAYS = 30\n# Primary-sale TGE window: a CCA/IDO/launchpad distributes around listing. Scan\n# [listing − PRE_DAYS, listing + POST_DAYS] so later staking/vesting/claim emissions\n# (which fan out over months) are NOT mislabeled a primary sale. (adversarial review HIGH: the\n# old `>= date_floor … present` scan caught ongoing emissions.)\nWINDOW_PRE_DAYS = 30\nWINDOW_POST_DAYS = 60\n\n_TRANSFERS_TABLE = {\n \"ethereum\": \"agent.ethereum_transfers\", \"bsc\": \"agent.bsc_transfers\",\n \"base\": \"agent.base_transfers\", \"arbitrum\": \"agent.arbitrum_transfers\",\n \"polygon\": \"agent.polygon_transfers\", \"optimism\": \"agent.optimism_transfers\",\n}\n# Infra labels that are NOT a primary sale — DEX/router/bridge (secondary trading)\n# PLUS staking/vesting/timelock/claim/rewards/distributor/custody/vault/treasury\n# (ongoing emissions or custody, not a public sale). adversarial review HIGH: a treasury-funded\n# vesting/staking-rewards contract with fan-out fit every other detector condition.\n_NON_SALE_INFRA_RE = _re.compile(\n r\"uniswap|pancake|pool\\s*manager|poolmanager|\\brouter\\b|aggregator|\\bv[234]\\b|\"\n r\"\\bamm\\b|liquidity|bridge|layerzero|lz\\s*multicall|settler|permit2|stargate|\"\n r\"multicall|1inch|paraswap|0x\\s*protocol|cow\\s*protocol|\"\n r\"stak|vest|timelock|time\\s*lock|\\bclaim\\b|reward|distributor|custod|\\bvault\\b|\"\n r\"treasury|escrow|gauge|locker|merkle\", _re.IGNORECASE)\n# Sale-venue override (adversarial review MED): a label like \"Uniswap CCA\" / \"PancakeSwap IDO\" /\n# \"… auction\" / \"launchpad\" is the EXACT pool we want — never let the infra regex\n# (which matches \"uniswap\"/\"pancake\") drop it. A sale term in the label wins.\n_SALE_VENUE_RE = _re.compile(\n r\"\\bcca\\b|continuous\\s+clearing|\\bido\\b|\\bipo\\b|public\\s+sale|token\\s+sale|\"\n r\"token\\s+auction|\\bauction\\b|launchpad|launch\\s*pad|fair\\s+launch|\\blbp\\b|\"\n r\"bootstrap|\\btge\\b|presale|pre-sale\", _re.IGNORECASE)\n\n_ADDR_RE = _re.compile(r\"^0x[0-9a-f]{40}$\")\n_DATE_RE = _re.compile(r\"^(\\d{4}-\\d{2}-\\d{2})(?:[T ].*)?$\")\n\n\ndef _safe_addr(a: str) -> str | None:\n \"\"\"Return a lowercased EVM address only if it is a strict 0x+40-hex string,\n else None. Hardens every SQL interpolation against malformed/injected input\n (adversarial review MED — this helper is exported, no upstream guarantee).\"\"\"\n if not a:\n return None\n a = a.strip().lower()\n return a if _ADDR_RE.match(a) else None\n\n\ndef _safe_date(d: str | None) -> str | None:\n \"\"\"Return the YYYY-MM-DD prefix of a date/ISO-datetime string, or None. Strict:\n rejects trailing garbage (adversarial review MED — old code truncated to 10 chars first, so\n \"2026-01-01garbage\" silently passed). Accepts \"2026-01-01\" and\n \"2026-01-01T00:00:00Z\"; rejects anything else.\"\"\"\n if not d:\n return None\n m = _DATE_RE.match(str(d).strip())\n if not m:\n return None\n try: # reject calendar-invalid dates (e.g. 2026-13-99) — shape alone is not enough\n datetime.strptime(m.group(1), \"%Y-%m-%d\")\n except ValueError:\n return None\n return m.group(1)\n\n\ndef _window_bounds(listing_date: str | None, date_floor: str) -> tuple[str, str | None]:\n \"\"\"(lo, hi) date bounds for the primary-sale scan. With a listing date, clamp to\n [max(date_floor, listing−PRE), listing+POST]; without one, fall back to\n (date_floor, None) = `>= date_floor` (legacy, flagged in the result).\"\"\"\n df = _safe_date(date_floor) or \"2020-01-01\"\n ld = _safe_date(listing_date)\n if not ld:\n return df, None\n base = datetime.strptime(ld, \"%Y-%m-%d\").replace(tzinfo=timezone.utc)\n lo = (base - timedelta(days=WINDOW_PRE_DAYS)).strftime(\"%Y-%m-%d\")\n hi = (base + timedelta(days=WINDOW_POST_DAYS)).strftime(\"%Y-%m-%d\")\n if df > lo:\n lo = df\n return lo, hi\n\n\n# A KOL / ENS / social-named recipient is a HUMAN-identifiable wallet (ENS name,\n# X/Polymarket handle, or a named person/entity) — NOT exchange/DEX infra. A large\n# primary-sale allocation to such named wallets is the whitelist / associated-party\n# signal the report must surface in full.\n# surf_labels_probe classification enum: CEX_DEPOSIT / CEX_HOT_WALLET / DEX_POOL /\n# BRIDGE / MARKET_MAKER / OTHER_NAMED / UNLABELED. Exclude all infra/entity classes\n# from the KOL highlight — only a named human/KOL (ENS / handle / OTHER_NAMED) counts.\n_KOL_INFRA_CLS = {\"CEX_HOT_WALLET\", \"CEX_DEPOSIT\", \"DEX_POOL\", \"BRIDGE\", \"MARKET_MAKER\"}\n_KOL_INFRA_RE = _re.compile(\n r\"uniswap|pancake|pool|router|exchange|deposit|hot\\s*wallet|cold\\s*wallet|\"\n r\"vault|bridge|binance|coinbase|okx|bybit|kucoin|gate|mexc|kraken|bitvavo|\"\n r\"layerzero|multicall|settler|safe\\s*proxy|gnosis\", _re.IGNORECASE)\n_KOL_NAME_RE = _re.compile(r\"\\.eth\\b|@\\w|polymarket|\\bon\\s+x\\b|farcaster|lens\\b\", _re.IGNORECASE)\n\n\ndef _named_kind(info: dict, label: str | None) -> str | None:\n \"\"\"Classify a named recipient (adversarial review MED — don't call a foundation/protocol a\n \"KOL\"): returns 'kol' for an ENS / social-handle wallet (high-confidence human\n KOL), 'entity' for an OTHER_NAMED label without ENS/social (could be a\n foundation/protocol/custodian — a named entity, not necessarily a KOL), or None\n for exchange/DEX/MM/bridge infra and unlabeled wallets.\"\"\"\n if not label:\n return None\n if (info or {}).get(\"classification\") in _KOL_INFRA_CLS:\n return None\n if _KOL_INFRA_RE.search(label):\n return None\n # ENS / social handle / platform reference → high-confidence KOL.\n if _KOL_NAME_RE.search(label):\n return \"kol\"\n # OTHER_NAMED with a real entity_name but no ENS/social → a named entity.\n if (info or {}).get(\"classification\") == \"OTHER_NAMED\" and (info or {}).get(\"entity_name\"):\n return \"entity\"\n return None\n\n\ndef _run_sql(sql: str, max_rows: int = 1000):\n from section_a_scope import _run_surf_with_retry\n try:\n doc, _err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=json.dumps({\"sql\": sql, \"max_rows\": max_rows}),\n base_timeout=60, max_attempts=3,\n )\n return (doc or {}).get(\"data\") or []\n except Exception as e: # noqa: BLE001\n print(f\"[primary_sale] sql failed (non-fatal): {str(e)[:120]}\", file=sys.stderr)\n return []\n\n\ndef detect_primary_sale_pools(\n *, prefix: str, supply_ca: str, operator_seed: set[str], operator_set: set[str],\n total_supply: float, circulating_supply: float, date_floor: str,\n is_contract_fn, code_cache: dict[str, bool], listing_date: str | None = None,\n) -> list[dict[str, Any]]:\n \"\"\"On-chain detection + attribution of primary-sale distribution pools.\n\n Must be called inside `chain_lock(prefix)` (caller holds it). `operator_seed`\n = the treasury/genesis/Safe FUNDING sources (the funding gate uses ONLY this).\n `operator_set` = the full insider wallet set (used for recipient overlap only).\n `is_contract_fn(prefix, addr, cache)` = the shared eth_getCode contract test.\n `listing_date` (YYYY-MM-DD) constrains the scan to the TGE window.\n\n Returns a list of pool dicts (largest first), each:\n {pool_addr, label, tokens_distributed, pct_of_circulating, n_recipients,\n top5_pct, top10_pct, operator_overlap_pct, operator_funded, is_dex_infra,\n insider_concentration_pct, window_lo, window_hi, named_pct_is_lower_bound,\n rows[]}\n \"\"\"\n table = _TRANSFERS_TABLE.get(prefix)\n supply_ca = _safe_addr(supply_ca)\n if not table or not supply_ca:\n return []\n circ = circulating_supply or 0\n if circ \u003c= 0:\n return []\n operator_seed = operator_seed or set() # tolerate None from external callers\n operator_set = operator_set or set()\n # FRESH-listing gate (FAIL CLOSED): require a valid listing date, and skip if\n # the TGE is more than MAX_TGE_AGE_DAYS old. Without a verifiable listing date we\n # cannot prove freshness — and the window would fall back to an unbounded\n # `>= date_floor` scan that catches later rewards/claims emissions — so we bail\n # rather than risk a stale false positive (adversarial review HIGH / product spec gate).\n _ld_gate = _safe_date(listing_date)\n if not _ld_gate:\n return []\n _age = (date.today() - datetime.strptime(_ld_gate, \"%Y-%m-%d\").date()).days\n if _age > MAX_TGE_AGE_DAYS:\n return []\n # v1.2.0: decimals factor from the ACTIVE chain (set by the caller's\n # chain_lock / set_token_decimals), never a hardcoded 1e18 — non-18-decimal\n # supply tokens (USDC=6, WBTC=8) would otherwise be off by orders of magnitude\n # (matches the codebase-wide v0.9.7 decimals_factor migration).\n try:\n from chain_router import decimals_factor_str\n _df = decimals_factor_str()\n except Exception:\n _df = \"1e18\"\n # TGE-window clause for DISTRIBUTION (outflows): don't catch later\n # staking/vesting emissions (adversarial review HIGH).\n _lo, _hi = _window_bounds(listing_date, date_floor)\n # FUNDING-provenance lower bound (inflows): wider than the TGE window (pre-sale\n # prefund) but capped at the TGE upper bound (adversarial review MED: funding arriving months\n # AFTER the TGE distribution is not prefund — don't promote it).\n _fund_lo = _safe_date(date_floor) or \"2020-01-01\"\n # CLAMP both lower bounds to surf's large-table lookback (~365d): querying\n # block_date earlier than this returns EMPTY on agent.{chain}_transfers, silently\n # dropping every pool. The full-pipeline date_floor (= listing−365d) sits right at\n # that edge — the cause of the \"0 pools in full run\" bug (standalone used a later\n # floor so never hit it).\n try:\n from surf_constraints import surf_earliest_date_floor\n _earliest = surf_earliest_date_floor()\n except Exception:\n _earliest = None\n # Cap the upper bound at today: there is no future on-chain data, and a fresh\n # listing's `listing+60d` is in the future — leaving it would make the funding\n # span (earliest .. listing+60d) exceed surf's ~364-day large-table window and\n # return EMPTY. Capping keeps span = [today−364, today] ≤ 364 (the real bug).\n _today = date.today().isoformat()\n if _hi and _hi > _today:\n _hi = _today\n if _earliest:\n if _lo \u003c _earliest:\n _lo = _earliest\n if _fund_lo \u003c _earliest:\n _fund_lo = _earliest\n # adversarial review HIGH: a token listed >~424d ago has its whole TGE window below surf's\n # lookback, so `_hi` (= listing+60d, in the past) ends up BEFORE the clamped\n # lower bound → an inverted BETWEEN that silently returns empty. Bail explicitly\n # rather than emit a misleading \"no primary sale\" — the sale predates queryable\n # surf data, so the section is honestly omitted.\n if _hi and (_hi \u003c _lo or _hi \u003c _fund_lo):\n print(f\"[primary_sale] TGE window [{_lo}..{_hi}] is below surf lookback \"\n f\"({_earliest}); primary sale predates queryable data — skipping.\",\n file=sys.stderr)\n return []\n _win = (f\"AND block_date BETWEEN '{_lo}' AND '{_hi}' \" if _hi\n else f\"AND block_date >= '{_lo}' \")\n _fund_win = (f\"AND block_date BETWEEN '{_fund_lo}' AND '{_hi}' \" if _hi\n else f\"AND block_date >= '{_fund_lo}' \")\n\n # 1. DETECT high-fan-out senders (candidate distribution pools). Rank by\n # tokens distributed (materiality), not recipient count — a smaller-fan-out\n # but high-value sale pool must not be crowded out by tiny dust senders.\n sql = (\n 'SELECT lower(\"from\") AS src, count(DISTINCT lower(\"to\")) AS n_recip, '\n f'sum(toFloat64(toDecimal256(amount_raw,0))/{_df}) AS amt, '\n 'min(block_date) AS first_out '\n f\"FROM {table} WHERE contract_address='{supply_ca}' {_win}\"\n f\"GROUP BY src HAVING n_recip >= {MIN_RECIPIENTS} \"\n f\"ORDER BY amt DESC LIMIT 40\"\n )\n cands = _run_sql(sql, max_rows=50)\n if not cands:\n return []\n\n BURN = {\"0x0000000000000000000000000000000000000000\",\n \"0x000000000000000000000000000000000000dead\"}\n try:\n from surf_labels_probe import resolve_labels\n except Exception:\n resolve_labels = lambda a: {} # noqa: E731\n\n # Pre-filter: contracts that are NOT infra (DEX/router/bridge/staking/vesting/\n # claim/rewards/custody/vault), distributing a material share of circulating.\n src_labels = resolve_labels([(c.get(\"src\") or \"\").lower() for c in cands]) or {}\n pools_raw: list[dict[str, Any]] = []\n for c in cands:\n src = _safe_addr(c.get(\"src\") or \"\")\n if not src or src in BURN:\n continue\n amt = float(c.get(\"amt\") or 0)\n pct_circ = 100.0 * amt / circ\n if pct_circ \u003c MIN_POOL_PCT_CIRC:\n continue\n info = src_labels.get(src) or {}\n label = \" | \".join(x for x in [info.get(\"entity_name\"), info.get(\"label\")] if x)\n # infra unless the label itself names a sale venue (then it's the pool we want)\n is_dex = bool(_NON_SALE_INFRA_RE.search(label)) and not _SALE_VENUE_RE.search(label)\n is_c = is_contract_fn(prefix, src, code_cache)\n pools_raw.append({\n \"pool_addr\": src, \"label\": label or None, \"tokens_distributed\": amt,\n \"pct_of_circulating\": round(pct_circ, 1),\n \"n_recipients\": int(c.get(\"n_recip\") or 0),\n \"first_out\": c.get(\"first_out\"), # earliest distribution date (ordering)\n \"is_contract\": is_c, \"is_dex_infra\": is_dex,\n })\n\n # OPERATOR-FUNDED check (true semantics): a primary-sale pool is FUNDED by the\n # operator TREASURY *before* it distributes. Verify with one batched query — who\n # funded each candidate and when — and require a material inflow (>= min_funding)\n # FROM operator_seed ONLY (genesis/reserve/multisig/deployer treasury) whose first\n # inflow is AT OR BEFORE the pool's first distribution outflow. adversarial review HIGH:\n # (a) operator_seed | operator_set wrongly promoted cluster-funded pools — now\n # seed-only; (b) accepting any same-window inflow let a contract fan out FIRST then\n # receive treasury funds LATER and still be called a sale — now ordering-gated.\n cand_pools = [p for p in pools_raw if p[\"is_contract\"] and not p[\"is_dex_infra\"]]\n # group inflow by (pool, funder, block_date) so the ordering gate can sum ONLY the\n # tranches that arrived AT OR BEFORE the pool's first distribution outflow. adversarial review\n # HIGH: a min(block_date) gate over the whole funder sum let dust-before + material-\n # after still pass — summing per-date and filtering by block_date closes that.\n funders_by_pool: dict[str, list[tuple[str, float, Any]]] = {}\n safe_in = [p[\"pool_addr\"] for p in cand_pools if _safe_addr(p[\"pool_addr\"])]\n seed = {a for a in (_safe_addr(x) for x in operator_seed) if a}\n # Push the operator-seed funder filter INTO the SQL (adversarial review MED): without it the\n # query returns every funder of every candidate, which can exceed max_rows and\n # truncate before the seed rows arrive — suppressing a real pool. Filtering to\n # seed funders keeps the result tiny and the ordering sum exact.\n if safe_in and seed:\n in_list = \",\".join(\"'%s'\" % a for a in safe_in)\n seed_list = \",\".join(\"'%s'\" % a for a in sorted(seed))\n frows = _run_sql(\n 'SELECT lower(\"to\") AS pool, lower(\"from\") AS funder, block_date AS bd, '\n f'sum(toFloat64(toDecimal256(amount_raw,0))/{_df}) AS amt '\n f\"FROM {table} WHERE contract_address='{supply_ca}' \"\n f'AND lower(\"to\") IN ({in_list}) AND lower(\"from\") IN ({seed_list}) {_fund_win}'\n \"GROUP BY pool, funder, bd\",\n max_rows=4000,\n )\n for r in frows:\n funders_by_pool.setdefault((r.get(\"pool\") or \"\").lower(), []).append(\n ((r.get(\"funder\") or \"\").lower(), float(r.get(\"amt\") or 0), r.get(\"bd\")))\n min_funding = max(0.001 * (total_supply or 0), 1_000_000) # material inflow floor\n sale_pools = []\n for p in cand_pools:\n funders = funders_by_pool.get(p[\"pool_addr\"], [])\n _fout = p.get(\"first_out\")\n # sum operator-seed inflow that landed at/before the first distribution outflow\n # (block_date granularity; same-day counts as before/at). A row dated after the\n # first outflow does NOT prove the pool was operator-funded *to* distribute.\n op_inflow = sum(v for f, v, bd in funders\n if f in seed and (not _fout or (bd is not None and bd \u003c= _fout)))\n p[\"operator_funded\"] = bool(op_inflow >= min_funding)\n p[\"operator_inflow_tokens\"] = round(op_inflow, 0)\n if p[\"operator_funded\"]:\n sale_pools.append(p)\n # tokens DESC, then pool_addr ASC for a deterministic order on exact ties\n sale_pools.sort(key=lambda p: (-p[\"tokens_distributed\"], p[\"pool_addr\"]))\n sale_pools = sale_pools[:MAX_POOLS]\n\n # 2. ATTRIBUTE each pool: recipient concentration + operator overlap.\n for p in sale_pools:\n rows = _run_sql(\n 'SELECT lower(\"to\") AS r, '\n f'sum(toFloat64(toDecimal256(amount_raw,0))/{_df}) AS amt '\n f\"FROM {table} WHERE contract_address='{supply_ca}' \"\n f\"AND lower(\\\"from\\\")='{p['pool_addr']}' {_win}\"\n \"GROUP BY r ORDER BY amt DESC LIMIT 1000\",\n max_rows=1000,\n )\n recips = [((r.get(\"r\") or \"\").lower(), float(r.get(\"amt\") or 0))\n for r in rows if r.get(\"r\")]\n recips = [(a, v) for a, v in recips if a not in BURN and v > 0]\n recips.sort(key=lambda x: x[1], reverse=True)\n # adversarial review HIGH: denominator = the UNCAPPED pool outflow (tokens_distributed\n # from the detect aggregate), NOT sum(top-1000). Using the capped sum would\n # inflate concentration for a sale dispersed across >1000 wallets.\n pool_total = p[\"tokens_distributed\"] or (sum(v for _a, v in recips) or 1.0)\n top5 = sum(v for _a, v in recips[:5])\n top10 = sum(v for _a, v in recips[:10])\n op_overlap = sum(v for a, v in recips if a in operator_set or a in operator_seed)\n # label the top-K recipients (cheap; the dispersed tail is genuine public)\n rlabels = resolve_labels([a for a, _v in recips[:TOP_LABEL_K]]) or {}\n rrows = []\n named_recips = [] # ENS / social / named recipients — surfaced in full\n named_tokens = 0.0\n for a, v in recips[:TOP_LABEL_K]:\n li = rlabels.get(a) or {}\n label = \" | \".join(x for x in [li.get(\"entity_name\"), li.get(\"label\")] if x) or None\n is_op = (a in operator_set or a in operator_seed)\n kind = _named_kind(li, label) # 'kol' | 'entity' | None\n rrows.append({\n \"addr\": a, \"tokens\": v, \"pct_of_pool\": round(100.0 * v / pool_total, 1),\n \"label\": label, \"is_operator\": is_op, \"is_named\": bool(kind),\n })\n if kind:\n named_recips.append({\n \"addr\": a, \"name\": label, \"tokens\": v, \"kind\": kind,\n \"pct_of_pool\": round(100.0 * v / pool_total, 1),\n })\n named_tokens += v\n p[\"n_recipients_resolved\"] = len(recips)\n p[\"top5_pct\"] = round(100.0 * top5 / pool_total, 1)\n p[\"top10_pct\"] = round(100.0 * top10 / pool_total, 1)\n p[\"operator_overlap_pct\"] = round(100.0 * op_overlap / pool_total, 1)\n # insider concentration headline = max(top-5 concentration, operator overlap).\n # A fair public sale is dispersed; >40% in the top 5 OR direct operator\n # overlap is the insider-capture signal.\n p[\"insider_concentration_pct\"] = round(max(p[\"top5_pct\"], p[\"operator_overlap_pct\"]), 1)\n # Named (ENS/social/entity) recipients. adversarial review MED: named_pct is computed\n # over the labeled top-K only, so it is a LOWER BOUND when the pool has more\n # recipients than we resolved labels for — flag it so render can say \"≥\".\n named_recips.sort(key=lambda x: x[\"tokens\"], reverse=True)\n p[\"named_recipients\"] = named_recips\n p[\"n_named_recipients\"] = len(named_recips)\n p[\"n_kol_recipients\"] = sum(1 for r in named_recips if r[\"kind\"] == \"kol\")\n p[\"named_pct_of_pool\"] = round(100.0 * named_tokens / pool_total, 1)\n p[\"named_pct_is_lower_bound\"] = bool(len(recips) > TOP_LABEL_K)\n p[\"window_lo\"] = _lo\n p[\"window_hi\"] = _hi\n p[\"rows\"] = rrows\n # drop bulky internals\n for k in (\"is_contract\",):\n p.pop(k, None)\n return sale_pools\n\n\n_SOCIAL_EVENT_RE = _re.compile(\n r\"\\b(cca|continuous\\s+clearing\\s+auction|ido|public\\s+sale|token\\s+auction|\"\n r\"launchpad|tge|fair\\s+launch|lbp|liquidity\\s+bootstrap)\\b\", _re.IGNORECASE)\n\n\ndef enrich_with_social(symbol: str, pools: list[dict[str, Any]]) -> dict[str, Any]:\n \"\"\"Best-effort: surf social-post search to NAME the detected pools (CCA / IDO\n / auction) + surface any announced allocation %. Advisory only — fuzzy on\n common tickers; never alters the on-chain numbers. Returns\n {event_hits: [...], _note}. The caller attaches it under\n primary_sales.social (clearly flagged as best-effort).\"\"\"\n if not symbol or not pools:\n return {}\n import subprocess\n # cashtag form ($CAP) — the crypto convention that filters \"capital\" /\n # \"market cap\" noise that plagues short tickers in a plain keyword search.\n cashtag = \"$\" + symbol.lstrip(\"$\")\n sym_re = _re.compile(r\"(?:\\$%s\\b|@\\w*%s\\w*)\" % (_re.escape(symbol), _re.escape(symbol)),\n _re.IGNORECASE)\n hits: list[dict[str, Any]] = []\n try:\n proc = subprocess.run(\n [\"surf\", \"search-social-posts\", \"--q\",\n f'{cashtag} (CCA OR IDO OR auction OR launchpad OR \"public sale\")',\n \"--limit\", \"15\"],\n capture_output=True, text=True, check=False, timeout=40,\n )\n doc = json.loads(proc.stdout) if proc.returncode == 0 else {}\n for t in (doc.get(\"data\") or [])[:15]:\n text = t.get(\"text\") or \"\"\n # require the ticker as a cashtag or in a handle — not a bare substring\n if not sym_re.search(text):\n continue\n m = _SOCIAL_EVENT_RE.search(text)\n if not m:\n continue\n # pull an allocation % if mentioned near the event word\n pct = None\n pm = _re.search(r\"(\\d+(?:\\.\\d+)?)\\s*%[^.]{0,40}(?:supply|allocation|pool|cca|ido)\",\n text, _re.IGNORECASE)\n if pm:\n pct = pm.group(1)\n hits.append({\n \"event\": m.group(0).lower(),\n \"announced_pct\": pct,\n \"handle\": (t.get(\"author\") or {}).get(\"handle\"),\n \"likes\": (t.get(\"stats\") or {}).get(\"likes\"),\n \"excerpt\": text[:220],\n \"url\": t.get(\"url\"),\n })\n except Exception as e: # noqa: BLE001\n print(f\"[primary_sale] social enrich failed (non-fatal): {str(e)[:120]}\",\n file=sys.stderr)\n return {\"_error\": str(e)[:120]}\n # rank by likes; keep the most-engaged few\n hits.sort(key=lambda h: (h.get(\"likes\") or 0), reverse=True)\n return {\n \"event_hits\": hits[:5],\n \"_note\": (\"best-effort surf social naming; fuzzy on common tickers, \"\n \"advisory only — on-chain pool numbers are authoritative.\"),\n }\n\n\n__all__ = [\"detect_primary_sale_pools\", \"enrich_with_social\"]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":27009,"content_sha256":"885a8d7114cea5856ebebc6bcaad75b21537938c02aa30ead07a74fd235b2f6f"},{"filename":"alpha/v06/helpers/protocol_lockup_detector.py","content":"#!/usr/bin/env python3\n\"\"\"protocol_lockup_detector.py — classify a holder as project-side /\ninfrastructure (vesting / multisig / treasury / DEX-infra / CEX-custody)\nvs. unlabeled (a genuine insider candidate).\n\nWhy this exists (v0.7.10.3):\n\n rule_11_backward_trace is a pure transfer-graph analysis — it follows\n \"who sent tokens to whom\" and labels every pre-listing receiver as an\n \"insider\". It has no identity awareness. Two failure cases this caused:\n\n 1. COLLECT (2026-05-26): largest \"quiet wallet\" `0xf27d6fc9…` (79% of\n supply, dumped_pct=0) is Arkham-labeled \"Vesting (Proxy)\" — the\n project's lockup contract, NOT an insider hoarding. 2nd quiet wallet\n `0x1ac06807…` is \"Gnosis Safe Proxy\" (multisig).\n 2. COLLECT m6 partial-dumper `0x73d8bd54…` is Arkham-labeled\n \"Binance Wallet\" (entity_type=\"misc\", NOT \"cex\") — Binance Alpha's\n omnibus custody wallet. It's #1 holder in ~105 Alpha projects at 50%+\n each because Alpha user holdings are custodied there. Its transfers\n are user buys/sells, NOT insider distribution.\n\n This module batch-queries Arkham labels and classifies each address so\n rule_11 / verdict / alloc / section_l can exclude infrastructure from\n the \"insider\" framing.\n\nConsumed by:\n - rule_11_backward_trace (enrich pre_launch_receivers)\n - _derive_verdict_enum (exclude protocol lockups from quiet_with_size)\n - section_alloc (separate \"项目方锁仓/基建\" row from \"未分内幕\")\n - section_l_distribution (PROJECT_LOCKUP role)\n\nCost: one `surf wallet-labels-batch` call per chunk of 20 addresses.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent))\n\n\n# ---- label classification (pure-Python, no I/O) ----\n\nVESTING_LABEL_RE = re.compile(\n # v0.8.2 fix: no word boundary on vendor brand names so camelcase\n # labels like `SablierLockup` / `HedgeyLock` / `StreamflowVesting` get\n # caught. Velvet test: top other `SablierLockup` 107% 流通 was missed\n # because `\\bsablier\\b` doesn't match mid-camelcase.\n r\"(\"\n r\"\\bvesting\\b|\"\n r\"\\b(lockup|locker)\\b|\"\n r\"sablier|streamflow|hedgey|vyper\\s*finance|\"\n r\"unicrypt|team\\s*finance|pinklock|dx\\s*lock\"\n r\")\",\n re.IGNORECASE,\n)\n\nMULTISIG_LABEL_RE = re.compile(\n r\"\\b(\"\n r\"gnosis\\s*safe|safe\\s*proxy|safe\\s*module|\"\n r\"multisig|multi-sig|\"\n r\"squads\"\n r\")\\b\",\n re.IGNORECASE,\n)\n\nTREASURY_LABEL_RE = re.compile(\n r\"\\b(treasury|dao\\s*treasury|foundation\\s*wallet)\\b\",\n re.IGNORECASE,\n)\n\n# DEX / bridge / DeFi router infra (third-party, not insider).\n#\n# v0.8.2 additions:\n# - PoolManager (UniV4 / PancakeV4 singleton pool manager). H token\n# top-2 holder showed `PoolManager` 254% 流通, was unclassified.\n# - Universal Router (Uniswap Universal Router contracts).\n# - V4 hooks / hook contracts.\nDEX_INFRA_LABEL_RE = re.compile(\n r\"\\b(\"\n r\"dex\\s*router|\"\n r\"pancake\\w*\\s*router|uniswap\\w*\\s*router|sushi\\w*\\s*router|\"\n r\"universal\\s*router|\"\n r\"pool\\s*manager|poolmanager|\"\n r\"lifi\\s*diamond|\"\n r\"stargate|hop\\s*protocol|across\\s*protocol|squid\\s*router|\"\n r\"v[2-4]\\s*pool|liquidity\\s*pool|\"\n r\"v[2-4]\\s*hook|hooks\\b\"\n r\")\\b\",\n re.IGNORECASE,\n)\n# A bare \"Vault\" is NOT enough to call DEX/DeFi infra — \"Team Vault\" / \"Operator\n# Vault\" are operator-controlled treasuries, not neutral DeFi. Only count a vault as\n# DEX/DeFi-neutral when a yield/DEX protocol name co-occurs (yearn/beefy/convex/...).\nDEX_VAULT_PROTOCOL_RE = re.compile(\n r\"\\b(yearn|beefy|convex|aura|tokemak|pancake\\w*|uniswap|sushi\\w*|curve|\"\n r\"balancer|thena|wombat|apeswap|trader\\s*joe|biswap|mdex|venus|alpaca|\"\n r\"autofarm|morpho|aave|compound|gmx|pendle|gamma|arrakis|ichi|reaper|\"\n r\"frax|lido|maker|stakedao|harvest|badger|idle|enzyme|sommelier)\\b\",\n re.IGNORECASE,\n)\n# VC / investment arms (\"Binance Labs\", \"Coinbase Ventures\", \"OKX Ventures\") carry an\n# exchange brand but are operator-side investors, NOT neutral CEX custody — exclude\n# from is_cex_custody. NOTE: deliberately NOT \"fund\" — an exchange \"Insurance Fund\" /\n# \"SAFU Fund\" / \"Reserve Fund\" IS real CEX custody, not a VC arm.\nVC_ARM_LABEL_RE = re.compile(\n r\"\\b(labs|ventures|capital|partners|incubat|accelerat)\\b\",\n re.IGNORECASE,\n)\n\n# CEX custody / exchange omnibus wallets. CRITICAL: Arkham labels these\n# inconsistently — \"Binance Wallet\" has entity_type=\"misc\", not \"cex\". So\n# we match on entity_name / label text containing an exchange brand, not\n# just entity_type. Covers hot/cold/deposit/withdrawal/custody variants.\n#\n# v0.8.1 brand list expansion (false-positive mitigation):\n# - Tier 1 (was already covered): top 19 majors\n# - Tier 2/3 added: smaller CEX that run launchpool / IEO / airdrop\n# programs — same shape as Tier 1, just less labeled\n# - Tier 1 launchpool sub-platforms are covered by the brand match\n# alone (CEX_CUSTODY_LABEL_RE matches `Binance: Launchpool` because\n# \"binance\" appears; trailing wallet/hot/cold keyword is optional)\n_CEX_BRANDS = (\n # Tier 1 (v0.7.x baseline)\n r\"binance|coinbase|okx|okex|bybit|bitget|gate\\.?io|gate\\s|kucoin|\"\n r\"mexc|htx|huobi|kraken|bitfinex|upbit|bithumb|crypto\\.com|\"\n r\"gemini|bitstamp|bitkub|backpack|aster|\"\n # Tier 2/3 (v0.8.1 add) — also run launchpool / IEO / airdrop programs\n r\"bingx|phemex|whitebit|pionex|probit|digifinex|lbank|bitrue|\"\n r\"coinex|bitmart|tapbit|hotbit|xt\\.?com|deepcoin|biconomy|toobit|\"\n r\"hashkey|cryptocom\"\n)\nCEX_CUSTODY_LABEL_RE = re.compile(\n r\"\\b(\" + _CEX_BRANDS + r\")\\b\"\n r\"(.*\\b(wallet|hot|cold|deposit|withdrawal|custody|reserve|exchange|launchpool|launchpad|hodler|megadrop|jumpstart|earn|cryptopedia|spotlight|kickstarter|prime)\\b)?\",\n re.IGNORECASE,\n)\n\n# v0.8.1: third-party distribution platforms (not CEX, not DEX, but\n# project-side allocation channels). False-positive mitigation for the\n# \"deployer → claim contract → retail self-claim\" pattern (Pattern B/D):\n# - DEX launchpads / IDO platforms — project mints to platform pool\n# wallet, retail buys/claims, platform wallet's residual stock is\n# not insider-controlled (it's earmarked for ongoing public sale).\n# - Task / quest platforms — same shape; project funds the campaign\n# and the platform contract holds the unclaimed remainder.\n# - Airdrop SaaS (vesting / streaming / claim distributors) — the\n# contract holds the un-claimed remainder on behalf of retail,\n# not the project-side hand. Distinct from VESTING_LABEL_RE (which\n# covers per-allocation lockups, not retail claim distributors).\n#\n# Effect: matched wallets are flagged is_airdrop_platform → excluded\n# from `pure_insider_holds_pct_supply` (potential-dump-pressure metric)\n# but KEPT in `insider_addrs` because their reclaim / admin-withdraw\n# events ARE genuine insider actions (the project still controls the\n# admin key). Conservative on both edges.\n_LAUNCHPAD_BRANDS = (\n r\"dao\\s*maker|polkastarter|seedify|trustpad|gamefi\\.?org|\"\n r\"bscpad|camelot|fjord|sushi\\s*miso|raydium\\s*acceleraytor|\"\n r\"impossible\\s*finance|red\\s*kite|trader\\s*joe\\s*launch|\"\n r\"copper\\s*launch|balancer\\s*lbp\"\n)\n_TASK_PLATFORM_BRANDS = (\n r\"galxe|layer3|taskon|questn|rabbit\\s*hole|gitcoin\\s*passport|\"\n r\"zealy|guild\\.xyz|cred\\s*protocol\"\n)\n_AIRDROP_SAAS_BRANDS = (\n r\"hedgey|sablier|superfluid|llamapay|magna|coinvise|merkle\\s*distributor|\"\n r\"merkl\\b|jiritsu|wallet3|disco\\.xyz\"\n)\n_AIRDROP_KEYWORDS = (\n # v0.8.1 audit MED #1 fix: bare `airdrop` is too broad — Arkham\n # labels like \"Airdrop Recipient\" or \"Project Airdrop Wallet\" would\n # match and exclude actual retail/insider EOAs. Require an explicit\n # distributor-shaped suffix (claim/contract/distributor/pool/campaign)\n # or pair with `merkle/launchpool/emission/allocation/reward`.\n # audit LOW #2 fix: also catch camelcase `AirdropClaim` by\n # adding `airdrop\\s*claim` explicitly and allowing optional space\n # between the airdrop prefix and the trailing keyword.\n r\"airdrop\\s*(claim|contract|distributor|pool|campaign|claim\\s*contract)|\"\n r\"merkle\\s*distributor|launchpool\\s*distributor|\"\n r\"campaign\\s*pool|emission\\s*contract|\"\n r\"allocation\\s*pool|reward\\s*distributor|\"\n r\"claim\\s*contract\"\n)\nAIRDROP_PLATFORM_LABEL_RE = re.compile(\n r\"\\b(\"\n + _LAUNCHPAD_BRANDS + r\"|\"\n + _TASK_PLATFORM_BRANDS + r\"|\"\n + _AIRDROP_SAAS_BRANDS + r\"|\"\n + _AIRDROP_KEYWORDS\n + r\")\\b\",\n re.IGNORECASE,\n)\n\n\ndef classify_protocol_lockup(\n arkham_label_text: str | None = None,\n entity_name: str | None = None,\n entity_type: str | None = None,\n raw_labels: list[dict] | None = None,\n) -> dict[str, Any]:\n \"\"\"Classify a single Arkham label response.\n\n Returns dict with is_vesting / is_multisig / is_treasury / is_dex_infra\n / is_cex_custody / is_protocol_lockup (any) / label_match / display_label.\n \"\"\"\n candidates: list[str] = []\n if arkham_label_text:\n candidates.append(str(arkham_label_text))\n if entity_name:\n candidates.append(str(entity_name))\n for lbl in (raw_labels or []):\n if isinstance(lbl, dict) and lbl.get(\"label\"):\n candidates.append(str(lbl[\"label\"]))\n text = \" \".join(candidates).strip()\n et = (entity_type or \"\").lower()\n\n is_vesting = bool(VESTING_LABEL_RE.search(text))\n is_multisig = bool(MULTISIG_LABEL_RE.search(text))\n is_treasury = bool(TREASURY_LABEL_RE.search(text))\n is_dex_infra = bool(DEX_INFRA_LABEL_RE.search(text)) or (\n \"vault\" in text.lower() and bool(DEX_VAULT_PROTOCOL_RE.search(text)))\n # CEX custody: brand name in label text, OR Arkham entity_type=cex/exchange.\n # dex_infra takes PRECEDENCE: a label like \"Dex Router (OKX)\" is OKX's DEX\n # aggregator router (a swap venue), NOT an OKX exchange deposit — the CEX\n # brand regex would otherwise match \"OKX\" and miscount router transfers as\n # CEX deposits (R2 inflated confirmed-CEX to 183% this way).\n # v0.8.2 fix: orphan generic CEX label heuristic. Surf sometimes\n # returns `Cold Wallet` / `Hot Wallet` / `Deposit` / `Withdrawal`\n # labels with no entity_name (brand) and no entity_type=\"cex\" — these\n # are CEX wallets with weak Arkham metadata. JCT test: `0x26209d9f0dc3`\n # had label `Cold Wallet` only, was classified as \"other\" / 散户. We\n # heuristically catch these as CEX provided they don't match dex_infra\n # or other lockup classes.\n # v0.8.2.2 audit LOW #5 fix: exclude generic deposit / withdrawal\n # labels that are paired with bridge / staking / project context words\n # (e.g. a bridge contract labeled \"Bridge Deposit Address\" should not\n # be classified as CEX). The CEX heuristic now requires either:\n # (a) a bare wallet keyword (`Cold Wallet` / `Hot Wallet`) — high-\n # confidence CEX-shape, OR\n # (b) a deposit/withdrawal keyword WITHOUT a non-CEX context word.\n _ORPHAN_CEX_HIGH_CONF_RE = re.compile(\n r\"\\b(cold\\s*wallet|hot\\s*wallet|exchange\\s*(wallet|reserve))\\b\",\n re.IGNORECASE,\n )\n _ORPHAN_CEX_LOW_CONF_RE = re.compile(\n r\"\\b(deposit\\s*(wallet|address)?|withdrawal\\s*(wallet|address)?)\\b\",\n re.IGNORECASE,\n )\n _NON_CEX_CONTEXT_RE = re.compile(\n r\"\\b(bridge|staking|stake|claim|distribut|reward|\"\n r\"airdrop|emission|allocation|launchpad|launchpool|\"\n r\"vesting|lockup|multisig|treasury|dao|protocol|\"\n r\"vault|pool|router)\\b\",\n re.IGNORECASE,\n )\n _orphan_high = bool(_ORPHAN_CEX_HIGH_CONF_RE.search(text))\n _orphan_low = bool(_ORPHAN_CEX_LOW_CONF_RE.search(text))\n _has_non_cex_context = bool(_NON_CEX_CONTEXT_RE.search(text))\n _orphan_cex_match = (\n _orphan_high\n or (_orphan_low and not _has_non_cex_context)\n )\n is_cex_custody = (\n (bool(CEX_CUSTODY_LABEL_RE.search(text)) or et in (\"cex\", \"exchange\")\n or (_orphan_cex_match\n and not is_vesting and not is_multisig and not is_treasury))\n and not is_dex_infra\n # VC / investment arm (\"Binance Labs\", \"Coinbase Ventures\") carries an\n # exchange brand but is an operator-side EARLY INVESTOR (an insider-type\n # holder that can dump), NOT neutral exchange custody — must stay in the\n # insider/operator set, so it does NOT qualify as CEX.\n and not VC_ARM_LABEL_RE.search(text)\n )\n # v0.8.1: third-party distribution platforms — launchpads, task /\n # quest platforms, airdrop SaaS, generic claim-distributor contracts.\n # The contract's residual balance belongs to retail (un-claimed pool),\n # not to the project-side hand.\n #\n # PRECEDENCE: dex_infra > cex_custody > vesting > multisig > treasury\n # > airdrop_platform. Reasoning per audit LOW #1:\n # - A `DAO Treasury Airdrop Campaign` wallet should be classified\n # as treasury (project-side hand), not airdrop_platform (retail\n # un-claimed pool).\n # - A `Gnosis Safe Airdrop Admin` is a multisig with admin power\n # over the campaign, also project-side.\n is_airdrop_platform = (\n bool(AIRDROP_PLATFORM_LABEL_RE.search(text))\n and not is_dex_infra\n and not is_cex_custody\n and not is_vesting\n and not is_multisig\n and not is_treasury\n )\n\n is_protocol_lockup = (\n is_vesting or is_multisig or is_treasury or is_dex_infra or is_cex_custody\n or is_airdrop_platform\n )\n\n display = None\n if is_protocol_lockup:\n for lbl in (raw_labels or []):\n if isinstance(lbl, dict) and lbl.get(\"label\"):\n cand = str(lbl[\"label\"])\n if (VESTING_LABEL_RE.search(cand) or MULTISIG_LABEL_RE.search(cand)\n or TREASURY_LABEL_RE.search(cand) or DEX_INFRA_LABEL_RE.search(cand)\n or CEX_CUSTODY_LABEL_RE.search(cand)\n or AIRDROP_PLATFORM_LABEL_RE.search(cand)):\n display = cand\n break\n if not display and entity_name:\n display = str(entity_name)\n if not display:\n display = text[:60]\n\n match = None\n for regex in (VESTING_LABEL_RE, MULTISIG_LABEL_RE, TREASURY_LABEL_RE,\n DEX_INFRA_LABEL_RE, CEX_CUSTODY_LABEL_RE,\n AIRDROP_PLATFORM_LABEL_RE):\n m = regex.search(text)\n if m:\n match = m.group(0)\n break\n\n return {\n \"is_vesting\": is_vesting,\n \"is_multisig\": is_multisig,\n \"is_treasury\": is_treasury,\n \"is_dex_infra\": is_dex_infra,\n \"is_cex_custody\": is_cex_custody,\n \"is_airdrop_platform\": is_airdrop_platform,\n \"is_protocol_lockup\": is_protocol_lockup,\n \"label_match\": match,\n \"display_label\": display,\n }\n\n\n# ---- batch surf lookup ----\n\ndef fetch_arkham_labels_batch(addresses: list[str]) -> dict[str, dict]:\n \"\"\"Call `surf wallet-labels-batch` in chunks of 20.\n\n surf silently drops the entire response above ~25 addresses\n (empirically verified 2026-05-26 on COLLECT 55-addr run: CHUNK=30\n returned 0 rows, CHUNK=20 returned 13 labeled rows). Stick to 20.\n\n Returns {addr_lower: {entity_name, entity_type, labels}}. Addresses\n Arkham has no entry for are absent from the dict.\n \"\"\"\n if not addresses:\n return {}\n addrs = sorted({a.lower() for a in addresses if a})\n if not addrs:\n return {}\n\n CHUNK = 20\n out: dict[str, dict] = {}\n for i in range(0, len(addrs), CHUNK):\n chunk = addrs[i:i + CHUNK]\n cmd = [\"surf\", \"wallet-labels-batch\", \"--addresses\", \",\".join(chunk), \"--json\"]\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(cmd, base_timeout=20)\n if doc is None:\n continue # chunk failed even with retry; addresses fall through as unlabeled\n for row in (doc.get(\"data\") or []):\n addr = (row.get(\"address\") or \"\").lower()\n if not addr:\n continue\n label_blob = row.get(\"label\") if isinstance(row.get(\"label\"), dict) else None\n out[addr] = {\n \"entity_name\": row.get(\"entity_name\") or (label_blob or {}).get(\"entity_name\"),\n \"entity_type\": row.get(\"entity_type\") or (label_blob or {}).get(\"entity_type\"),\n \"labels\": row.get(\"labels\") or (label_blob or {}).get(\"labels\") or [],\n }\n return out\n\n\ndef enrich_addresses_with_lockup_classification(\n addresses: list[str],\n) -> dict[str, dict]:\n \"\"\"Batch query Arkham + classify each address.\n\n Returns {addr_lower: {arkham, is_vesting, is_multisig, is_treasury,\n is_dex_infra, is_cex_custody, is_protocol_lockup, label_match,\n display_label}}.\n \"\"\"\n arkham = fetch_arkham_labels_batch(addresses)\n out: dict[str, dict] = {}\n for addr in {a.lower() for a in addresses if a}:\n ark = arkham.get(addr)\n if ark:\n cls = classify_protocol_lockup(\n entity_name=ark.get(\"entity_name\"),\n entity_type=ark.get(\"entity_type\"),\n raw_labels=ark.get(\"labels\"),\n )\n out[addr] = {\"arkham\": ark, **cls}\n else:\n out[addr] = {\n \"arkham\": None,\n \"is_vesting\": False, \"is_multisig\": False,\n \"is_treasury\": False, \"is_dex_infra\": False,\n \"is_cex_custody\": False, \"is_airdrop_platform\": False,\n \"is_protocol_lockup\": False,\n \"label_match\": None, \"display_label\": None,\n }\n return out\n\n\nif __name__ == \"__main__\":\n import argparse\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"addresses\", nargs=\"+\", help=\"Addresses to classify\")\n args = ap.parse_args()\n print(json.dumps(\n enrich_addresses_with_lockup_classification(args.addresses),\n indent=2, ensure_ascii=False,\n ))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":18046,"content_sha256":"fe5ce946809149543b09e35cb7d4feec2ffc59c9943490e659998b9132897332"},{"filename":"alpha/v06/helpers/push_airdrop_detector.py","content":"#!/usr/bin/env python3\n\"\"\"push_airdrop_detector.py — identify retail \"push\" airdrop recipients\nin the pre-launch deployer-outflow window so dump_tracker does not\nmis-classify them as insiders.\n\n# Why this exists (v0.8.1)\n\n`pre_launch_receivers` is everyone who received tokens directly from the\ndeployer between `trace_floor` and `alpha_listing_date`. That set is the\nbasis for the \"insider\" universe used to compute `confirmed_total_tokens`\n(a)+(b) — the \"确认毛卖出\" metric.\n\nFailure case: if the project did a `disperse`-style push airdrop to N\nretail wallets (`deployer → batch_transfer → 5,000 EOAs`) before listing,\nevery one of those EOAs lands in `pre_launch_receivers` and their post-\nlisting sells inflate the \"insider 真实变现\" number. Pull / Merkle-claim\nairdrops do NOT trigger this — retail wallets receive from the claim\ncontract, not the deployer, so they never enter `pre_launch_receivers`.\n\n# Heuristic\n\nA pre-launch deployer outflow tx is classified as a push-airdrop\ndistribution iff:\n\n COUNT(DISTINCT to_addr) within one tx_hash >= PUSH_MIN_RECIPIENTS\n\nDefault threshold = 50. Rationale:\n - VC / team allocation rounds use Gnosis Safe multi-call or hand-\n submitted batches, typically \u003c= 20 recipients per tx (often 1).\n - Retail airdrop disperse libraries (disperse.app, gas-optimized\n multi-send) emit transactions with 50-1000 recipients.\n - 50 is conservative: it accepts a few false negatives (a VC round\n with 50+ allocations in one tx is unusual but possible) in\n exchange for near-zero false positives on the retail side.\n\n# Output\n\nEvery recipient address appearing in any tx that crosses the threshold\nis labeled `is_push_airdrop_recipient`. Those addresses are excluded\nfrom `insider_addrs` (so their CEX deposits / DEX swaps no longer\ninflate the (a)+(b) confirmed-sellout floor).\n\nConservative semantics:\n - We exclude from the INSIDER set, not from the holdings tree. The\n tokens are still tracked for supply accounting; we just no longer\n attribute their post-launch sells to insider exit.\n - This is one-way: a wallet flagged via this detector stays flagged\n even if it also received a separate \"VC-shaped\" allocation. The\n rare false positive of misclassifying a hybrid wallet is preferred\n over the systemic false positive of treating every airdrop recipient\n as an insider.\n\nCost: 1 surf transfers query per CA, time-bounded to the rule_11 window\n(deployer outflows are sparse so the GROUP BY is cheap).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent))\nfrom chain_router import transfers_table # v0.7.20\nfrom chain_router import decimals_factor_str # v0.9.7\n\n_HEX_ADDR_RE = re.compile(r\"^0x[0-9a-fA-F]{40}$\")\n\n# Threshold rationale documented in module docstring. Exposed as constant\n# so calibration scripts can sweep it if a token-class-specific override\n# is ever needed (e.g. Solana SPL programs that batch differently).\nPUSH_MIN_RECIPIENTS = 50\n\n# v0.8.1 audit MED fix: shape guard. A push airdrop's per-recipient\n# amount is small (each retail wallet gets a tiny fraction of supply). A\n# large delegated VC/MM allocation batch that happens to have 50+ unique\n# recipients (rare but possible) would otherwise be fully excluded from\n# insiders by the simple count gate. We additionally require the mean\n# per-recipient amount in the flagged tx to be at most this fraction of\n# supply. Default 0.1% supply: typical retail airdrop = $1-100/wallet\n# (well under 0.01% supply); typical VC allocation = 1-5% supply / wallet\n# (well over 0.1%). 0.1% sits comfortably in the gap.\nPUSH_MAX_MEAN_PER_RECIPIENT_PCT_SUPPLY = 0.1\n\n\ndef _valid_ca(ca: str | None) -> str | None:\n if not ca or not isinstance(ca, str):\n return None\n if not _HEX_ADDR_RE.match(ca):\n return None\n return ca.lower()\n\n\ndef _valid_addr(a: str | None) -> str | None:\n if not a or not isinstance(a, str):\n return None\n a = a.lower()\n if not _HEX_ADDR_RE.match(a):\n return None\n return a\n\n\ndef fetch_push_airdrop_recipients(\n ca: str,\n deployer: str,\n trace_floor: str,\n listing_date: str,\n *,\n min_recipients: int = PUSH_MIN_RECIPIENTS,\n total_supply: float | None = None,\n max_mean_per_recipient_pct_supply: float = PUSH_MAX_MEAN_PER_RECIPIENT_PCT_SUPPLY,\n) -> dict[str, Any]:\n \"\"\"Identify push-airdrop batch transfers in the pre-launch deployer\n outflow window.\n\n Args:\n ca: token contract address (lower-cased internally).\n deployer: rule_11-derived deployer address.\n trace_floor: 'YYYY-MM-DD' lower bound for the pre-launch trace.\n listing_date: 'YYYY-MM-DD' Alpha-listing date (inclusive upper).\n min_recipients: distinct-to-addr threshold per tx_hash. Default 50.\n\n Returns:\n {\n \"airdrop_tx_hashes\": [str], # tx hashes flagged\n \"airdrop_recipients\": [str], # lowercase 0x recipients\n \"n_tx\": int,\n \"n_recipients\": int,\n \"tokens_airdropped_total\": float, # sum across all flagged tx\n \"mean_recipients_per_tx\": float,\n \"max_recipients_per_tx\": int,\n }\n On surf error returns {\"__ERR\": str, \"airdrop_recipients\": []} so the\n caller can fail-loud without breaking downstream insider_addrs logic.\n On no-pattern returns the dict with empty arrays — distinguishable\n from error by absence of __ERR.\n \"\"\"\n ca = _valid_ca(ca)\n deployer = _valid_addr(deployer)\n if not ca or not deployer:\n return {\"airdrop_recipients\": [], \"airdrop_tx_hashes\": [],\n \"n_tx\": 0, \"n_recipients\": 0,\n \"tokens_airdropped_total\": 0.0,\n \"mean_recipients_per_tx\": 0.0,\n \"max_recipients_per_tx\": 0}\n\n transfers = transfers_table()\n # v0.8.1 audit MED #2 + LOW fix: amount-shape gate. Push\n # airdrops have small per-recipient amounts (each retail wallet\n # gets \u003c0.1% supply typically). Add a `HAVING avg(amt) \u003c= cap`\n # filter in the inner CTE so a 50-receiver VC allocation tx (where\n # each VC gets 0.5-5% supply) does NOT flag as push airdrop.\n # audit LOW #1 also fixed: explicit GROUP BY (recipient, tx)\n # in the outer query so multi-log txs collapse to one row per\n # (recipient, tx, amount).\n mean_cap_raw_supply = None\n if total_supply and total_supply > 0:\n mean_cap_raw_supply = (total_supply * max_mean_per_recipient_pct_supply / 100.0)\n cap_clause = (\n f' AND avg(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) \u003c= {mean_cap_raw_supply}'\n if mean_cap_raw_supply is not None else ''\n )\n sql = (\n f'WITH flagged_tx AS ('\n f' SELECT tx_hash,'\n f' count(DISTINCT \"to\") AS n_to,'\n f' avg(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS mean_amt'\n f' FROM {transfers}'\n f\" WHERE contract_address = '{ca}'\"\n f' AND \"from\" = \\'{deployer}\\''\n f\" AND block_date BETWEEN '{trace_floor}' AND '{listing_date}'\"\n f' GROUP BY tx_hash'\n f' HAVING n_to >= {min_recipients}'\n f'{cap_clause}'\n f') '\n f'SELECT \"to\" AS recipient, tx_hash,'\n f' sum(toFloat64(toDecimal256(amount_raw,0))/{decimals_factor_str()}) AS amount'\n f' FROM {transfers}'\n f\" WHERE contract_address = '{ca}'\"\n f' AND \"from\" = \\'{deployer}\\''\n f\" AND block_date BETWEEN '{trace_floor}' AND '{listing_date}'\"\n f' AND tx_hash IN (SELECT tx_hash FROM flagged_tx)'\n f' GROUP BY recipient, tx_hash'\n )\n try:\n from section_a_scope import _run_surf_with_retry\n doc, err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n # v0.8.5.2: 50000 撞 surf 10K cap → INVALID_REQUEST → 整 SQL fail\n # → buckets_complete=false 误触发 \"dump_tracker 不可信\" banner.\n # adversarial review CLO bug report 2026-06-12. push-airdrop pattern 通常\n # \u003c 1K recipients per tx batch, 10K 足够.\n stdin=json.dumps({\"sql\": sql, \"max_rows\": 10000}),\n base_timeout=60,\n max_attempts=4,\n )\n except Exception as e:\n doc, err = None, f\"exception: {str(e)[:120]}\"\n\n if not doc:\n return {\"__ERR\": err or \"surf returned no doc\",\n \"airdrop_recipients\": [], \"airdrop_tx_hashes\": [],\n \"n_tx\": 0, \"n_recipients\": 0,\n \"tokens_airdropped_total\": 0.0,\n \"mean_recipients_per_tx\": 0.0,\n \"max_recipients_per_tx\": 0}\n\n rows = doc.get(\"data\") or []\n if not rows:\n return {\"airdrop_recipients\": [], \"airdrop_tx_hashes\": [],\n \"n_tx\": 0, \"n_recipients\": 0,\n \"tokens_airdropped_total\": 0.0,\n \"mean_recipients_per_tx\": 0.0,\n \"max_recipients_per_tx\": 0}\n\n recipients: set[str] = set()\n tx_hashes: set[str] = set()\n tx_recipient_counts: dict[str, int] = {}\n total_amt = 0.0\n for r in rows:\n recipient = r.get(\"recipient\") or \"\"\n if isinstance(recipient, str):\n recipient = recipient.lower()\n if not _HEX_ADDR_RE.match(recipient or \"\"):\n continue\n tx = r.get(\"tx_hash\") or \"\"\n if not isinstance(tx, str) or not tx:\n continue\n amt = r.get(\"amount\")\n try:\n amt_f = float(amt) if amt is not None else 0.0\n except (TypeError, ValueError):\n amt_f = 0.0\n recipients.add(recipient)\n tx_hashes.add(tx)\n tx_recipient_counts[tx] = tx_recipient_counts.get(tx, 0) + 1\n total_amt += amt_f\n\n if not recipients:\n return {\"airdrop_recipients\": [], \"airdrop_tx_hashes\": [],\n \"n_tx\": 0, \"n_recipients\": 0,\n \"tokens_airdropped_total\": 0.0,\n \"mean_recipients_per_tx\": 0.0,\n \"max_recipients_per_tx\": 0}\n\n counts = list(tx_recipient_counts.values())\n return {\n \"airdrop_tx_hashes\": sorted(tx_hashes),\n \"airdrop_recipients\": sorted(recipients),\n \"n_tx\": len(tx_hashes),\n \"n_recipients\": len(recipients),\n \"tokens_airdropped_total\": float(total_amt),\n \"mean_recipients_per_tx\": (sum(counts) / len(counts)) if counts else 0.0,\n \"max_recipients_per_tx\": max(counts) if counts else 0,\n \"_threshold_used\": min_recipients,\n }\n\n\n__all__ = [\"fetch_push_airdrop_recipients\", \"PUSH_MIN_RECIPIENTS\"]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10535,"content_sha256":"1b6e0e30e995f72edb742da54fe12a4baf7fab4b995524d43176b570ee07f9f6"},{"filename":"alpha/v06/helpers/recent_flow_actions.py","content":"\"\"\"recent_flow_actions.py — v1.2.9 (product spec 2026-06-29).\n\nNear-72h FAN-OUT (an operator source spraying to many EOAs) + CONSOLIDATION (many\nEOAs concentrating into a CEX / hub) detection, surfaced at HIGHEST priority in the\n一屏结论 / 速读.\n\nWHY THIS EXISTS\n===============\n`discover_cex_fanout_hubs` only keeps a hub whose TOP SENDER is a CEX — it is built\nfor the \"pull from Gate hot wallet → hub → sub-wallets\" pattern. It MISSES a\nMINT-AUTHORITY / cluster-hub source fanning out to fresh sockpuppets, which is the\npre-dump *seeding* signal. JCT/Janction (2026-06-29): a mint-authority Gnosis Safe\nfanned out to 50 EOAs in the last 72h (~120-234M tokens each, one tx each) — a\ntextbook \"mint → seed 50 wallets → they'll deposit to CEX / dump\" move, and it was\ninvisible to every existing detector (cex_fanout wants a CEX source; anomaly_72h\ntruncated the events; rule_11/mint_authority don't look at *recent* fan-out shape).\n\nThis detector answers the recurring operator question: \"is the operator, RIGHT NOW\n(72h), preparing the next dump — seeding wallets (fan-out) or cashing out\n(consolidation to CEX)?\"\n\nDesign (cheap: 2 SQL over a 3-day window on one chain)\n 1. fan-out: GROUP BY `from` HAVING count(distinct `to`) >= min_counterparties\n 2. consolidation:GROUP BY `to` HAVING count(distinct `from`)>= min_counterparties\nThen classify each hub in Python against the operator / CEX / infra address sets, and\ndrop bidirectional wash-bots (a hub that is a TOP node in BOTH lists = MM churner, not\na distribution/consolidation action).\n\"\"\"\nfrom __future__ import annotations\n\nfrom datetime import date, timedelta\nfrom typing import Any, Callable\n\nfrom chain_router import transfers_table, decimals_factor_str\n\n_SURF_MAX_LOOKBACK_DAYS = 365\n\n_SQL_FANOUT = (\n 'SELECT \"from\" AS hub, count(DISTINCT \"to\") AS n_cp, count() AS n_tx, '\n \"sum(toFloat64(toDecimal256(amount_raw,0))/{df}) AS total \"\n \"FROM {transfers} WHERE contract_address='{ca}' AND block_date >= '{floor}' \"\n \"AND \\\"to\\\" NOT IN ('0x0000000000000000000000000000000000000000',\"\n \"'0x000000000000000000000000000000000000dead') \"\n \"GROUP BY hub HAVING n_cp >= {min_cp} ORDER BY n_cp DESC LIMIT 40\"\n)\n_SQL_CONSOLIDATE = (\n 'SELECT \"to\" AS hub, count(DISTINCT \"from\") AS n_cp, count() AS n_tx, '\n \"sum(toFloat64(toDecimal256(amount_raw,0))/{df}) AS total \"\n \"FROM {transfers} WHERE contract_address='{ca}' AND block_date >= '{floor}' \"\n \"GROUP BY hub HAVING n_cp >= {min_cp} ORDER BY n_cp DESC LIMIT 40\"\n)\n\n\ndef _default_run_sql(sql: str) -> dict[str, Any] | None:\n from section_a_scope import _run_surf_with_retry\n import json as _json\n doc, _err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=_json.dumps({\"sql\": sql, \"max_rows\": 100}), base_timeout=40,\n max_attempts=4,\n )\n return doc\n\n\ndef _norm(a: str | None) -> str:\n return (a or \"\").lower()\n\n\ndef detect_recent_flow_actions(\n *,\n ca: str,\n operator_addrs: set[str] | None = None,\n cex_addrs: set[str] | None = None,\n infra_addrs: set[str] | None = None,\n window_days: int = 3,\n min_counterparties: int = 10,\n run_sql: Callable[[str], dict[str, Any] | None] | None = None,\n) -> dict[str, Any]:\n \"\"\"Detect operator-source fan-out + CEX consolidation in the last `window_days`.\n\n operator_addrs: mint authorities + cluster hubs + suspected reserves + deployer.\n cex_addrs: CEX deposit / hot-wallet addresses (resolve_labels CEX class).\n infra_addrs: DEX pools / routers — neutral, excluded from being flagged.\n Returns a dict stored on skeleton.recent_flow_actions and read by screen_summary.\n \"\"\"\n operator_addrs = {_norm(a) for a in (operator_addrs or set()) if a}\n cex_addrs = {_norm(a) for a in (cex_addrs or set()) if a}\n infra_addrs = {_norm(a) for a in (infra_addrs or set()) if a}\n run_sql = run_sql or _default_run_sql\n\n ca_lc = _norm(ca)\n floor = (date.today() - timedelta(days=max(1, window_days))).isoformat()\n df = decimals_factor_str()\n transfers = transfers_table()\n\n def _rows(sql_tpl: str) -> list[dict[str, Any]]:\n sql = sql_tpl.format(df=df, transfers=transfers, ca=ca_lc, floor=floor,\n min_cp=int(min_counterparties))\n try:\n doc = run_sql(sql)\n except Exception as e: # noqa: BLE001\n return [{\"__err\": str(e)[:120]}]\n if not doc:\n return [{\"__err\": \"surf_no_doc\"}]\n return doc.get(\"data\") or []\n\n fan_rows = _rows(_SQL_FANOUT)\n con_rows = _rows(_SQL_CONSOLIDATE)\n err = next((r[\"__err\"] for r in (fan_rows + con_rows) if \"__err\" in r), None)\n fan_rows = [r for r in fan_rows if \"__err\" not in r]\n con_rows = [r for r in con_rows if \"__err\" not in r]\n\n # bidirectional = wash-bot / MM churner: a hub that is a top node in BOTH lists.\n fan_hubs = {_norm(r.get(\"hub\")) for r in fan_rows}\n con_hubs = {_norm(r.get(\"hub\")) for r in con_rows}\n bidirectional = fan_hubs & con_hubs\n\n def _mk(r: dict[str, Any], side: str) -> dict[str, Any]:\n hub = _norm(r.get(\"hub\"))\n is_op = hub in operator_addrs\n is_cex = hub in cex_addrs\n is_infra = hub in infra_addrs\n is_wash = hub in bidirectional\n # Check the SEMANTIC role first: a CEX / operator / DEX hub is bidirectional\n # by nature (a CEX both receives and pays out), so the wash-churn exclusion\n # (bidirectional) must apply ONLY to *unknown* hubs — otherwise a genuine CEX\n # consolidation gets wrongly dropped as \"wash\".\n if side == \"fanout\":\n # operator-source fan-out = pre-dump seeding (the JCT case). A DEX\n # router / CEX fanning out is just swaps / withdrawals → infra, not a signal.\n if is_op:\n kind = \"operator_source_fanout\"\n elif is_infra or is_cex:\n kind = \"infra_fanout\"\n elif is_wash:\n kind = \"wash_churn\"\n else:\n kind = \"unknown_hub_fanout\" # possible hidden operator hub\n else: # consolidation\n if is_cex:\n kind = \"cex_consolidation\" # active cashing out (the signal)\n elif is_op:\n kind = \"operator_consolidation\"\n elif is_infra:\n kind = \"infra_consolidation\" # EOAs → DEX pool = selling\n elif is_wash:\n kind = \"wash_churn\"\n else:\n kind = \"unknown_hub_consolidation\"\n return {\n \"hub\": hub, \"n_counterparties\": int(r.get(\"n_cp\") or 0),\n \"n_tx\": int(r.get(\"n_tx\") or 0), \"total_tokens\": float(r.get(\"total\") or 0),\n \"kind\": kind, \"is_operator\": is_op, \"is_cex\": is_cex,\n }\n\n fanout = [_mk(r, \"fanout\") for r in fan_rows]\n consolidation = [_mk(r, \"consolidation\") for r in con_rows]\n\n # the signal actions retail must see first. v1.2.12: keep the CONFIRMED-\n # operator source fan-out separate from the UNKNOWN-hub fan-out — the former\n # is the top-priority 🔴 headline, the latter a 🟡 疑似批量分发 medium row\n # (product spec 2026-07-01, TAC: an 88-wallet unknown-hub 72h fan-out was suppressed\n # entirely). top_operator_fanout must NOT be contaminated by unknown hubs.\n op_fanout = [a for a in fanout if a[\"kind\"] == \"operator_source_fanout\"]\n unknown_fanout = [a for a in fanout if a[\"kind\"] == \"unknown_hub_fanout\"]\n cex_con = [a for a in consolidation if a[\"kind\"] == \"cex_consolidation\"]\n\n return {\n \"window_days\": window_days,\n \"min_counterparties\": min_counterparties,\n \"fanout\": fanout,\n \"consolidation\": consolidation,\n \"has_operator_fanout\": bool(op_fanout),\n \"has_unknown_fanout\": bool(unknown_fanout),\n \"has_cex_consolidation\": bool(cex_con),\n \"n_operator_fanout\": len(op_fanout),\n \"n_unknown_fanout\": len(unknown_fanout),\n \"n_cex_consolidation\": len(cex_con),\n \"top_operator_fanout\": max(op_fanout, key=lambda a: a[\"n_counterparties\"], default=None),\n \"top_unknown_fanout\": max(unknown_fanout, key=lambda a: a[\"n_counterparties\"], default=None),\n \"top_cex_consolidation\": max(cex_con, key=lambda a: a[\"n_counterparties\"], default=None),\n \"_error\": err,\n }\n","content_type":"text/x-python; charset=utf-8","language":"python","size":8328,"content_sha256":"41ab79b70a72f7890c6effd80d86a243b1cd33d3ee6ac4086bab69c142199bdc"},{"filename":"alpha/v06/helpers/recent_mint_events.py","content":"\"\"\"recent_mint_events.py — v1.2.14 (product spec 2026-07-01, TAC).\n\nRecent MINT-EVENT detection (fresh supply created from the 0x0 black-hole), surfaced\nat HIGH priority in the 一屏结论 / 速读.\n\nWHY THIS EXISTS\n===============\n`discover_mint_authorities` answers WHO can mint and HOW MUCH total, but NOT WHEN — it\ncarries no timestamp, so a large recent mint (the operator creating fresh ammo, often\nthe pump source) was invisible to a reader of the 一屏结论 / 速读. TAC (2026-07-01): a\n164.2M-token mint on 06-29 (317 tx from 0x0) was the pump source per external on-chain\nwatchers, yet the report only showed the mint authorities' *held balance* with no date.\n\nA large mint from 0x0 is a time-sensitive operator action (\"new ammo just created\") and\nbelongs next to the recent fan-out / consolidation row — with a timestamp.\n\nDesign (cheap: 1 SQL, daily GROUP BY over a lookback window on one chain)\n SELECT toDate(block_time) AS d, sum(minted), count() FROM transfers\n WHERE from = 0x0 GROUP BY d\nThen in Python: mark days whose minted >= `min_pct_circ`% of circulating as SIGNIFICANT,\nand flag the largest SIGNIFICANT mint whose date is within `recent_window_days` of today.\nWe report the FACT (large mint at date T, % of supply) — NOT a causal \"pump source\"\nclaim; the copy asks the reader to corroborate with price / perp OI.\n\"\"\"\nfrom __future__ import annotations\n\nfrom datetime import date, datetime, timedelta, timezone\nfrom typing import Any, Callable\n\nfrom chain_router import transfers_table, decimals_factor_str\n\n_ZERO = \"0x0000000000000000000000000000000000000000\"\n\n_SQL_MINT_BY_DAY = (\n \"SELECT toDate(block_time) AS d, \"\n \"sum(toFloat64(toDecimal256(amount_raw,0))/{df}) AS minted, count() AS n_tx \"\n \"FROM {transfers} WHERE contract_address='{ca}' AND \\\"from\\\"='\" + _ZERO + \"' \"\n \"AND block_date >= '{floor}' GROUP BY d ORDER BY d DESC LIMIT 400\"\n)\n\n\ndef _default_run_sql(sql: str) -> dict[str, Any] | None:\n from section_a_scope import _run_surf_with_retry\n import json as _json\n doc, _err = _run_surf_with_retry(\n [\"surf\", \"onchain-sql\"],\n stdin=_json.dumps({\"sql\": sql, \"max_rows\": 400}), base_timeout=40,\n max_attempts=4,\n )\n return doc\n\n\ndef _to_iso_date(d: Any) -> str | None:\n \"\"\"surf may return toDate() as a 'YYYY-MM-DD' string or a unix-seconds integer.\"\"\"\n if d is None:\n return None\n s = str(d)\n if s.isdigit():\n try:\n return datetime.fromtimestamp(int(s), tz=timezone.utc).date().isoformat()\n except (ValueError, OSError, OverflowError):\n return None\n return s[:10]\n\n\ndef detect_recent_mint_events(\n *,\n ca: str,\n circ_supply: float,\n recent_window_days: int = 14,\n min_pct_circ: float = 1.0,\n lookback_days: int = 120,\n today: date | None = None,\n run_sql: Callable[[str], dict[str, Any] | None] | None = None,\n) -> dict[str, Any]:\n \"\"\"Detect recent significant mint events (fresh supply from 0x0).\n\n circ_supply: circulating supply; a mint DAY is SIGNIFICANT when its minted\n total is >= min_pct_circ% of it (0 circ → nothing significant).\n recent_window_days: a significant mint is surfaced as a headline only if its date is\n within this many days of `today`.\n Returns a dict stored on skeleton.recent_mint_events and read by screen_summary.\n \"\"\"\n run_sql = run_sql or _default_run_sql\n # UTC (chain block dates are UTC) so days_ago can't drift ±1 vs a host in another tz.\n today = today or datetime.now(timezone.utc).date()\n ca_lc = (ca or \"\").lower()\n floor = (today - timedelta(days=max(recent_window_days, lookback_days))).isoformat()\n sql = _SQL_MINT_BY_DAY.format(\n df=decimals_factor_str(), transfers=transfers_table(), ca=ca_lc, floor=floor)\n\n out: dict[str, Any] = {\n \"recent_window_days\": recent_window_days,\n \"min_pct_circ\": min_pct_circ,\n \"lookback_days\": lookback_days,\n \"mint_days\": [],\n \"significant_mints\": [],\n \"has_recent_significant_mint\": False,\n \"top_recent_mint\": None,\n \"recent_significant_total\": 0.0,\n \"_error\": None,\n }\n try:\n doc = run_sql(sql)\n except Exception as e: # noqa: BLE001\n out[\"_error\"] = str(e)[:150]\n return out\n if not doc:\n out[\"_error\"] = \"surf_no_doc\"\n return out\n\n try:\n circ = float(circ_supply or 0)\n except (TypeError, ValueError):\n circ = 0.0\n floor_tokens = circ * (min_pct_circ / 100.0) if circ > 0 else float(\"inf\")\n\n days: list[dict[str, Any]] = []\n for r in (doc.get(\"data\") or []):\n # v1.2.14 (adversarial review nit): keep ALL row parsing local so a malformed\n # minted / n_tx / date can never throw out of the helper — skip the bad row.\n try:\n iso = _to_iso_date(r.get(\"d\"))\n if not iso:\n continue\n minted = float(r.get(\"minted\") or 0)\n days_ago = (today - date.fromisoformat(iso)).days\n rec = {\n \"date\": iso,\n \"minted\": minted,\n \"n_tx\": int(r.get(\"n_tx\") or 0),\n \"pct_circ\": (minted / circ * 100) if circ > 0 else 0.0,\n \"days_ago\": days_ago,\n }\n except (TypeError, ValueError):\n continue\n days.append(rec)\n\n days.sort(key=lambda x: x[\"date\"], reverse=True)\n out[\"mint_days\"] = days\n significant = [d for d in days if d[\"minted\"] >= floor_tokens]\n out[\"significant_mints\"] = significant\n\n recent_sig = [d for d in significant if 0 \u003c= d[\"days_ago\"] \u003c= recent_window_days]\n if recent_sig:\n out[\"has_recent_significant_mint\"] = True\n out[\"recent_significant_total\"] = sum(d[\"minted\"] for d in recent_sig)\n # headline = the LARGEST recent significant mint day\n out[\"top_recent_mint\"] = max(recent_sig, key=lambda x: x[\"minted\"])\n return out\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5971,"content_sha256":"c68ebca41c22366b36ed1b1a0db92b401c4fb33df176eaf05845bbda6245c7f8"},{"filename":"alpha/v06/helpers/role_classifier.py","content":"#!/usr/bin/env python3\n\"\"\"role_classifier.py — v0.7.7 whale role classification.\n\nInputs: a cross-sym whale candidate (address + the token CA + listing date).\nOutput: deterministic role + evidence chain, derived purely from on-chain\nbehavior. Does NOT depend on Arkham labels (Arkham is used by the sibling\nsurf_labels_probe to filter known infrastructure BEFORE this classifier\nruns, but here we work with what's left).\n\n6-step decision tree (see reference_whale_role_and_wash_methodology.md\nfor the full doc):\n\n Step 1: EOA vs CONTRACT (eth_getCode → BSC RPC, free)\n Step 2: funder distribution (1 SQL)\n Step 3: recipient distribution (1 SQL)\n Step 4: upstream chain trace, max 3 hops by default (1-3 SQL)\n Step 5: temporal pattern derived from existing data (0 SQL)\n Step 6: net-flow derived (0 SQL)\n\nTotal: ~3-5 SQL per candidate. Returns role + confidence + evidence.\n\nRole enum:\n insider_allocation_holder — project-aligned, got lump sum on TGE\n active_multi_token_allocator — like above but actively manages many bags\n dex_mm_bot — MM contract trading vs LP\n retail_holder — DEX-bought, long-held\n wash_infra_member — part of a wash setup (defer to wash detector)\n unknown — insufficient signal\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport subprocess\nimport sys\nfrom typing import Any\nfrom chain_router import ( # v0.7.20 / .21.7\n transfers_table, dex_trades_table,\n is_valid_addr as _chain_is_valid_addr,\n get_active_chain as _chain_get_active,\n)\n\n# Surf SQL timeout — narrower than CA-wide queries to fail fast\nSURF_TIMEOUT_SECS = 30\n# v0.7.21.7: kept for back-compat callers; chain-aware checks go through\n# `_addr_ok` below so Solana base58 candidates survive the guard.\n_ADDR_RE = re.compile(r\"^0x[0-9a-f]{40}$\")\n\n\ndef _addr_ok(addr) -> bool:\n if not isinstance(addr, str) or not addr:\n return False\n if _chain_get_active() == \"solana\":\n return _chain_is_valid_addr(addr)\n return _chain_is_valid_addr(addr.lower())\nMINT_ADDR = \"0x0000000000000000000000000000000000000000\"\n\n# Binance Alpha 2.0 Router — well-known liquidation venue used by insiders\n# to sell via off-chain orderbook without leaving DEX volume traces.\nALPHA_ROUTER_BSC = \"0x73d8bd54f7cf5fab43fe4ef40a62d390644946db\"\n\n\nclass RoleClassifierError(Exception):\n \"\"\"Hard failure. Caller should catch and default classification to unknown.\"\"\"\n\n\ndef _run_sql(sql: str, max_rows: int = 50) -> tuple[list[dict], int]:\n body = json.dumps({\"sql\": sql, \"max_rows\": max_rows})\n try:\n proc = subprocess.run(\n [\"surf\", \"onchain-sql\"],\n input=body, capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\",\n timeout=SURF_TIMEOUT_SECS, check=False,\n )\n except (subprocess.TimeoutExpired, OSError) as e:\n raise RoleClassifierError(f\"surf onchain-sql failed: {e}\") from e\n if proc.returncode != 0:\n raise RoleClassifierError(\n f\"surf exit {proc.returncode}: {proc.stderr[:200]}\"\n )\n try:\n doc = json.loads(proc.stdout)\n except json.JSONDecodeError as e:\n raise RoleClassifierError(f\"surf non-JSON: {e}\") from e\n if doc.get(\"error\"):\n raise RoleClassifierError(f\"surf API error: {doc['error']}\")\n credits = int((doc.get(\"meta\") or {}).get(\"credits_used\") or 1)\n return doc.get(\"data\") or [], credits\n\n\ndef _eth_get_code(addr: str) -> tuple[str, int]:\n \"\"\"Returns (code_hex, byte_count). 0x → EOA. Free RPC (no surf credits).\"\"\"\n payload = {\n \"jsonrpc\": \"2.0\",\n \"method\": \"eth_getCode\",\n \"params\": [addr, \"latest\"],\n \"id\": 1,\n }\n try:\n proc = subprocess.run(\n [\"curl\", \"-sS\", \"-X\", \"POST\", \"https://bsc-dataseed.binance.org\",\n \"-H\", \"Content-Type: application/json\",\n \"--max-time\", \"8\",\n \"-d\", json.dumps(payload)],\n capture_output=True, text=True, check=False,\n timeout=13, # v0.9.9: subprocess ceiling > curl --max-time 8 (deadlock guard)\n )\n result = json.loads(proc.stdout).get(\"result\", \"\")\n except Exception:\n return \"\", 0\n if not result or not result.startswith(\"0x\"):\n return \"\", 0\n code_bytes = (len(result) - 2) // 2\n return result, code_bytes\n\n\ndef _step1_eoa_vs_contract(addr: str) -> dict:\n \"\"\"eth_getCode → EOA vs contract size tier. Free (no surf).\"\"\"\n code, code_bytes = _eth_get_code(addr)\n if code == \"0x\" or code_bytes == 0:\n return {\"is_contract\": False, \"code_bytes\": 0, \"size_tier\": \"EOA\"}\n if code_bytes \u003c 5000:\n tier = \"PROXY_OR_SIMPLE\"\n elif code_bytes \u003c 15000:\n tier = \"LITE_MM\"\n elif code_bytes \u003c 50000:\n tier = \"FULL_MM\"\n else:\n tier = \"COMPLEX_AGGREGATOR\"\n return {\"is_contract\": True, \"code_bytes\": code_bytes, \"size_tier\": tier}\n\n\ndef _step2_funders(ca: str, addr: str, listing_date: str) -> tuple[dict, int]:\n \"\"\"Who sent token to this addr? Returns top-funder pattern.\"\"\"\n sql = (\n f\"SELECT `from` AS funder, count(*) AS n, \"\n f\"sum(amount) AS tok, sum(coalesce(amount_usd,0)) AS usd, \"\n f\"min(block_time) AS first_ts \"\n f\"FROM {transfers_table()} \"\n f\"WHERE contract_address = '{ca}' \"\n f\"AND block_date >= '{listing_date}' \"\n f\"AND `to` = '{addr}' \"\n f\"GROUP BY funder ORDER BY tok DESC LIMIT 5\"\n )\n rows, credits = _run_sql(sql)\n if not rows:\n return {\"pattern\": \"no_inflow\", \"top_funder\": None, \"total_in_tok\": 0}, credits\n total_tok = sum(float(r.get(\"tok\") or 0) for r in rows)\n top = rows[0]\n top_share = float(top.get(\"tok\") or 0) / max(total_tok, 1)\n pattern = \"unknown\"\n if len(rows) == 1 and int(top.get(\"n\") or 0) == 1:\n pattern = \"single_lump\"\n elif top_share > 0.9 and int(top.get(\"n\") or 0) \u003c= 3:\n pattern = \"single_funder_few_tx\"\n elif len(rows) >= 3:\n pattern = \"multi_funder\"\n return {\n \"pattern\": pattern,\n \"top_funder\": top.get(\"funder\"),\n \"top_funder_tok\": float(top.get(\"tok\") or 0),\n \"top_funder_usd\": float(top.get(\"usd\") or 0),\n \"top_funder_first_ts\": int(top.get(\"first_ts\") or 0),\n \"top_share\": top_share,\n \"total_in_tok\": total_tok,\n }, credits\n\n\ndef _step3_recipients(ca: str, addr: str, listing_date: str) -> tuple[dict, int]:\n \"\"\"Where did this addr send token? Pattern reveals exit channel.\"\"\"\n sql = (\n f\"SELECT `to` AS dst, count(*) AS n, \"\n f\"sum(amount) AS tok, sum(coalesce(amount_usd,0)) AS usd \"\n f\"FROM {transfers_table()} \"\n f\"WHERE contract_address = '{ca}' \"\n f\"AND block_date >= '{listing_date}' \"\n f\"AND `from` = '{addr}' \"\n f\"GROUP BY dst ORDER BY tok DESC LIMIT 5\"\n )\n rows, credits = _run_sql(sql)\n if not rows:\n return {\"pattern\": \"no_outflow\", \"top_recipient\": None, \"total_out_tok\": 0}, credits\n total_tok = sum(float(r.get(\"tok\") or 0) for r in rows)\n top = rows[0]\n top_share = float(top.get(\"tok\") or 0) / max(total_tok, 1)\n top_addr = (top.get(\"dst\") or \"\").lower()\n pattern = \"unknown\"\n if top_addr == ALPHA_ROUTER_BSC and top_share > 0.85:\n pattern = \"alpha_router_dominant\" # 经 Alpha 卖 = insider 液体化\n elif top_share > 0.9:\n pattern = \"single_destination\"\n elif len(rows) >= 3 and top_share \u003c 0.5:\n pattern = \"distributed\"\n return {\n \"pattern\": pattern,\n \"top_recipient\": top_addr,\n \"top_share\": top_share,\n \"total_out_tok\": total_tok,\n }, credits\n\n\ndef _step4_upstream_chain(ca: str, addr: str, max_hops: int = 3) -> tuple[dict, int]:\n \"\"\"Walk backwards from addr → biggest funder → biggest funder → ...\n Stop when reach mint, cycle, or max_hops. Returns chain + reached_mint.\"\"\"\n chain = [addr.lower()]\n credits_total = 0\n current = addr.lower()\n for hop in range(max_hops):\n sql = (\n f\"SELECT `from` AS src, sum(amount) AS tok \"\n f\"FROM {transfers_table()} \"\n f\"WHERE contract_address = '{ca}' \"\n f\"AND block_date >= '2025-01-01' \"\n f\"AND `to` = '{current}' \"\n f\"GROUP BY src ORDER BY tok DESC LIMIT 1\"\n )\n try:\n rows, c = _run_sql(sql)\n credits_total += c\n except RoleClassifierError:\n break\n if not rows:\n break\n top_src = (rows[0].get(\"src\") or \"\").lower()\n if not top_src or top_src in chain:\n break\n chain.append(top_src)\n if top_src == MINT_ADDR:\n return {\"chain\": chain, \"reached_mint\": True, \"hops\": len(chain) - 1}, credits_total\n current = top_src\n return {\"chain\": chain, \"reached_mint\": False, \"hops\": len(chain) - 1}, credits_total\n\n\ndef _step5_temporal(ca: str, addr: str, listing_date: str,\n funder_first_ts: int) -> dict:\n \"\"\"Derive temporal pattern from funder timestamp + listing date.\n Free (no extra SQL — uses data from Step 2).\"\"\"\n if not funder_first_ts or not listing_date:\n return {\"tge_proximity_minutes\": None, \"interpretation\": \"unknown\"}\n try:\n from datetime import datetime, timezone, date\n # listing_date is YYYY-MM-DD; treat as UTC midnight (TGE actual time\n # may be later in the day — same-day window of ±24h covers that).\n listing_dt = datetime.combine(\n date.fromisoformat(listing_date), datetime.min.time(),\n tzinfo=timezone.utc,\n )\n funder_dt = datetime.fromtimestamp(funder_first_ts, tz=timezone.utc)\n delta_seconds = (funder_dt - listing_dt).total_seconds()\n delta_minutes = int(delta_seconds / 60)\n except Exception:\n return {\"tge_proximity_minutes\": None, \"interpretation\": \"unknown\"}\n interp = \"unknown\"\n # Thresholds: TGE listing date is given as date-only, so the actual TGE\n # event sits somewhere within ±24h of the date's UTC midnight. A \"TGE-\n # aligned\" event is anything from 24h before to 24h after the date,\n # which captures both pre-listing OTC settlements and same-day allocations.\n if -60 * 24 \u003c= delta_minutes \u003c= 60 * 24:\n interp = \"tge_aligned\" # 上线日 ±24h = insider (lump-sum 通常发生在此窗口)\n elif delta_minutes \u003c -60 * 24 * 7:\n interp = \"pre_launch_insider\" # 上线前 7+ 天 = 早 insider (Round-1 投资者等)\n elif delta_minutes \u003c -60 * 24:\n interp = \"pre_launch_recent\" # 上线前 1-7 天 (= 公告期 / TGE-day 准备期)\n else:\n interp = \"post_launch_acquisition\" # 上线后 > 1 天 (= 二级市场买入, 不是 allocation)\n return {\n \"tge_proximity_minutes\": delta_minutes,\n \"interpretation\": interp,\n }\n\n\ndef _step6_netflow_signature(total_in_tok: float, total_out_tok: float) -> dict:\n \"\"\"Net-flow shape inferred from Step 2 + Step 3 totals. Free (no SQL).\"\"\"\n if total_in_tok == 0 and total_out_tok == 0:\n return {\"shape\": \"no_activity\", \"out_in_ratio\": None}\n if total_in_tok == 0:\n return {\"shape\": \"send_only\", \"out_in_ratio\": float(\"inf\")}\n ratio = total_out_tok / total_in_tok\n shape = \"unknown\"\n if abs(ratio - 1.0) \u003c 0.001:\n shape = \"perfect_round_trip\" # = wash / atomic-pair routing\n elif ratio \u003c 0.1:\n shape = \"accumulating\" # tok 收下后基本没动 = dormant bag\n elif ratio > 10:\n shape = \"distributing\" # 卖出远超买入 (净卖出)\n elif 0.95 \u003c= ratio \u003c= 1.05:\n shape = \"mm_balanced\" # MM 1:1 双向 with small drift\n elif ratio \u003c 0.5:\n shape = \"net_holder\" # 卖了一部分, 持大头\n else:\n shape = \"net_seller\"\n return {\"shape\": shape, \"out_in_ratio\": ratio}\n\n\ndef _decide_role(s1: dict, s2: dict, s3: dict, s4: dict, s5: dict, s6: dict) -> dict:\n \"\"\"Combine 6 steps into a single role classification.\"\"\"\n is_contract = s1.get(\"is_contract\", False)\n size_tier = s1.get(\"size_tier\", \"EOA\")\n funder_pat = s2.get(\"pattern\", \"unknown\")\n recip_pat = s3.get(\"pattern\", \"unknown\")\n reached_mint = s4.get(\"reached_mint\", False)\n tge_interp = s5.get(\"interpretation\", \"unknown\")\n flow_shape = s6.get(\"shape\", \"unknown\")\n\n # MM bot path — contract in 15-50KB range with LP-dominant counterparties\n if is_contract and size_tier in (\"LITE_MM\", \"FULL_MM\", \"COMPLEX_AGGREGATOR\"):\n # wash infra: contract with perfect round-trip 1:1 flow\n if flow_shape == \"perfect_round_trip\":\n return {\"role\": \"wash_infra_member\", \"confidence\": 0.7,\n \"rationale\": f\"Contract with perfect round-trip flow {s6}\"}\n # active MM: balanced buy/sell against LP\n if flow_shape == \"mm_balanced\" and recip_pat == \"single_destination\":\n return {\n \"role\": \"dex_mm_bot\",\n \"confidence\": 0.85,\n \"rationale\": (\n f\"Contract {size_tier} ({s1.get('code_bytes')} bytes), \"\n f\"balanced 1:1 flow vs single LP destination\"\n ),\n }\n # MM unwinding: was MM (LP-routed), now net-selling — common late-stage\n if (flow_shape in (\"net_seller\", \"distributing\")\n and recip_pat == \"single_destination\"):\n return {\n \"role\": \"dex_mm_bot_unwinding\",\n \"confidence\": 0.8,\n \"rationale\": (\n f\"Contract {size_tier} ({s1.get('code_bytes')} bytes), \"\n f\"net-selling to single LP destination \"\n f\"(out/in ratio {s6.get('out_in_ratio'):.2f}) — MM winding down\"\n ),\n }\n\n # Insider allocation path (EOA + lump sum + reached mint + TGE-aligned + Alpha exit)\n if (not is_contract\n and funder_pat in (\"single_lump\", \"single_funder_few_tx\")\n and reached_mint\n and tge_interp in (\"tge_aligned\", \"pre_launch_recent\", \"pre_launch_insider\")\n and recip_pat == \"alpha_router_dominant\"):\n return {\n \"role\": \"insider_allocation_holder\",\n \"confidence\": 0.9,\n \"rationale\": (\n f\"EOA + single lump from {s2.get('top_funder','?')[:12]} \"\n f\"({s5.get('tge_proximity_minutes')} min from TGE), \"\n f\"upstream {s4.get('hops')}-hop to mint, \"\n f\"exits via Alpha Router\"\n ),\n }\n\n # Active multi-token allocator (cross-token activity needed; deferred — set by caller)\n # This branch returns insider but flags for caller upgrade based on cross-token check.\n\n # Insider but didn't sell via Alpha router (might hold long-term)\n if (not is_contract\n and funder_pat in (\"single_lump\", \"single_funder_few_tx\")\n and reached_mint\n and tge_interp in (\"tge_aligned\", \"pre_launch_recent\")):\n return {\n \"role\": \"insider_allocation_holder\",\n \"confidence\": 0.75,\n \"rationale\": (\n f\"EOA + single lump near TGE + upstream-to-mint, but exit \"\n f\"pattern={recip_pat} (not Alpha-router dominant)\"\n ),\n }\n\n # Retail / late buyer\n if (not is_contract\n and not reached_mint\n and flow_shape in (\"accumulating\", \"net_holder\")):\n return {\n \"role\": \"retail_holder\",\n \"confidence\": 0.6,\n \"rationale\": (\n f\"EOA + multi-source funder + upstream stops outside mint chain \"\n f\"({s4.get('hops')} hops), holding {flow_shape}\"\n ),\n }\n\n # Contract but small / unknown — proxy or aggregator\n if is_contract and size_tier == \"PROXY_OR_SIMPLE\":\n return {\n \"role\": \"unknown_contract_small\",\n \"confidence\": 0.4,\n \"rationale\": f\"Small contract ({s1.get('code_bytes')} bytes), pattern inconclusive\",\n }\n\n # Default: unknown\n return {\n \"role\": \"unknown\",\n \"confidence\": 0.3,\n \"rationale\": (\n f\"Signals: contract={is_contract}/{size_tier} funder={funder_pat} \"\n f\"recip={recip_pat} mint={reached_mint} tge={tge_interp} flow={flow_shape}\"\n ),\n }\n\n\ndef classify(addr: str, ca: str, listing_date: str | None = None) -> tuple[dict, int]:\n \"\"\"Classify one whale candidate. Returns (result, credits_used).\n\n `result` shape:\n {\n \"role\": str,\n \"confidence\": float,\n \"rationale\": str,\n \"evidence\": {\n \"step1_contract\": {...},\n \"step2_funder\": {...},\n \"step3_recipient\": {...},\n \"step4_upstream\": {...},\n \"step5_temporal\": {...},\n \"step6_netflow\": {...},\n },\n \"_credits_used\": int,\n }\n \"\"\"\n a = (addr or \"\").lower()\n c = (ca or \"\").lower()\n if not _addr_ok(a):\n raise RoleClassifierError(f\"invalid addr: {addr!r}\")\n if not _addr_ok(c):\n raise RoleClassifierError(f\"invalid ca: {ca!r}\")\n if not listing_date:\n listing_date = \"2025-01-01\" # safe fallback\n # Cross-LLM security audit fix: use fullmatch — `re.match` lets ` hertzflow — Skillopedia \n # match before a trailing `\\n`, allowing newline-suffix SQL injection\n # via the listing_date interpolated into f-string SQL below.\n if not re.fullmatch(r\"\\d{4}-\\d{2}-\\d{2}\", listing_date):\n raise RoleClassifierError(f\"invalid listing_date: {listing_date!r}\")\n\n credits = 0\n\n # Step 1 — free RPC\n s1 = _step1_eoa_vs_contract(a)\n\n # Steps 2 + 3 — 1 SQL each\n s2, c2 = _step2_funders(c, a, listing_date)\n credits += c2\n s3, c3 = _step3_recipients(c, a, listing_date)\n credits += c3\n\n # Step 4 — up to 3 SQL (max_hops default)\n s4, c4 = _step4_upstream_chain(c, a, max_hops=3)\n credits += c4\n\n # Steps 5 + 6 — derived, free\n s5 = _step5_temporal(c, a, listing_date,\n funder_first_ts=s2.get(\"top_funder_first_ts\") or 0)\n s6 = _step6_netflow_signature(\n total_in_tok=s2.get(\"total_in_tok\") or 0,\n total_out_tok=s3.get(\"total_out_tok\") or 0,\n )\n\n decision = _decide_role(s1, s2, s3, s4, s5, s6)\n decision[\"evidence\"] = {\n \"step1_contract\": s1,\n \"step2_funder\": s2,\n \"step3_recipient\": s3,\n \"step4_upstream\": s4,\n \"step5_temporal\": s5,\n \"step6_netflow\": s6,\n }\n decision[\"_credits_used\"] = credits\n return decision, credits\n\n\ndef main() -> int:\n import argparse\n ap = argparse.ArgumentParser()\n ap.add_argument(\"--addr\", required=True)\n ap.add_argument(\"--ca\", required=True)\n ap.add_argument(\"--listing-date\", default=None)\n args = ap.parse_args()\n try:\n result, credits = classify(args.addr, args.ca, listing_date=args.listing_date)\n except RoleClassifierError as e:\n print(f\"ERROR: {e}\", file=sys.stderr)\n return 1\n print(json.dumps(result, indent=2, ensure_ascii=False, default=str))\n print(f\"[surf credits: {credits}]\", file=sys.stderr)\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":19242,"content_sha256":"680c6e9cf10fb4db4c7dcbef145e593acf689392bec6ec9489d8668f5766b77e"},{"filename":"alpha/v06/render_report.py","content":"#!/usr/bin/env python3\n\"\"\"v0.6 deterministic report renderer.\n\nReads:\n --skeleton Pipeline output (locked + writable placeholders)\n --filled LLM-filled JSON (writable placeholders replaced)\n\nRuns validate_report_data.py FIRST (mandatory, no opt-out). On pass,\nrenders the jinja2 template producing a complete report.md.\n\nInherits v0.5 security hardening verbatim:\n- path-traversal / symlink / hardlink guards on out_path\n- regular-file check on data paths (FIFO/socket DoS block)\n- unpaired-surrogate rejection (UTF-8 round-trip)\n- max nesting depth (64) — adversarial JSON DoS\n- md_cell pipe + newline escape\n- html_escape_finalize — every `{{ ... }}` HTML-entity + markdown-link escape\n- mermaid label/id escape — neutralize `\"`, `|`, `\u003c>`, `;`, `-->`, etc.\n\nv0.6 schema differences from v0.5:\n- evidence_graph (new top-level)\n- decision_action_block (new structured immediate_action / stop_loss /\n re_entry_conditions; replaces v0.5 freeform \"action narrative\")\n- holdings_distribution.role_rows now uses role/role_label/n_wallets/\n total_balance/pct_of_total/top_addr_short (no buy/sell columns;\n Phase C+ may add)\n- lineage.m6.rows uses addr_short/dumped_pct (no identity/net_buy/exit_pct)\n- anomaly.waves uses pipeline-built waves_proposal shape\n- v0.5's convergence_hubs / preparation_phase / multihop_verification /\n g_section_notes / hub_detection NOT YET in v0.6 — template skips them\n with `{% if %}` guards (Phase C+ re-introduction)\n\nv0.6 (2026-05-24, Phase D)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom jinja2 import Environment, StrictUndefined\n\nsys.path.insert(0, str(Path(__file__).parent))\nsys.path.insert(0, str(Path(__file__).parent / \"helpers\"))\nfrom validate_report_data import Validator\nimport monitoring_export\nfrom i18n import t, set_lang, get_lang # v0.6.2 i18n\n\n\nTEMPLATE = \"\"\"# {{ t(\"report.title\", symbol=meta.symbol, name=meta.name) }}\n\n{# v0.7.24d: 🎯 retail summary section — 4-5 lines of plain Chinese at the\n very top of the report. Neutral tone, no financial advice (no \"建议卖出\"\n or \"不要买\"). Derives all numbers from skeleton data — no hard-coded\n recommendations. For LLM-to-LLM consumption + LLM-to-retail explanation\n flow, this gives the AI a clean Chinese summary to read first before\n diving into English-jargon-heavy detail sections. #}\n{% set _fa_for_summary = funding_attribution if funding_attribution is defined else None -%}\n{% set _mfo_for_summary = (_fa_for_summary.mining_fed_outflows if _fa_for_summary and _fa_for_summary.mining_fed_outflows is defined else None) -%}\n{% set _mad_for_summary = (_fa_for_summary.mint_authority_dumps if _fa_for_summary and _fa_for_summary.mint_authority_dumps is defined else None) -%}\n{% set _mc_for_summary = (_fa_for_summary.multi_chain if _fa_for_summary and _fa_for_summary.multi_chain is defined else None) -%}\n{% set _htd_for_summary = (_fa_for_summary.high_throughput_dumpers if _fa_for_summary and _fa_for_summary.high_throughput_dumpers is defined else None) -%}\n{% set _captured_usd_total = (\n (_mfo_for_summary.summary.total_dex_sold_usd if _mfo_for_summary and _mfo_for_summary.summary else 0) +\n (_mad_for_summary.summary.total_dex_sold_usd if _mad_for_summary and _mad_for_summary.summary else 0)\n) -%}\n{# v0.8.6.6: dump_tracking_mining fallback aliased to dump_tracking.\n Mining fallback insider = m6 + mint_authority + cluster + ht + fanout +\n wcg cluster ≈ 100-200 wallets vs m6 only ≈ 0-50. When mining mode catches\n bigger net_sellout (cluster-heavy / cross-chain / mining tokens), use it.\n BEAT m6=$233K → mining=$37M (159x). JCT m6=$0 → mining=$7.8M. #}\n{%- set _dtm = (dump_tracking_mining if (dump_tracking_mining is defined and dump_tracking_mining) else None) -%}\n{%- set _dtm_net = (_dtm.get('confirmed_net_sellout_usd') or 0) if _dtm else 0 -%}\n{%- set _dt_base = (dump_tracking if (dump_tracking is defined and dump_tracking) else None) -%}\n{%- set _dt_base_net = (_dt_base.get('confirmed_net_sellout_usd') or 0) if _dt_base else 0 -%}\n{%- if _dtm and _dtm_net > _dt_base_net and not _dtm.get('_error') -%}\n {%- set dump_tracking = _dtm -%}\n{%- endif -%}\n{% set _captured_tokens_total = (\n (_mfo_for_summary.summary.total_dex_sold_tokens if _mfo_for_summary and _mfo_for_summary.summary else 0) +\n (_mad_for_summary.summary.total_dex_sold_tokens if _mad_for_summary and _mad_for_summary.summary else 0)\n) -%}\n{% set _captured_pct_supply = ((_captured_tokens_total / meta.total_supply * 100) if (meta.total_supply and _captured_tokens_total) else 0) -%}\n{% set _mc_chains_summary = [] -%}\n{% if _mc_for_summary -%}\n {% for _chain_name, _mc_data in _mc_for_summary.items() -%}\n {% if _mc_data.get('mint_authorities') or _mc_data.get('high_throughput_dumpers') -%}\n {% set _ = _mc_chains_summary.append(_chain_name) -%}\n {%- endif -%}\n {%- endfor -%}\n{%- endif -%}\n{% set _has_summary_data = _fa_for_summary and (_captured_usd_total > 0 or _mc_chains_summary or (_htd_for_summary and _htd_for_summary.summary and _htd_for_summary.summary.n_dumpers > 0)) -%}\n{% if _has_summary_data %}\n\n{# v0.8.7.0: ## 0. 一屏结论 段 — 6 维度 deterministic TL;DR.\n ChatGPT review (2026-06-12): \"第一屏要压成阶段/筹码/最近动 3 个判断\".\n helpers/screen_summary.py 算出 6 维度 + 1 句话 deterministic, 这里 render. #}\n{% if screen_summary is defined and screen_summary\n and (screen_summary.get('dimensions') or []) | length >= 6 %}\n## {{ t(\"render.screen_summary_h2\") }}\n\n> {{ t(\"render.screen_summary_intro\") }}\n\n| {{ t(\"render.screen_summary_col_dim\") }} | {{ t(\"render.screen_summary_col_conclusion\") }} | {{ t(\"render.screen_summary_col_evidence\") }} |\n|---|---|---|\n{% for _d in (screen_summary.dimensions or []) -%}\n| **{{ _d.name }}** | {{ _d.label }} | {{ _d.evidence }} |\n{% endfor %}\n\n**{{ t(\"render.one_sentence_label\") }}**: {{ screen_summary.one_sentence }}\n\n{% endif %}\n## {{ t(\"render.tldr_summary_h2\") }}\n\n> {{ t(\"render.tldr_summary_intro\") }}\n\n- **{{ t(\"render.tldr_label_project\") }}**: {{ meta.name }} ({{ meta.symbol }}){% if meta.contract_address %}, {{ t(\"render.tldr_main_contract\") }} [`{{ meta.contract_address[:10] }}…`]({{ explorer_url(meta.contract_address) }}){% endif %}\n\n- **{{ t(\"render.tldr_label_listing\") }}**: {{ t(\"render.tldr_binance_alpha\") }} {{ tier_classification.tier }}{% if tier_classification.tier == \"S2\" %} {{ t(\"render.tldr_tier_s2_paren\") }}{% elif tier_classification.tier == \"S3\" %} {{ t(\"render.tldr_tier_s3_paren\") }}{% elif tier_classification.tier == \"S1\" %} {{ t(\"render.tldr_tier_s1_paren\") }}{% endif %} · {{ t(\"render.tldr_primary_chain\", chain=meta.chain) }}\n\n{# v1.2.9 (product spec): 近 72h 分发/归集动作 = 速读最高优先级, 复用 screen_summary dim 的 label+evidence.\n v1.2.13 (product spec 2026-07-01, TAC): also surface the 🟠 SUSPECTED_BATCH_DISTRIBUTION\n medium tier (unknown-hub fan-out) here — it was already in the 一屏结论 dim table\n but the 速读摘要 bullet only fired for confirmed operator/CEX, so the 88-wallet\n TAC finding was invisible to a reader who only scans the 速读摘要. #}\n{% set _rf = recent_flow_actions | default({}) -%}\n{% if _rf and (_rf.get('has_operator_fanout') or _rf.get('has_cex_consolidation') or _rf.get('has_unknown_fanout')) -%}\n{% for _d in (screen_summary.dimensions or []) -%}{% if _d.get('_state') in ('OPERATOR_FANOUT','CEX_CONSOLIDATION','FANOUT_AND_CONSOLIDATION','SUSPECTED_BATCH_DISTRIBUTION') -%}\n- {{ _d.label }} — {{ _d.evidence }}\n{% endif %}{% endfor -%}\n{% endif %}\n{# v1.2.14 (product spec 2026-07-01, TAC): 近期铸新 (fresh operator ammo) also surfaces in the\n 速读, mirroring the 一屏结论 recent-mint row. #}\n{% set _rme = recent_mint_events | default({}) -%}\n{% if _rme and _rme.get('has_recent_significant_mint') -%}\n{% for _d in (screen_summary.dimensions or []) -%}{% if _d.get('_state') == 'RECENT_MINT' -%}\n- {{ _d.label }} — {{ _d.evidence }}\n{% endif %}{% endfor -%}\n{% endif %}\n\n{% if dump_tracking and (dump_tracking.confirmed_total_tokens or 0) > 0 -%}\n- {{ t(\"render.tldr_confirmed_realization\", tokens=\"{:,.0f}\".format(dump_tracking.confirmed_total_tokens), pct=\"%.2f\"|format(dump_tracking.confirmed_total_pct or 0), usd=\"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd or 0)) }}\n{%- endif %}\n{% if _captured_tokens_total > 0 -%}\n- **{{ t(\"render.tldr_tge_sold_label\") }}**: **{{ \"{:,.0f}\".format(_captured_tokens_total) }} tokens**{% if _captured_usd_total > 0 %} ≈ **${{ \"{:,.0f}\".format(_captured_usd_total) }} USD**{% endif %}{% if _captured_pct_supply > 0 %}{% if _captured_pct_supply > 100 %} {{ t(\"render.tldr_tge_pct_over100\", pct=\"%.0f\"|format(_captured_pct_supply)) }}{% else %} {{ t(\"render.tldr_tge_pct_normal\", pct=\"%.2f\"|format(_captured_pct_supply)) }}{% endif %}{% endif %} — {{ t(\"render.tldr_tge_via\") }} {% if _mfo_for_summary and _mfo_for_summary.summary and _mfo_for_summary.summary.n_addrs_with_dex_sells > 0 %}{{ t(\"render.tldr_tge_via_mining\", n=_mfo_for_summary.summary.n_addrs_with_dex_sells) }}{% endif %}{% if (_mfo_for_summary and _mfo_for_summary.summary and _mfo_for_summary.summary.n_addrs_with_dex_sells > 0) and (_mad_for_summary and _mad_for_summary.summary and _mad_for_summary.summary.n_addrs_with_dex_sells > 0) %} + {% endif %}{% if _mad_for_summary and _mad_for_summary.summary and _mad_for_summary.summary.n_addrs_with_dex_sells > 0 %}{{ t(\"render.tldr_tge_via_mint\", n=_mad_for_summary.summary.n_addrs_with_dex_sells) }}{% endif %} {{ t(\"render.tldr_tge_back_to_market\") }}\n{%- endif %}\n\n{% if _htd_for_summary and _htd_for_summary.summary and _htd_for_summary.summary.n_dumpers > 0 %}\n- {{ t(\"render.tldr_ht_operators\", n=_htd_for_summary.summary.n_dumpers, tokens=\"{:,.0f}\".format(_htd_for_summary.summary.total_throughput)) }}\n\n{% endif %}\n{% if _mc_chains_summary %}\n- {{ t(\"render.tldr_cross_chain\", chains=(_mc_chains_summary | join(\", \"))) }}\n\n{% endif %}\n- {{ t(\"render.tldr_completeness_note\") }}\n\n- {{ t(\"render.tldr_how_to_use\") }}\n\n{% endif %}\n{# v0.7.9.1: narrative_warnings banner removed from user-facing report.\n These are validator NARRATIVE_QUALITY soft warnings (e.g. numeric\n hallucination in templated fill, narrative duplication). They're\n useful for dev validation but pure noise for retail traders reading\n the report. Still emitted to stderr by render_report for debugging.\n#}\n{% if meta.data_freshness is defined and meta.data_freshness and meta.data_freshness.warning %}\n> 📌 **{{ t(\"report.banner_data_freshness_title\") }}**: {{ t(\"report.banner_data_freshness_body\", lag_hours=meta.data_freshness.lag_hours, latest_date=meta.data_freshness.latest_surf_date_utc) }}\n{% endif %}\n{# v0.7.21.8: skip the \"BSC 镜像端 → 主战场\" 提示条 on 持币人-snapshot\n chains (Solana). That banner claims pipeline already routes all\n forensic SQL to the primary chain, which is true on EVM but a lie on\n Solana — the SQL detectors are skipped, not routed. The dedicated\n HOLDER_SNAPSHOT banner below carries the correct copy. #}\n{% if meta.primary_chain is defined and meta.primary_chain and meta.primary_chain != \"binance-smart-chain\" and not meta.get('_holder_snapshot_mode') %}\n> 📌 **{{ t(\"report.banner_non_bsc_primary_title\") }}**: {{ t(\"report.banner_non_bsc_primary_body\", primary_chain=meta.primary_chain, scope_chains=(meta.coingecko_platforms|default({})|list|reject('eq','binance-smart-chain')|list|join(\", \")) ) }}\n{% endif %}\n{# v1.2.10 (TAC 2026-06-29): the v1.2.6 coingecko-based off_coverage \"mirror slice\"\n banner was REMOVED. 主战场 (main chain) is decided by surf real-time LP via\n derive_primary_chain + alpha_chain_authoritative — Alpha IS the BSC market — NOT\n by CoinGecko asset_platform metadata. The banner fired \"TON/Solana is the real\n chain, BSC is just a mirror\" purely because CoinGecko listed a non-EVM platform,\n ignoring that BSC (Alpha) held e.g. 38× the DEX volume of the TON side. That\n contradicted the skill's own primary-chain SOP and mislabelled the report. #}\n{% if meta.primary_chain_derivation is defined and meta.primary_chain_derivation and \"unreliable_fetch_failed\" in meta.primary_chain_derivation %}\n> {{ t(\"render.banner_primary_chain_unreliable\", failed_chain=meta.primary_chain_derivation.split(\":\")[1]) }}\n{% endif %}\n{% set _rti_banner = meta.realtime_token_info | default({}) %}\n{# v0.7.16: 只要任何一个 source 拿到了价格 (surf project-detail 或 Alpha API),\n 下方的\"代币行情\"表就照常显示, 不再弹 \"实时行情未就绪\" 警告. 仅当两边都没价格\n (surf NOT_FOUND + alpha API 也没收录) 才提示用户回去 BscScan 手动核对. #}\n{% if _rti_banner and _rti_banner.get('fetch_ok') == false and meta.get('alpha_price_usd') is none %}\n> {{ t(\"render.banner_realtime_not_ready\") }}\n{% endif %}\n{# v0.7.21.8: Solana / 任何无 surf onchain-sql 覆盖的链顶部一次性 提示条.\n pre-v0.7.21.8 这些段都显示 \"0 命中\" 让用户误以为 链上侦测 通过, 实际是 SQL\n 层根本没数据. 现在明文告诉用户哪些 链上侦测 段是 chain 限制不是 检测器 结果. #}\n{% if meta.get('_holder_snapshot_mode') %}\n\n> {{ t(\"render.banner_holder_snapshot_line1\", chain=meta.chain) }}\n> {{ t(\"render.banner_holder_snapshot_line2\", primary_chain=meta.primary_chain) }}\n> {{ t(\"render.banner_holder_snapshot_line3\") }}\n> {{ t(\"render.banner_holder_snapshot_skip_intro\") }}\n> - {{ t(\"render.banner_holder_snapshot_skip_1\") }}\n> - {{ t(\"render.banner_holder_snapshot_skip_2\") }}\n> - {{ t(\"render.banner_holder_snapshot_skip_3\") }}\n> - {{ t(\"render.banner_holder_snapshot_skip_4\") }}\n> - {{ t(\"render.banner_holder_snapshot_skip_5\") }}\n>\n> {{ t(\"render.banner_holder_snapshot_kept\") }}\n>\n> {{ t(\"render.banner_holder_snapshot_manual\") }}\n{% endif %}\n{# v0.7.25 unified top callout: 之前 wash_dominated (🚨) + mining-model (⚪)\n + completeness (🟡) 三个独立 提示条 叠在 report 顶部, 散户 直接被劝退,\n 每条都要 scroll 看. 现在合并成 1 个统一 \"链上侦测限制 / 数据提醒\"\n callout, 每条 1 行 + 链接到详细段 锚点. data quality (surf-failure) 提示条\n 保持独立, 因为它是 transient surf 问题不是 token 结构.\n\n 3 conditions:\n wash_dominated → 24H vol 被 对敲机器人主导, 不可信\n mining_model → 合约部署地址 placeholder, 标准 内幕 集合空 (非 surf 失败)\n completeness_low → 跨链 / 跨链桥 / CEX 提币阶段 出货 路径结构性不可见\n\n v0.7.23.2 fix preserved: distinguish \"rule_11 m6 empty (mining-token model,\n no insider set to query, NOT a surf failure)\" from \"surf bucket queries\n actually failed\". Real mining tokens have a placeholder deployer detected. #}\n{# v0.8.4.9.4: 加 mint_authorities 实证 condition.\n 之前 AOP cold-start surf 失败 → m6 trace 空 → 误判矿币模式 (实际是数据残缺).\n 真矿币必有 mint_authorities.authorities 非空 (JCT 2 authorities). #}\n{%- set _has_mint_authorities = (funding_attribution is defined and funding_attribution\n and ((funding_attribution.get('mint_authorities') or {}).get('authorities') or [])\n | selectattr('is_excluded', 'equalto', false) | list | length > 0) -%}\n{% set _dq_mining_model = (\n lineage is defined and lineage and (lineage.get('m6', {}).get('rows')|length == 0)\n and lineage.get('deployer_addr')\n and lineage.get('_status') != 'no_deployer_anchor'\n and dump_tracking is defined and dump_tracking\n and dump_tracking.get('buckets_complete') == false\n and (dump_tracking.get('insider_n_wallets') or 0) == 0\n and _has_mint_authorities\n) -%}\n{# v0.8.5.2: banner 分级 — adversarial review CLO bug report 2026-06-12:\n push_airdrop_detector 子 SQL fail (max_rows>10K cap) 不该让整个\n dump_tracker 段报\"不可信\". 真核心 CEX/DEX confirmed 数据完整时, fail\n 只是辅助 detector. 用 confirmed_total_tokens > 0 / tree_holds > 0\n 信号判断核心数据可用 → suppress banner.\n 仍触发 case: insider_n_wallets=0 + 全 0 (真 surf 系统性失败 case). #}\n{%- set _has_core_dump_data = (dump_tracking is defined and dump_tracking and (\n (dump_tracking.get('confirmed_total_tokens') or 0) > 0\n or (dump_tracking.get('tree_holds_tokens') or 0) > 0\n or (dump_tracking.get('insider_n_wallets') or 0) > 0\n)) -%}\n{% set _dq_dump_bad = (\n dump_tracking is defined and dump_tracking\n and dump_tracking.get('buckets_complete') == false\n and not _has_core_dump_data\n and not _dq_mining_model\n) -%}\n{# v0.8.4.9.4: 矿币模式 — 内幕变现 detector 当前不 quantify mint_authority +\n fake_mining cluster 的 net sell, 显示 $0 误导. Banner 提醒用户看 high_throughput\n 段实际 dumper 数 + 链上链路. #}\n{% set _dq_mining_no_sellout = (_dq_mining_model\n and dump_tracking is defined and dump_tracking\n and (dump_tracking.get('confirmed_net_sellout_usd') or 0) == 0\n and funding_attribution is defined and funding_attribution\n and ((funding_attribution.get('high_throughput_dumpers') or {}).get('summary') or {}).get('n_dumpers')) -%}\n{% set _dq_wash_bad = (wash_infrastructure is defined and wash_infrastructure and wash_infrastructure.get('_truncated')) -%}\n{% set _dq_flow_bad = (\n flow_operators is defined and flow_operators\n and (flow_operators.get('_credits_used') or 0) == 0\n and (flow_operators.get('_n_candidates_scanned') or 0) > 0\n and not flow_operators.get('operators')\n and flow_operators.get('_skip_reason') != 'surf_no_sql_solana'\n) -%}\n{% set _wash_dominated = (dump_tracking is defined and dump_tracking and dump_tracking.get('wash_dominated')) -%}\n{% set _completeness_low = (\n dump_tracking is defined and dump_tracking\n and dump_tracking.get('wash_dominated') == true\n and (\n (lineage is defined and lineage and (lineage.get('m6', {}).get('rows')|length == 0))\n or (dump_tracking.get('insider_n_wallets') or 0) \u003c 3\n )\n) -%}\n{% set _has_top_callout = (_wash_dominated or _dq_mining_model or _completeness_low) -%}\n{% if _has_top_callout %}\n\n> {{ t(\"render.callout_must_read_title\") }}\n>\n{% if _wash_dominated -%}\n> - {{ t(\"render.callout_wash_dominated\", n_sellers=(dump_tracking.n_dex_sellers or 0), total_swaps=\"{:,}\".format(dump_tracking.total_dex_swaps or 0), top_swaps=\"{:,}\".format(dump_tracking.top_seller_swaps or 0)) }}{% if dump_tracking.total_dex_swaps and dump_tracking.total_dex_swaps > 0 %} {{ t(\"render.callout_wash_dominated_share\", share=\"%.1f\"|format((dump_tracking.top_seller_swaps or 0) / dump_tracking.total_dex_swaps * 100)) }}{% endif %}. {{ t(\"render.callout_wash_dominated_tail\") }}\n{% endif -%}\n{% if _dq_mining_model -%}\n> - {{ t(\"render.callout_mining_model\") }}\n{% endif -%}\n{% if _completeness_low -%}\n> - {{ t(\"render.callout_completeness_low\") }}\n{% endif -%}\n{% endif %}{% if _dq_dump_bad or _dq_wash_bad or _dq_flow_bad %}\n\n> {{ t(\"render.callout_data_incomplete_title\") }}\n>\n{% if _dq_dump_bad -%}\n> - {{ t(\"render.callout_dump_bad\") }}\n{% endif -%}\n{% if _dq_wash_bad -%}\n> - {{ t(\"render.callout_wash_bad\", budget=(wash_infrastructure._truncation_meta.wall_clock_budget_seconds | int), processed=wash_infrastructure._truncation_meta.n_candidates_processed, total=wash_infrastructure._truncation_meta.n_candidates_total) }}\n{% endif -%}\n{% if _dq_flow_bad -%}\n> - {{ t(\"render.callout_flow_bad\", n=flow_operators._n_candidates_scanned) }}\n{% endif -%}\n>\n> {{ t(\"render.callout_data_incomplete_advice\") }}\n{% endif %}\n_{{ t(\"report.meta_line_1\", version=_schema_version, chain=meta.chain, listing_date=meta.alpha_listing_date_utc) }}_\n\n{% if meta.total_supply -%}\n_{{ t(\"report.meta_line_2\", total_supply=meta.total_supply, circ_supply=meta.circulating_supply or 0, circ_pct=(meta.circ_ratio * 100) if meta.circ_ratio else 0, token_type=meta.token_type_initial) }}_\n{%- else -%}\n_{{ t(\"report.meta_line_2_unknown\", token_type=meta.token_type_initial) }}_\n{% endif %}\n\n{% if tier_classification.s3_date -%}\n_{{ t(\"report.meta_line_tier_with_s2_s3\", tier=tier_classification.tier, s1_date=tier_classification.s1_date or \"—\", s2_date=tier_classification.s2_date, s3_date=tier_classification.s3_date) }}_\n{% elif tier_classification.s2_date -%}\n_{{ t(\"report.meta_line_tier_with_s2\", tier=tier_classification.tier, s1_date=tier_classification.s1_date or \"—\", s2_date=tier_classification.s2_date) }}_\n{%- else -%}\n_{{ t(\"report.meta_line_tier\", tier=tier_classification.tier, s1_date=tier_classification.s1_date or \"—\") }}_\n{% endif %}\n\n{% set rti = meta.realtime_token_info | default({}) %}\n{# v0.7.16: fallback chain on every field — surf project-detail (rti) is\n primary, Binance Alpha token-list (meta.alpha_*) is the fallback. The\n table renders if EITHER source has a price; previously a single rti\n failure (e.g. surf NOT_FOUND on GUA, 3 months listed but un-indexed)\n hid the whole header. Adds the project NAME, primary chain, and LP\n columns the user asked for so retail-trader readers see \"what is this\n token / what's it priced / how active\" at a glance. #}\n{# adversarial review HIGH#1: prefer `is not none` over truthiness — vol/LP/mcap can be a\n legitimate 0 on a freshly-listed / illiquid token, and treating 0 as\n missing would wrongly drop it and silently switch to the Alpha API value. #}\n{% set _price_usd = rti.get('price_usd') if rti.get('price_usd') is not none else meta.get('alpha_price_usd') %}\n{% set _price_chg = rti.get('price_change_24h_pct') if rti.get('price_change_24h_pct') is not none else meta.get('alpha_percent_change_24h') %}\n{% set _vol_24h = rti.get('volume_24h_usd') if rti.get('volume_24h_usd') is not none else meta.get('alpha_vol_24h_usd') %}\n{% set _lp_usd = rti.get('liquidity_usd') if rti.get('liquidity_usd') is not none else meta.get('alpha_liquidity_usd') %}\n{% set _mcap = rti.get('market_cap_usd') if rti.get('market_cap_usd') is not none else meta.get('alpha_market_cap_usd') %}\n{% set _fdv = rti.get('fdv_usd') if rti.get('fdv_usd') is not none else meta.get('alpha_fdv_usd') %}\n{# adversarial review MEDIUM#1: per-field source flag — price may be from surf while vol\n is from Alpha (or vice versa); previous `_src` only looked at price and\n mislabeled mixed rows. Use a Jinja namespace because `{% set %}` inside\n `{% for %}` does NOT mutate outer-scope variables (loop-local scoping). #}\n{% set _ns = namespace(surf=0, alpha=0) %}\n{% for _key_pair in [('price_usd','alpha_price_usd'), ('volume_24h_usd','alpha_vol_24h_usd'), ('liquidity_usd','alpha_liquidity_usd'), ('market_cap_usd','alpha_market_cap_usd'), ('fdv_usd','alpha_fdv_usd')] -%}\n{%- if rti.get(_key_pair[0]) is not none -%}{% set _ns.surf = _ns.surf + 1 -%}{% elif meta.get(_key_pair[1]) is not none -%}{% set _ns.alpha = _ns.alpha + 1 -%}{%- endif -%}\n{%- endfor -%}\n{% set _src = ('surf+Alpha API' if (_ns.surf > 0 and _ns.alpha > 0) else ('surf' if _ns.surf > 0 else ('Alpha API' if _ns.alpha > 0 else 'none'))) %}\n{% if _price_usd is not none %}\n## {{ t(\"render.market_h2\") }}\n\n| {{ t(\"render.market_col_item\") }} | {{ t(\"render.market_col_value\") }} |\n|---|---|\n| **{{ t(\"render.market_project_name\") }}** | **{{ meta.name | md_cell }}** ({{ meta.symbol | md_cell }}) |\n| Ticker | `{{ meta.symbol | md_cell }}` |\n| {{ t(\"render.market_primary_chain\") }} | {{ meta.chain | md_cell }}{% if meta.get('primary_chain') and meta.get('primary_chain') != \"binance-smart-chain\" %} → {{ t(\"render.market_actual_primary\") }} **{{ meta.get('primary_chain') | md_cell }}**{% if meta.get('_holder_snapshot_mode') %} (HOLDER_SNAPSHOT mode){% else %} {{ t(\"render.market_bsc_mirror\") }}{% endif %}{% endif %} |\n| {{ t(\"render.market_listing\") }} | {{ tier_classification.tier }} ({% if tier_classification.tier == \"S1\" %}{{ t(\"render.market_tier_s1\") }}{% elif tier_classification.tier == \"S2\" %}{{ t(\"render.market_tier_s2\") }}{% elif tier_classification.tier == \"S3\" %}{{ t(\"render.market_tier_s3\") }}{% else %}—{% endif %}) · {{ t(\"render.market_alpha_listed\") }} {{ tier_classification.s1_date or \"—\" }}{% if tier_classification.s2_date %} · {{ t(\"render.market_perp_listed\") }} {{ tier_classification.s2_date }}{% endif %} |\n| **{{ t(\"render.market_current_price\") }}** | **${{ \"%.4g\" | format(_price_usd) }}** ({% if _price_chg is not none and _price_chg \u003c 0 %}🔴 {{ \"%.2f\" | format(_price_chg) }}%{% elif _price_chg is not none and _price_chg > 0 %}🟢 +{{ \"%.2f\" | format(_price_chg) }}%{% else %}—{% endif %} 24H) |\n| **{{ t(\"render.market_vol_24h\") }}** | {% if _vol_24h is not none %}**${{ \"{:,.0f}\".format(_vol_24h) }}**{% else %}—{% endif %} |\n| {{ t(\"render.market_lp\") }} | {% if _lp_usd is not none %}${{ \"{:,.0f}\".format(_lp_usd) }}{% else %}—{% endif %} |\n| {{ t(\"render.market_mcap_fdv\") }} | {% if _mcap is not none %}${{ \"{:,.0f}\".format(_mcap) }}{% else %}—{% endif %} / {% if _fdv is not none %}${{ \"{:,.0f}\".format(_fdv) }}{% else %}—{% endif %} |\n| {{ t(\"render.market_data_source\") }} | {{ _src }} ({{ t(\"render.market_realtime\") }}){% if meta.get('alpha_holders') is not none %} · {{ t(\"render.market_holders\", n=\"{:,.0f}\".format(meta.get('alpha_holders'))) }}{% endif %}{% if meta.get('alpha_count_24h') is not none %} · {{ t(\"render.market_count_24h\", n=\"{:,.0f}\".format(meta.get('alpha_count_24h'))) }}{% endif %} |\n\n{% endif %}\n{# v0.7.22 P0 #2 (v0.7.25 中性化): 渲染端 判定 5 档 + risk score (1-10) 推导.\n Moved OUTSIDE the `{% if decision_summary %}` block (was inside in\n the first cut) — conclusion section below references `_v.subtype` and\n `_v.risk` unconditionally, and a skeleton without decision_summary\n (legacy / smoke fixtures) made jinja2 StrictUndefined raise. By\n computing _v in the outer scope with safe defaults for every input,\n both the decision-summary table AND the conclusion header can read\n it without scope leaks.\n 工作流 现 判定 3 档把 3 个不同时态信号 OR 合一全映射 EXIT_IF_HOLDING,\n 90% Alpha 币掉同一档 → 无区分度. 本段从 raw signals 推 5 档子类 + 1-10 风险评分\n 不动 工作流 schema.\n v0.7.25: 标签从 EXIT_* (含交易建议含义) 改成 链上侦测-neutral 链上状态:\n CLEAN — 无显著 链上侦测 触发信号\n WATCH — 部分异动但未触发主信号\n OPERATOR_DUMPED — 历史 庄家 已派完 (链上证据: ≥3 个内幕钱包已转出 ≥95%)\n DORMANT_INSIDER_RISK — 潜伏内幕未派 (内幕钱包仍持有 ≥0.5% 总供应)\n RECENT_DISTRIBUTION — 近 72h 大额链上活动 (≥10 笔异常转移) #}\n{% set _m6_quiet = (lineage.m6.n_quiet if lineage is defined and lineage and lineage.m6 is defined and lineage.m6 else 0) or 0 -%}\n{% set _m6_partial = (lineage.m6.n_partial_dumper if lineage is defined and lineage and lineage.m6 is defined and lineage.m6 else 0) or 0 -%}\n{% set _m6_full = (lineage.m6.n_full_dumper if lineage is defined and lineage and lineage.m6 is defined and lineage.m6 else 0) or 0 -%}\n{% set _recent_ns = namespace(n=0) -%}\n{% if anomaly is defined and anomaly -%}\n {% for d in (anomaly.detector_summary or []) -%}\n {% if d.label and ('72h' in d.label or '近期' in d.label) -%}\n {% set _recent_ns.n = d.count or 0 -%}\n {%- endif -%}\n {%- endfor -%}\n{%- endif -%}\n{% set _recent_anomaly_n = _recent_ns.n -%}\n{% set _wash = dump_tracking.get('wash_dominated', false) if dump_tracking is defined and dump_tracking else false -%}\n{% set _pure_pct = (dump_tracking.get('pure_insider_holds_pct_supply') or 0) if dump_tracking is defined and dump_tracking else 0 -%}\n\n{# v0.8.7.3: 加 subtype_zh 中文化 user-facing label. subtype 内部 enum 用作\n logic gate (==, downgrade rules), subtype_zh 仅 display. #}\n{% set _v = namespace(risk=0, subtype=\"CLEAN\", subtype_zh=t(\"render.chain_state_clean_zh\"),\n label=t(\"render.chain_state_clean_label\")) -%}\n\n{# 潜伏内幕未派 — 内幕钱包仍持有 ≥0.5% 总供应 (最高优先级, 因为 时点 不可预测) #}\n{% if _m6_quiet > 0 and _pure_pct > 0.5 -%}\n {% set _v.risk = _v.risk + 4 -%}\n {% set _v.subtype = \"DORMANT_INSIDER_RISK\" -%}\n {% set _v.subtype_zh = t(\"render.chain_state_dormant_zh\") -%}\n {% set _v.label = t(\"render.chain_state_dormant_label\", n=_m6_quiet, pct=\"%.1f\"|format(_pure_pct)) -%}\n{%- endif -%}\n\n{# 近期链上活动 — 72h ≥ 10 异常 #}\n{% if _recent_anomaly_n >= 10 -%}\n {% set _v.risk = _v.risk + 4 -%}\n {% set _v.subtype = \"RECENT_DISTRIBUTION\" -%}\n {% set _v.subtype_zh = t(\"render.chain_state_recent_zh\") -%}\n {% set _v.label = t(\"render.chain_state_recent_label\", n=_recent_anomaly_n) -%}\n{% elif _recent_anomaly_n >= 3 -%}\n {% set _v.risk = _v.risk + 2 -%}\n {% if _v.subtype == \"CLEAN\" -%}\n {% set _v.subtype = \"WATCH\" -%}\n {% set _v.subtype_zh = t(\"render.chain_state_watch_zh\") -%}\n {% set _v.label = t(\"render.chain_state_watch_label\", n=_recent_anomaly_n) -%}\n {%- endif -%}\n{%- endif -%}\n\n{# 历史 庄家 已派完 — 内幕大量已分完 #}\n{% if _m6_full >= 3 -%}\n {% set _v.risk = _v.risk + 3 -%}\n {% if _v.subtype == \"CLEAN\" -%}\n {% set _v.subtype = \"OPERATOR_DUMPED\" -%}\n {% set _v.subtype_zh = t(\"render.chain_state_dumped_zh\") -%}\n {% set _v.label = t(\"render.chain_state_dumped_label\", n=_m6_full) -%}\n {%- endif -%}\n{%- endif -%}\n\n{# 对敲主导加 risk #}\n{% if _wash -%}{% set _v.risk = _v.risk + 2 -%}{%- endif -%}\n{# 纯内幕剩 5%+ 进一步加 risk #}\n{% if _pure_pct > 5 -%}{% set _v.risk = _v.risk + 2 -%}{%- endif -%}\n{% if _v.risk > 10 -%}{% set _v.risk = 10 -%}{%- endif -%}\n\n{% if decision_summary is defined and decision_summary %}\n## {{ t(\"render.decision_summary_h2\") }}\n\n| {{ t(\"render.market_col_item\") }} | {{ t(\"render.market_col_value\") }} |\n|---|---|\n| **{{ t(\"render.decision_risk_score\") }}** | **{% for _ in range(_v.risk) %}🟥{% endfor %}{% for _ in range(10 - _v.risk) %}⬜{% endfor %} ({{ _v.risk }}/10)** |\n| **{{ t(\"render.decision_chain_state_label\") }}** | **{{ _v.subtype_zh }}** — {{ _v.label }} |\n| {{ t(\"render.market_primary_chain\") }} | {{ decision_summary.primary_chain or \"—\" | md_cell }}{% if decision_summary.primary_chain_lp_usd %} ({{ t(\"render.decision_chain_lp\", usd=\"{:,.0f}\".format(decision_summary.primary_chain_lp_usd)) }}){% endif %} |\n| **{{ t(\"render.decision_entry_cap\") }}** | **{% if decision_summary.entry_size_cap_usd %}${{ \"{:,.0f}\".format(decision_summary.entry_size_cap_usd) }}{% else %}—{% endif %}** |\n| {{ t(\"render.decision_short_catalyst\") }} | {{ (decision_summary.short_term_catalysts|join(\"; \")) or t(\"render.none_word\") | md_cell }} |\n| {{ t(\"render.decision_blindspots\") }} | {{ (decision_summary.blindspots|join(\"; \")) or t(\"render.none_word\") | md_cell }} |\n\n> {{ t(\"render.decision_5tier_title\") }}\n> - {{ t(\"render.decision_5tier_clean\") }}\n> - {{ t(\"render.decision_5tier_watch\") }}\n> - {{ t(\"render.decision_5tier_dumped\") }}\n> - {{ t(\"render.decision_5tier_dormant\") }}\n> - {{ t(\"render.decision_5tier_recent\") }}\n\n> {{ decision_summary.narrative | md_cell }}\n\n{% endif %}\n\n{# v0.7.16: 核心决策信息前置 — 结论 / 真实派发 / 风险信号聚合. 完整异动事件\n 列表 (data 出货) 现在沉到 holdings 之前, 让 trader 先看完结论再看明细. #}\n\n{# v0.7.22 (v0.7.25 中性化): 段标题改用 5 档 subtype + risk score (跟决策摘要一致, 避免\n \"原 3 档 判定 / 新 5 档\" 两套同时出现矛盾). 删除 baseline / downgrade\n 那行 — 是 3 档系统的 baseline-AVOID + downgrade-1-tier 衍生品, 5 档不再\n 需要这种 \"降级路径\" 概念.\n v0.7.25: \"结论\" → \"链上状态\" 避免 判定 含交易判断含义. #}\n## {{ t(\"render.chain_state_h2\", subtype_zh=_v.subtype_zh, risk=_v.risk) }}\n\n**{{ _v.label }}**\n\n{# v0.7.25: hide verdict.one_liner (LLM-filled freeform). LLM filler\n historically includes advice phrases (\"建议持仓者择机退出 alpha 头寸\",\n \"Recommend holders exit Alpha positions\") which violate the core\n \"no buy/sell advice\" constraint. _v.label above already states the\n on-chain state deterministically; one_liner adds nothing without LLM\n inference. v0.7.26 will replace LLM-fill with deterministic narrative\n derived from _v.subtype + key_metrics. Schema slot preserved in\n skeleton.json for AI二次研究. #}\n\n| {{ t(\"section.decision.anchors_table_header_anchor\") }} | {{ t(\"section.decision.anchors_table_header_value\") }} | {{ t(\"section.decision.anchors_table_header_status\") }} |\n|---|---|---|\n{% for a in decision_anchors %}| {{ a.anchor | md_cell }} | {{ a.value | md_cell }} | {{ a.status | md_cell }} |\n{% endfor %}\n\n{# v0.7.25: 删除 advice block (decision_action_block: immediate_action /\n stop_loss / re_entry_conditions). 报告核心约束是\"不给买卖建议\", advice\n block 整段违反 (\"立即在 alpha 分 3 笔抛售 滑点 ≤ 3%\" / \"止损 $0.005362\" /\n \"重新进场条件\"). decision_action_block 字段在 skeleton.json 保留供 AI\n 二次研究使用, 不在 user-facing markdown 渲染.\n 被删段: ### 行动方案 + immediate_action + stop_loss + re_entry_conditions\n 未删段: ### 决策锚点汇总 (lines above, 确定性 锚点 数字, 非 advice) #}\n\n{# v0.7.26: 当前链上行为画像 — 确定性 behavior_classifier 输出.\n 把 10 个 raw 检测器 翻译成 4 大类 / 10 种行为标签, 按 severity\n STRONG/MEDIUM/WEAK 渲染, OFF 折叠. 每条 1 行: emoji + 标签 + 严重度\n badge + 量化 trigger metric + 1 句话人话. 不上 LLM, render 端只取\n classifier 确定性 输出.\n\n 阈值校准来源: v0725_5_CALIBRATION.md (8 token backtest, 2026-06-11).\n\n 多标签 本设计如此: 同一 token 可以同时是 A1+B1+C3+D2. 不要把行为画像\n 理解为 \"判定\", 它跟 判定 是两层 - 判定 是综合状态, 行为画像\n 是 检测器 → human translation 中间层. #}\n{% if behavior_profile is defined and behavior_profile and behavior_profile.active_labels %}\n## {{ t(\"render.behavior_h2\") }}\n\n> {{ t(\"render.behavior_intro\") }}\n\n| {{ t(\"render.behavior_col_severity\") }} | {{ t(\"render.behavior_col_label\") }} | {{ t(\"render.behavior_col_category\") }} | {{ t(\"render.behavior_col_trigger\") }} | {{ t(\"render.behavior_col_fact\") }} |\n|:-:|---|---|---|---|\n{% for lid in behavior_profile.active_labels %}\n{% set info = behavior_profile.by_label[lid] %}\n{% set sev_emoji = \"🔴\" if info.severity == \"STRONG\" else (\"🟠\" if info.severity == \"MEDIUM\" else \"🟡\") %}\n{% set sev_text = info.severity %}\n{% set cat = info.category %}\n{% set cat_name = behavior_profile.category_names_zh[cat] %}\n{% set label_name = behavior_profile.label_names_zh[lid] %}\n{% set _metric_parts = [] %}\n{% for k, v in (info.trigger_metrics or {}).items() %}{% set _ = _metric_parts.append(k ~ \"=\" ~ v) %}{% endfor %}\n{% set metrics_str = _metric_parts | join(\", \") %}\n{# v0.7.26.1 adversarial review MED fix: apply md_cell escape to metrics_str. Current\n trigger_metrics are numeric/bool so risk is theoretical, but future\n addr/symbol values could otherwise break table pipes / backtick fences. #}\n| {{ sev_emoji }} **{{ sev_text }}** | `{{ lid | md_cell }}` {{ label_name | md_cell }} | {{ cat }} {{ cat_name | md_cell }} | `{{ metrics_str | md_cell }}` | {{ info.human_summary_zh | md_cell }} |\n{% endfor %}\n\n> {{ t(\"render.behavior_footer\") }}\n\n{% elif behavior_profile is defined and behavior_profile and behavior_profile.active_labels is iterable %}\n## {{ t(\"render.behavior_h2\") }}\n\n> {{ t(\"render.behavior_none\") }}\n\n{% endif %}\n\n{# v0.7.19.4 adversarial review HIGH#1: section guard must use `.get()` for the new\n tree_holds_tokens key (it's absent on pre-v0.7.19.4 skeletons), else\n StrictUndefined raises before the `or` falls back to the alias. The\n earlier test passed only because confirmed_total_tokens > 0 short-\n circuited the OR before the second branch was evaluated. #}\n{# v0.7.23 fix: original guard required confirmed_total_tokens > 0 OR\n tree_holds_tokens > 0 to render the section. On a surf-failed\n dump_tracker run (H 2026-06-09) both are zero and the entire section\n would disappear silently, hiding the buckets_complete=false banner\n that lives inside. Now we ALSO render when buckets_complete is\n explicitly false, so the user always sees the failure marker. #}\n{% if dump_tracking is defined and dump_tracking and ((dump_tracking.confirmed_total_tokens or 0) > 0 or (dump_tracking.get('tree_holds_tokens') or dump_tracking.get('insider_holds_tokens') or 0) > 0 or dump_tracking.get('buckets_complete') == false) %}\n## {{ t(\"render.dump_tracker_h2\") }}\n\n{# v0.8.1.2: 控筹口径汇总表 — 用户要求放在 section 顶部一目了然.\n 3 个口径分别口径不同 (纯内幕 / 内幕树含锁仓 / A2 净控筹), 在这里\n 统一摆出来跟 tokens / 占总供应 / 占流通 三栏对齐. #}\n{%- set _circ = meta.circulating_supply or 0 -%}\n{%- set _tot = meta.total_supply or 0 -%}\n{%- set _pure_tok = dump_tracking.pure_insider_holds_tokens or 0 -%}\n{%- set _pure_pct_tot = dump_tracking.pure_insider_holds_pct_supply or 0 -%}\n{%- set _tree_tok = dump_tracking.tree_holds_tokens or 0 -%}\n{%- set _tree_pct_tot = dump_tracking.tree_holds_pct_supply or 0 -%}\n{%- set _cfh_sum_top = (funding_attribution.cex_fanout_hubs.summary\n if funding_attribution is defined and funding_attribution\n and funding_attribution.cex_fanout_hubs is defined\n and funding_attribution.cex_fanout_hubs\n and funding_attribution.cex_fanout_hubs.summary\n else {}) -%}\n{%- set _a2_tok = (_cfh_sum_top.net_structured_fanout_tokens_total\n if _cfh_sum_top.net_structured_fanout_tokens_total is defined\n else (_cfh_sum_top.net_fanout_tokens_total\n if _cfh_sum_top.net_fanout_tokens_total is defined\n else None)) -%}\n{# v0.8.6.7 fix: 直接看 net_structured_fanout_tokens_total 是否 > 0.\n v0.8.5.4 per-hub fallback 保证 confirmed_hub recipients 完整即使 bulk\n truncated. 用 net 数字本身判断是否可算 — 算出来就显示 net, 算不出\n (=0) 才标 \"不可算\". BEAT 案例 recipients_truncated=True 但 net=80M\n 实际算出, 不能误标 \"不可算\". #}\n{%- set _a2_net = (_cfh_sum_top.get('net_structured_fanout_tokens_total') or 0) if _cfh_sum_top else 0 -%}\n{%- set _a2_stale = (_a2_net \u003c= 0) -%}\n\n{# v0.8.1.3: disjoint 拆分 + 加总. 用户视角:\n \"庄家敢不敢拉盘\"取决于**非庄家方手里有多少现可砸的筹码**.\n 非庄家方抛压 = 流通 - 庄家集团总控筹.\n 庄家弹药拆 3 桶:\n - 公开锁仓 (vesting/treasury/中转) = 内幕树 - 纯内幕. 按时间表释放, 拉盘时不立刻砸.\n - m6 可机动 = 纯内幕. 庄家想砸的话用这部分, 但拉盘时庄家不会砸自己.\n - CEX fan-out 外独立 = A2 净控筹. m6 之外通过 CEX→hub→拆散的庄家筹码.\n m6 钱包 (pre-launch 收 from deployer) 跟 A2 hub recipients (post-launch\n 收 from hub) 几乎肯定钱包级 disjoint — 直接加总作合理 upper bound. #}\n{# v0.8.1.6: 第 5 桶 — 其他庄家足迹钱包当前余额 (m6 / A2 / LP 之外).\n Aggregation 来源:\n - flow_operators: post-launch DEX_ARB_BOT / ASYNC_WASH / CROSS_ALPHA_OPERATOR\n / UNCLASSIFIED_SINGLE_OPERATOR 钱包 (sum net_balance_tokens > 0).\n - cross_sym whales: 跨币种鲸鱼 (sum current balance > 0).\n - high_throughput dumpers: 已识别过账 ≥ 1M tokens 的钱包 (balance > 0\n 的部分; 大部分历史庄家 balance ≈ 0 所以不计 — Velvet case).\n 排除已在 m6 / A2 / LP 集合的钱包避免双计 — Jinja 没有 set 操作, 改用\n 保守做法: 这一桶只算 dump_tracker 已知 insider_addrs **不包含**的部分\n (实测时这些 detector 输出的钱包 99% 是 post-launch 的 EOA 不会在 m6).\n 钱包重叠校验严格 disjoint 留 v0.8.2 backlog. #}\n{%- set _ops_ns = namespace(bal=0.0, n=0) -%}\n{% if flow_operators is defined and flow_operators and flow_operators.operators -%}\n {% for _op in flow_operators.operators -%}\n {%- set _opb = _op.get('net_balance_tokens') or 0 -%}\n {% if _opb > 0 -%}\n {%- set _ops_ns.bal = _ops_ns.bal + _opb -%}\n {%- set _ops_ns.n = _ops_ns.n + 1 -%}\n {% endif -%}\n {% endfor -%}\n{% endif -%}\n{% if cross_sym is defined and cross_sym and cross_sym.whales -%}\n {% for _w in cross_sym.whales -%}\n {%- set _wb = _w.get('current_balance') or 0 -%}\n {% if _wb > 0 -%}\n {%- set _ops_ns.bal = _ops_ns.bal + _wb -%}\n {%- set _ops_ns.n = _ops_ns.n + 1 -%}\n {% endif -%}\n {% endfor -%}\n{% endif -%}\n{% if funding_attribution is defined and funding_attribution\n and funding_attribution.high_throughput_dumpers is defined\n and funding_attribution.high_throughput_dumpers -%}\n {% for _d in (funding_attribution.high_throughput_dumpers.dumpers or []) -%}\n {%- set _db = _d.get('balance') or 0 -%}\n {% if _db > 1 -%}\n {%- set _ops_ns.bal = _ops_ns.bal + _db -%}\n {%- set _ops_ns.n = _ops_ns.n + 1 -%}\n {% endif -%}\n {% endfor -%}\n{% endif -%}\n{%- set _other_op_tok = _ops_ns.bal -%}\n{%- set _other_op_n = _ops_ns.n -%}\n\n{# v0.8.4: 先算 _suspected_tok 让 ceiling-clamp 公开锁仓桶时能扣它. #}\n{%- set _sus_ns = namespace(bal=0.0, n=0) -%}\n{% for _w in (monitoring_wallets or []) -%}\n {% if _w.monitor_role_enum == 'suspected_operator_reserve' -%}\n {%- set _b = _w.balance_tokens or 0 -%}\n {% if _b > 0 -%}\n {%- set _sus_ns.bal = _sus_ns.bal + _b -%}\n {%- set _sus_ns.n = _sus_ns.n + 1 -%}\n {% endif -%}\n {% endif -%}\n{% endfor -%}\n{%- set _suspected_tok = _sus_ns.bal -%}\n{%- set _suspected_n = _sus_ns.n -%}\n{%- set _operator_a2 = _a2_tok if (_a2_tok is not none and not _a2_stale) else 0 -%}\n{# v0.8.1.5: 所有链上 DEX pool LP 算庄家弹药 — 不只主池. S1/S2/S3\n Alpha token 的 LP 99% 是项目方/MM 提供 (散户 LP 极少). 算法:\n Primary: chain_lp_realtime.\u003cchain>.lp_tokens (surf 实测各 DEX-labeled\n holder 余额, sum across all chains)\n Fallback: alpha_liquidity_usd / 2 / alpha_price_usd (TVL 反推, 仅\n 主池 estimate, 当 surf 没拿到 DEX-labeled holder 数据时)\n 注: LP pool wallet 一般不在 m6 谱系 (LP create 时间在 trace_floor 前),\n 也不在 A2 净控筹 (LP 不是 fan-out hub), 否则会落进\"非庄家方抛压\"\n 错算. #}\n{%- set _lp_usd = (meta.alpha_liquidity_usd or 0) -%}\n{%- set _alpha_px = (meta.alpha_price_usd or 0) -%}\n{%- set _clp = meta.chain_lp_realtime if meta.chain_lp_realtime is defined else {} -%}\n{%- set _lp_ns = namespace(measured=0.0, n_pools=0, has_measured=false) -%}\n{% for _chain_name, _chain_lp in _clp.items() -%}\n {% if _chain_lp.lp_tokens is not none and _chain_lp.lp_tokens > 0 -%}\n {%- set _lp_ns.measured = _lp_ns.measured + _chain_lp.lp_tokens -%}\n {%- set _lp_ns.n_pools = _lp_ns.n_pools + (_chain_lp.n_dex_pools or 0) -%}\n {%- set _lp_ns.has_measured = true -%}\n {% endif -%}\n{% endfor -%}\n{%- set _lp_token_side = _lp_ns.measured if _lp_ns.has_measured else ((_lp_usd / 2 / _alpha_px) if (_lp_usd > 0 and _alpha_px > 0) else 0) -%}\n{%- set _lp_source = t(\"render.lp_source_measured\", n=_lp_ns.n_pools) if _lp_ns.has_measured else t(\"render.lp_source_derived\") -%}\n{# v0.8.3.4 — 矿币/跨链桥模式的 mint 合约 reserve 也算庄家弹药.\n JCT 测试发现: mint_authority_dumps.summary.total_current_balance = 4.25B\n (37% 流通) 是项目方未释放的矿币 reserve, 但 disjoint 5 桶完全没算.\n 伪矿币集群 (fake_mining_cluster_member) 的钱包当前余额也应进, 但\n enricher 没查 current balance — v0.8.4 backlog. 当前先把 mint reserve\n 接进来, 已经能把 JCT 类 token 的弹药估算从 ~1% 拉到 37%+. #}\n{%- set _mint_reserve_tok = ((funding_attribution.mint_authority_dumps.summary.get('total_current_balance') or 0)\n if funding_attribution is defined and funding_attribution\n and funding_attribution.mint_authority_dumps is defined\n and funding_attribution.mint_authority_dumps\n and funding_attribution.mint_authority_dumps.summary\n else 0) or 0 -%}\n{# v0.8.3.4 — 伪矿币集群已 mint 出去的部分 (累计 minted - 当前 reserve)\n 也是项目方控制的庄家弹药, 减去已经被 hidden_operator_activity probe\n surface 出来的\"已变现\"部分 (转 CEX / DEX 卖). 剩下还在集群钱包里的\n 就是潜在弹药. JCT 实测: 累计 mint 10.34B - reserve 4.25B - 已变现\n 313.66M ≈ 5.78B 还在 cluster 钱包里. #}\n{# v0.8.4: 必须 filter is_excluded=True 的 authority. Excluded 通常是\n deployer 钱包本身 (total_minted = 整个总供应), 不算 inflationary\n mint authority. 不 filter 会让 COLLECT/Velvet (非矿币 token) 误算\n 3B/1B 当 \"已 mint 给庄家集群\", 推 disjoint 表总数超 100% 流通. #}\n{%- set _fake_mining_addr_minted = 0.0 -%}\n{%- set _mining_ns = namespace(total_minted=0.0) -%}\n{% if funding_attribution is defined and funding_attribution\n and funding_attribution.mint_authorities is defined\n and funding_attribution.mint_authorities -%}\n {% for _auth in (funding_attribution.mint_authorities.authorities or []) -%}\n {% if not _auth.is_excluded -%}\n {%- set _mining_ns.total_minted = _mining_ns.total_minted + (_auth.total_minted or 0) -%}\n {% endif -%}\n {% endfor -%}\n{% endif -%}\n{%- set _hoa_realized = (hidden_operator_activity.confirmed_total_tokens\n if hidden_operator_activity is defined\n and hidden_operator_activity\n and hidden_operator_activity.confirmed_total_tokens is defined\n else 0) or 0 -%}\n{%- set _fake_mining_addr_minted = (_mining_ns.total_minted - _mint_reserve_tok - _hoa_realized) -%}\n{% if _fake_mining_addr_minted \u003c 0 %}{%- set _fake_mining_addr_minted = 0 %}{% endif -%}\n\n{# v0.8.4.2: 公开锁仓桶用 max(m6_locked, section_a_locked) 取**全网**\n vesting+multisig+treasury+airdrop_platform 总和, 避免漏算 m6 外的\n vesting wallets (Velvet 案例: SablierLockup `0x6e0bad2c` 471M m6 外,\n 旧算法漏算 → 庄家弹药低估).\n 再 unminted-subtract gate: 当 _locked_raw > circulating 时, m6/section_a\n 内含 unminted supply, 减 unminted_reserve_total. #}\n{%- set _unminted_reserve_total = ((meta.total_supply or 0) - _circ) if (meta.total_supply and _circ and meta.total_supply > _circ) else 0 -%}\n\n{# v0.8.4.6: 用 monitoring_wallets[role=deployer] 取 m6 enumerated set,\n 按\"m6 内 vs m6 外\" dedup section_a vesting/multisig. 避免 m6 tree 跟\n ①b 双算 (Velvet `0xd19dce53` Gnosis Safe 107M 同时在 m6 deployer +\n section_a multisig — 不 dedup 重算 107M; COLLECT 4 个 Gnosis Safe\n 257M 全在 m6, 不 dedup 重算 257M). #}\n{%- set _dpl_ns = namespace(set=[]) -%}\n{% for _w in (monitoring_wallets or []) -%}\n {% if _w.monitor_role_enum == 'deployer' -%}\n {%- set _dpl_ns.set = _dpl_ns.set + [(_w.addr_full or '')|lower] -%}\n {% endif -%}\n{% endfor -%}\n{%- set _m6_deployer_addrs = _dpl_ns.set -%}\n\n{%- set _sa_ns = namespace(\n vesting_out_m6=0.0,\n vesting_in_m6=0.0,\n multisig_out_m6=0.0,\n cex_operator_tok=0.0,\n cex_operator_n=0\n) -%}\n{%- set _clp_for_locked = meta.chain_lp_realtime if meta.chain_lp_realtime is defined else {} -%}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc -%}\n {# ①a vesting / treasury / airdrop_platform — m6 外才单独计, m6 内的\n vesting wallet 跟踪给 ② 桶减去 (m6 tree 含 vesting 是 raw 持仓\n 不该全算庄家弹药) #}\n {% for _cat in ['vesting', 'treasury', 'airdrop_platform'] -%}\n {% for _h in ((_thc.get(_cat) or {}).get('top') or []) -%}\n {%- set _addr = (_h.addr or '')|lower -%}\n {%- set _bal = _h.balance or 0 -%}\n {% if _addr not in _m6_deployer_addrs -%}\n {%- set _sa_ns.vesting_out_m6 = _sa_ns.vesting_out_m6 + _bal -%}\n {% else -%}\n {%- set _sa_ns.vesting_in_m6 = _sa_ns.vesting_in_m6 + _bal -%}\n {% endif -%}\n {% endfor -%}\n {% endfor -%}\n {# ①b multisig — m6 外才单独计 #}\n {% for _h in ((_thc.get('multisig') or {}).get('top') or []) -%}\n {%- set _addr = (_h.addr or '')|lower -%}\n {%- set _bal = _h.balance or 0 -%}\n {% if _addr not in _m6_deployer_addrs -%}\n {%- set _sa_ns.multisig_out_m6 = _sa_ns.multisig_out_m6 + _bal -%}\n {% endif -%}\n {% endfor -%}\n {# ⑦ CEX 项目方托管: 单 wallet ≥ 3% 流通 + m6 外\n v0.8.4.7: 10% → 3% (用户 review: ≥10% 阈值放过 Bitget Cold 21.6M\n Velvet 4.9% / Binance Wallet Proxy 31M COLLECT 5.8% 等大额单点\n CEX 持仓 — 5% 一个钱包散户充值不现实). #}\n {% for _h in ((_thc.get('cex') or {}).get('top') or []) -%}\n {%- set _addr = (_h.addr or '')|lower -%}\n {%- set _bal = _h.balance or 0 -%}\n {% if _circ and _bal / _circ >= 0.03 and _addr not in _m6_deployer_addrs -%}\n {%- set _sa_ns.cex_operator_tok = _sa_ns.cex_operator_tok + _bal -%}\n {%- set _sa_ns.cex_operator_n = _sa_ns.cex_operator_n + 1 -%}\n {% endif -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n\n{# v0.8.4.7: TOP-DOWN retail 算法 — 之前 bottom-up (circ - operator_total)\n 因桶 wallet 重叠 leak 严重 (Velvet 102% raw → retail 0 错误).\n 现在直接遍历 top 100 holders, 不在 operator union 的算 retail.\n v0.8.4.8: CEX 钱包整段从 retail/operator 双侧刨除 — CEX hot/cold/deposit\n 都是散户充值入口, 无法验证 wallet 余额属性 (项目方托管 vs 散户聚合).\n 归\"中性 CEX 中转池\", 速读分 3 个数字: operator / CEX 中转 / retail. #}\n{%- set _op_union_ns = namespace(set=_m6_deployer_addrs|list) -%}\n{%- set _retail_ns = namespace(tokens=0.0, n=0, top=[]) -%}\n{%- set _op_in_circ_ns = namespace(tokens=0.0) -%}\n{%- set _vest_unminted_ns = namespace(tokens=0.0) -%}\n{%- set _cex_pool_ns = namespace(tokens=0.0, n=0) -%}\n{# v1.2.11: operator-suspect whales — unlabelled top holders ≥ 1% of the on-chain\n holder-snapshot supply. Folded into operator (not retail). Parity with\n screen_summary._compute_chip_3way whale floor. #}\n{%- set _suspect_ns = namespace(tokens=0.0, n=0) -%}\n{%- set _mint_op_hit_ns = namespace(tokens=0.0, n=0) -%}\n{# Build operator union: m6 deployer + section_a multisig / vesting / treasury\n / airdrop_platform / lp + 启发式 / 检测器命中 wallets. NO CEX. #}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc -%}\n {% for _h in ((_thc.get('multisig') or {}).get('top') or []) -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_h.addr or '')|lower] -%}\n {% endfor -%}\n {% for _h in ((_thc.get('lp') or {}).get('top') or []) -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_h.addr or '')|lower] -%}\n {% endfor -%}\n {% for _h in ((_thc.get('treasury') or {}).get('top') or []) -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_h.addr or '')|lower] -%}\n {% endfor -%}\n {% for _h in ((_thc.get('airdrop_platform') or {}).get('top') or []) -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_h.addr or '')|lower] -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{# 启发式 + 其他检测器命中的 operator wallets from monitoring_wallets.\n v0.8.4.7.1: JCT review caught — 之前只含 suspected + fake_mining 漏了\n cross_alpha_inactive_whale (JCT `0x26209d9f0dc3` 1.58B = 13.7% 流通!)\n + anomaly_participant + public_cex_hot_wallet (这些都是 detector\n 命中的 operator-side, 不是 retail). #}\n{# v0.8.4.9.5: revert v0.8.4.9.4 — mint_authority 算 operator, 不算\n vesting_skip. 验证: Alpha API JCT circulating=11.5B 已包含 mint\n authority `0xe0b5ed` 持的 4.15B (已 mint 在它钱包里, 未 distribute).\n 是流通中项目方可机动弹药, 该算 operator. v0.8.4.9.4 错改 (用户 catch). #}\n{%- set _OPERATOR_ROLES = ['suspected_operator_reserve', 'fake_mining_cluster_member',\n 'cross_alpha_inactive_whale', 'anomaly_participant',\n 'public_cex_hot_wallet', 'cex_fanout_hub',\n 'cex_fanout_recipient', 'flow_operator',\n 'high_throughput_dumper', 'mining_fed_operator',\n 'mint_authority'] -%}\n{%- set _UNMINTED_ROLES = [] -%}\n{% for _w in (monitoring_wallets or []) -%}\n {% if _w.monitor_role_enum in _OPERATOR_ROLES -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_w.addr_full or '')|lower] -%}\n {% endif -%}\n{% endfor -%}\n{# Build vesting/unminted set (these wallets hold mostly unminted, skip from retail) #}\n{%- set _vest_set_ns = namespace(set=[]) -%}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc -%}\n {% for _h in ((_thc.get('vesting') or {}).get('top') or []) -%}\n {%- set _vest_set_ns.set = _vest_set_ns.set + [(_h.addr or '')|lower] -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{# v1.2.11 (TAC 2026-07-01): mint-authority WALLET balances are already-minted,\n dumpable operator ammo — NOT \"unminted reserve\". A wallet balance is by\n definition minted; unminted reserve = mint-cap − minted and is never a wallet\n balance. They now go to the OPERATOR union (was: _vest_set_ns / vest-skip,\n which hid TAC's 8 mint authorities holding 326.6M = 42% of BSC supply in\n ~38-45M equal chunks → chip reported \"100% 非庄家\"). This reverses the\n v0.8.6.1/v0.8.6.4 model that treated held balance as \"未释放锁仓\". Parity with\n screen_summary._compute_chip_3way (mint_op_addrs → op_union). #}\n{%- set _mint_op_ns = namespace(set=[]) -%}\n{% if funding_attribution is defined and funding_attribution\n and funding_attribution.mint_authorities is defined\n and funding_attribution.mint_authorities -%}\n {% for _auth in (funding_attribution.mint_authorities.authorities or []) -%}\n {%- set _mint_op_ns.set = _mint_op_ns.set + [(_auth.addr or '')|lower] -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [(_auth.addr or '')|lower] -%}\n {% endfor -%}\n{% endif -%}\n{# v0.8.6.5.0: wallet_cluster_graph cluster addrs 加进 _op_union_ns.\n Bubblemaps-style cluster detection — wallets transferring 给彼此 高额\n tokens (≥ 0.5% supply per edge) 形成 connected component ≥ 3 nodes.\n = 项目方控筹的非 cex_fanout / 非 mint_authority cluster. 加进 op_union\n 让 top-down hit 这些 addrs 时归 operator (不是 retail). #}\n{% if wallet_cluster_graph is defined and wallet_cluster_graph -%}\n {% for _cluster in (wallet_cluster_graph.get('clusters') or []) -%}\n {% for _a in (_cluster.get('addrs') or []) -%}\n {%- set _op_union_ns.set = _op_union_ns.set + [_a|lower] -%}\n {% endfor -%}\n {% endfor -%}\n{% endif -%}\n{# v1.0.5 (ARX 2026-06-22): drop DEX/CEX infra (surf/Arkham-labelled — e.g.\n \"PancakeSwap | Vault\", \"Binance Wallet\", routers) from the operator union so\n they are never counted as 项目方可控筹码. `_neutral_infra_addrs` is computed\n ONCE in the pipeline (single source of truth shared with\n screen_summary._compute_chip_3way) so this detail can't diverge from the\n 一屏结论 headline. #}\n{%- set _neutral_set = (_neutral_infra_addrs | default([])) | map('lower') | list -%}\n{%- if _neutral_set -%}\n {%- set _op_union_ns.set = _op_union_ns.set | reject('in', _neutral_set) | list -%}\n{%- endif -%}\n{# Iterate top 100 across primary chain only, classify each holder into 4 buckets:\n - vesting (unminted)\n - operator (m6 + multisig/vesting/treasury/airdrop_platform/lp + 检测器命中)\n - cex_pool (中性 — 散户充值 vs 项目方托管 无法区分)\n - retail (其他, 可验证非官方控筹)\n\n v0.8.5.0: 只算 primary_chain. JCT 跨链 case: Ethereum 链上 Gnosis Safe\n 持 13.1B JCT, 但 circulating_supply 只 BSC 数 (11.5B) — 混算导致 operator\n raw 撑爆 400%+. 严格分母 / 分子链一致. #}\n{%- set _primary_chain_for_classify = meta.get('primary_chain') if meta is defined and meta else None -%}\n{# v1.2.11: whale floor = 1% of Alpha circulating_supply (true float). A top\n holder that reached the retail fallthrough yet holds above this is\n operator-suspect (unverified), not retail. Keyed on circulating supply (NOT\n the thin surf snapshot — genuine retail is a large % of a thin snapshot). circ\n unknown → floor 0 → guard disabled. Parity with\n screen_summary._compute_chip_3way. #}\n{%- set _whale_floor = (meta.get('circulating_supply') or 0) * 0.01 -%}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc and (not _primary_chain_for_classify or _cn == _primary_chain_for_classify) -%}\n {% for _cat in ['vesting', 'multisig', 'treasury', 'airdrop_platform', 'cex', 'lp', 'unclassified'] -%}\n {% for _h in ((_thc.get(_cat) or {}).get('top') or []) -%}\n {%- set _addr = (_h.addr or '')|lower -%}\n {%- set _bal = _h.balance or 0 -%}\n {%- set _lbl = _h.label_text or '' -%}\n {% if _addr in _vest_set_ns.set -%}\n {%- set _vest_unminted_ns.tokens = _vest_unminted_ns.tokens + _bal -%}\n {% elif _cat == 'cex' -%}\n {# CEX wallet — 整段刨除. v0.8.4.8: 散户充值入口, 无法 attribute\n 余额是项目方托管 OR 散户聚合, 归中性 CEX 池, 不影响 retail/operator\n 判定. #}\n {%- set _cex_pool_ns.tokens = _cex_pool_ns.tokens + _bal -%}\n {%- set _cex_pool_ns.n = _cex_pool_ns.n + 1 -%}\n {% elif _addr in _op_union_ns.set -%}\n {%- set _op_in_circ_ns.tokens = _op_in_circ_ns.tokens + _bal -%}\n {% if _addr in _mint_op_ns.set -%}\n {%- set _mint_op_hit_ns.tokens = _mint_op_hit_ns.tokens + _bal -%}\n {%- set _mint_op_hit_ns.n = _mint_op_hit_ns.n + 1 -%}\n {% endif -%}\n {% elif _whale_floor > 0 and _bal >= _whale_floor and _addr not in _neutral_set -%}\n {# v1.2.11: unverified top holder (reached retail fallthrough) ≥ 1%\n circulating → operator-suspect, not retail. neutral_infra\n (vaults/routers) never counts here. #}\n {%- set _op_in_circ_ns.tokens = _op_in_circ_ns.tokens + _bal -%}\n {%- set _suspect_ns.tokens = _suspect_ns.tokens + _bal -%}\n {%- set _suspect_ns.n = _suspect_ns.n + 1 -%}\n {% else -%}\n {%- set _retail_ns.tokens = _retail_ns.tokens + _bal -%}\n {%- set _retail_ns.n = _retail_ns.n + 1 -%}\n {%- set _retail_ns.top = _retail_ns.top + [{'addr': _addr, 'bal': _bal, 'category': _cat, 'label': _lbl}] -%}\n {% endif -%}\n {% endfor -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{%- set _retail_topdown = _retail_ns.tokens -%}\n{%- set _retail_topdown_n = _retail_ns.n -%}\n{%- set _retail_topdown_pct = (_retail_topdown / _circ * 100) if _circ else 0 -%}\n{%- set _retail_top_wallets = _retail_ns.top | sort(attribute='bal', reverse=True) -%}\n{# v0.8.5.0: 加 cex_fanout_hubs.summary.net_structured_fanout_tokens_total\n 到 operator_in_circ. fan-out hubs + recipients 都是项目方控筹钱包,\n 但它们大多不在 top 100 持币人 (hub 转完币 balance ≈ 0, recipients 持仓.\n\n v0.8.6.1 update: mint authority 持仓不再算 op_in_circ, 而是归 vest_unminted\n (见上方 _vest_set_ns build). op_in_circ 只含 m6 / multisig / lp / 检测器\n 命中 / monitoring operator roles. audit 2026-06-12 M3 fix: 之前\n 注释说 \"mint authority 在 _op_in_circ_ns 已含\" 错误 (adversarial review catch). #\n \u003c 0.9% 流通 不进 top 100). top-down 遍历 top_holders 漏算这部分.\n net_structured_fanout_tokens_total = 排除 loopback + 跨 hub shuffle 后\n 真实 fan-out tokens 到独立钱包. 直接加 operator_in_circ.\n\n v0.8.5.1: dedup overlap — 91 fan-out recipients 中部分可能在 top 100\n 持币人 (top-down 已算 operator/cex/retail), 直接加 net_total 会双计.\n 计算 in-top-100 recipients 持仓 sum 作 overlap 减.\n\n 注: hubs 自己 (hub_addr) 不在 net_structured (那是 recipients 持仓), 不重叠. #}\n{%- set _cex_fanout_net = ((funding_attribution.cex_fanout_hubs or {}).get('summary') or {}).get('net_structured_fanout_tokens_total') or 0 if funding_attribution is defined else 0 -%}\n{%- set _fanout_recipient_addrs_ns = namespace(set=[]) -%}\n{% if funding_attribution is defined and funding_attribution and funding_attribution.cex_fanout_hubs is defined -%}\n {% for _h in (funding_attribution.cex_fanout_hubs.hubs or []) -%}\n {% for _a in (_h.get('_net_structured_recipient_addrs_raw') or []) -%}\n {%- set _fanout_recipient_addrs_ns.set = _fanout_recipient_addrs_ns.set + [_a|lower] -%}\n {% endfor -%}\n {% endfor -%}\n{% endif -%}\n{%- set _fanout_overlap_ns = namespace(tokens=0.0) -%}\n{# v0.8.6.2 adversarial review M1 fix: 加 primary_chain gate. _cex_fanout_net 是 BSC\n only 算的, overlap 也要限 primary_chain. 否则跨链 top_holders 偶然\n 碰到 BSC fan-out recipient addr 会错误扣减 BSC fanout tail. #}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc and (not _primary_chain_for_classify or _cn == _primary_chain_for_classify) -%}\n {% for _cat in ['vesting', 'multisig', 'treasury', 'airdrop_platform', 'cex', 'lp', 'unclassified'] -%}\n {% for _h in ((_thc.get(_cat) or {}).get('top') or []) -%}\n {%- set _addr = (_h.addr or '')|lower -%}\n {% if _addr in _fanout_recipient_addrs_ns.set -%}\n {%- set _fanout_overlap_ns.tokens = _fanout_overlap_ns.tokens + (_h.balance or 0) -%}\n {% endif -%}\n {% endfor -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{%- set _cex_fanout_tail = _cex_fanout_net - _fanout_overlap_ns.tokens if _cex_fanout_net > _fanout_overlap_ns.tokens else 0 -%}\n{# v0.8.6.5.0 adversarial review C2 fix: wallet_cluster_graph cluster wallets 不在 top\n 100 时持仓需 inject (跟 cex_fanout_tail 同 pattern). detector 已 emit\n per-addr balance via balance_sql. 这里算 cluster_tail = total cluster\n balance 减 in-top-100 overlap. #}\n{%- set _cluster_tail_overlap_ns = namespace(tokens=0.0) -%}\n{%- set _cluster_addrs_set_ns = namespace(set=[]) -%}\n{% if wallet_cluster_graph is defined and wallet_cluster_graph -%}\n {% for _cluster in (wallet_cluster_graph.get('clusters') or []) -%}\n {% for _a in (_cluster.get('addrs') or []) -%}\n {%- set _cluster_addrs_set_ns.set = _cluster_addrs_set_ns.set + [_a|lower] -%}\n {% endfor -%}\n {% endfor -%}\n{% endif -%}\n{%- set _cluster_total_balance_ns = namespace(tokens=0.0) -%}\n{% if wallet_cluster_graph is defined and wallet_cluster_graph -%}\n {% for _cluster in (wallet_cluster_graph.get('clusters') or []) -%}\n {# adversarial review v1.0.5 R1: subtract neutral-infra cluster members (via per-member\n addr_balances) so an off-top-100 DEX/CEX address can't re-enter operator\n through cluster_tail. Mirrors screen_summary._compute_chip_3way. #}\n {%- set _infra_in_cluster_ns = namespace(tokens=0.0) -%}\n {%- set _cab = _cluster.get('addr_balances') or {} -%}\n {% for _ca in (_cluster.get('addrs') or []) -%}\n {% if (_ca|lower) in _neutral_set -%}\n {%- set _infra_in_cluster_ns.tokens = _infra_in_cluster_ns.tokens + (_cab.get(_ca) or _cab.get(_ca|lower) or 0) -%}\n {% endif -%}\n {% endfor -%}\n {%- set _ct_net = (_cluster.get('cluster_balance_total_tokens') or 0) - _infra_in_cluster_ns.tokens -%}\n {%- set _cluster_total_balance_ns.tokens = _cluster_total_balance_ns.tokens + (_ct_net if _ct_net > 0 else 0) -%}\n {% endfor -%}\n{% endif -%}\n{# Subtract overlap with top_holders (primary chain only, per adversarial review M1 v0.8.6.2) #}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc and (not _primary_chain_for_classify or _cn == _primary_chain_for_classify) -%}\n {% for _cat in ['vesting', 'multisig', 'treasury', 'airdrop_platform', 'cex', 'lp', 'unclassified'] -%}\n {% for _h in ((_thc.get(_cat) or {}).get('top') or []) -%}\n {% if (_h.addr or '')|lower in _cluster_addrs_set_ns.set -%}\n {%- set _cluster_tail_overlap_ns.tokens = _cluster_tail_overlap_ns.tokens + (_h.balance or 0) -%}\n {% endif -%}\n {% endfor -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{%- set _wcg_cluster_tail = _cluster_total_balance_ns.tokens - _cluster_tail_overlap_ns.tokens if _cluster_total_balance_ns.tokens > _cluster_tail_overlap_ns.tokens else 0 -%}\n{# v0.8.6: implied_circ = top 100 sum (excl vesting) + cex_fanout tail. 用户\n review (2026-06-12): Alpha API circulating_supply 跟链上实际不一致\n (CLO Alpha 129M vs 链上 309M 2.4x), 速读用 Alpha 作分母让 sum 必然不为\n 100%. 改用 implied_circ = \"已 mint 进 wallet\" 总和:\n - top 100 sum 减 vesting unminted (锁仓不算流通) = op_in_circ + cex + retail\n - 加 cex_fanout tail (= 61 wallets in tail 持仓, 不在 top 100)\n - 速读 用 max(Alpha, implied) 作 effective_circ. sum (op + cex + retail)\n = 100% organic (无 ceiling-clamp 凑数).\n mint authority 持仓在 _op_in_circ_ns 已含 (= 已 mint 进 wallet 部分),\n 不再单独减 — per 用户 \"mint authority 持仓中已 mint 部分应该算 circ\". #}\n{%- set _top_100_excl_vesting = _op_in_circ_ns.tokens + _cex_pool_ns.tokens + _retail_ns.tokens -%}\n{%- set _implied_circ = _top_100_excl_vesting + _cex_fanout_tail + _wcg_cluster_tail -%}\n{%- set _circ_alpha = _circ -%}\n{# v0.8.6.1: 直接用 _implied_circ 作 effective. 不用 max(Alpha, implied).\n user wants sum (op + cex + retail) = 100%. vest_unminted (含 mint\n authority 持仓) 单独行不算 sum. Alpha API circ 跟 implied 差距加\n caveat. 例: JCT Alpha 11.5B (含 mint authority 4.26B), implied 7.18B\n (减 mint authority). 用 implied 显示流通 7.18B \"可砸盘\" 部分.\n CLO Alpha 129M (低估), implied 182M. 用 implied 反映链上实际. #}\n{%- set _circ = _implied_circ if _implied_circ > 0 else _circ_alpha -%}\n{%- set _implied_vs_alpha_ratio = (_implied_circ / _circ_alpha) if _circ_alpha else 1.0 -%}\n{# v0.8.6.5.3 fix: re-compute _retail_topdown_pct with implied_circ. Was\n computed earlier (line ~851) before _circ reassignment, leading to\n inconsistency where 速读 retail_with_tail_pct used implied_circ but\n retail_topdown_pct used Alpha circ — caused \"0.3% = 1.5% 内可验证\"\n contradiction in COLLECT case (Alpha 537M, implied 2980M, 5.55x). #}\n{%- set _retail_topdown_pct = (_retail_topdown / _circ * 100) if _circ else 0 -%}\n{%- set _operator_topdown = _op_in_circ_ns.tokens + _cex_fanout_tail + _wcg_cluster_tail -%}\n{%- set _operator_topdown_pct = (_operator_topdown / _circ * 100) if _circ else 0 -%}\n{# v0.8.5.3: 用户 review (2026-06-12 CLO 案例):\n 之前 logic 用 `total_supply - circulating_supply` (= unminted_reserve)\n 全减 operator_topdown 错误, 因为 (a) total-circ 含 mint 合约未释放储备\n 远超 mint authority 实际持仓, (b) operator_topdown 主体是流通中已\n 分发筹码, 不该用未释放储备减.\n\n CLO 案例: operator_topdown=278M, unminted_reserve=871M (= 1B - 129M),\n 278 - 871 = -593M → clamp 0. 速读显示 \"庄家弹药 0%\" 严重错.\n\n 修法: 用 _mint_authority_balance_in_top_holders (实际持已 mint 但\n 未分发部分) 替代 unminted_reserve_total. + ceiling-clamp at circ\n (avoid double-count from cross-bucket overlap).\n\n _mint_authority_balance_in_top_holders 计算: 遍历 BSC top_holders,\n 找 funding_attribution.mint_authorities.authorities 里 addrs, sum\n 它们 balance. 这才是 \"项目方控制已 mint 未分发\" 的真实量. #}\n{# v1.2.11: removed the dead _ma_set_ns / _ma_held_ns pre-pass (set-only, never\n read since v0.8.6). Mint-authority held balance now flows into _op_in_circ_ns\n directly via _op_union_ns (see the mint-authority → op_union block above), so\n there is nothing left to compute separately. #}\n{# v0.8.6: mint authority 持仓保留在 _operator_topdown (= 已 mint 进 wallet, 算\n 庄家弹药 per 用户决定). organic sum: op + cex + retail = 100%. #}\n{%- set _operator_topdown_in_circ = _operator_topdown -%}\n{%- set _operator_topdown_in_circ_pct = (_operator_topdown_in_circ / _circ * 100) if _circ else 0 -%}\n{%- set _operator_topdown_has_unminted = _implied_vs_alpha_ratio > 1.1 or _implied_vs_alpha_ratio \u003c 0.9 -%}\n{%- set _cex_pool_tokens = _cex_pool_ns.tokens -%}\n{# v0.8.4.9.5: tail estimate — surf token-holders 通常返 19-100 个, 真实\n holders 数千. circ 中 operator + CEX + retail_top + vesting_unminted 覆盖\n 外的部分 = 散户尾部 (top 100+ holders). 加进 retail 上界.\n\n v0.8.5.3: cex_fanout 67 recipients 平均 6 个在 top 100, 61 个在 tail.\n 用户 review (CLO 2026-06-12): tail 含 fan-out recipients = 庄家藏的筹码\n 不是散户. 计算 fan-out_in_tail = recipients NOT in top_holders 的 sum,\n 作 庄家 tail 标识, 不算 retail. #}\n{%- set _fanout_in_top_addrs_ns = namespace(set=[]) -%}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc and (not _primary_chain_for_classify or _cn == _primary_chain_for_classify) -%}\n {% for _cat in ['vesting', 'multisig', 'treasury', 'airdrop_platform', 'cex', 'lp', 'unclassified'] -%}\n {% for _h in ((_thc.get(_cat) or {}).get('top') or []) -%}\n {% if (_h.addr or '')|lower in _fanout_recipient_addrs_ns.set -%}\n {%- set _fanout_in_top_addrs_ns.set = _fanout_in_top_addrs_ns.set + [(_h.addr or '')|lower] -%}\n {% endif -%}\n {% endfor -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{%- set _n_fanout_recipients_total = (_fanout_recipient_addrs_ns.set | list | length) -%}\n{%- set _n_fanout_in_top = (_fanout_in_top_addrs_ns.set | list | unique | list | length) -%}\n{%- set _n_fanout_in_tail = _n_fanout_recipients_total - _n_fanout_in_top -%}\n{# v0.8.6.3: count surf top_holders returned (BSC primary), warn when \u003c 30.\n BEAT case (2026-06-12): Alpha API 报 142k holders 但 surf BSC top 100\n 只返 10 个 wallets (全是项目方). 速读 \"op 100% / retail 0%\" 看似离谱\n 实际 surf 数据 thin, 散户 142k 个在 tail 不可见. 加 caveat. #}\n{%- set _n_top_holders_returned_ns = namespace(n=0) -%}\n{% for _cn, _cd in (_clp_for_locked or {}).items() -%}\n {%- set _thc = _cd.get('top_holders_classified', {}) if _cd is mapping else {} -%}\n {% if _thc and '_error' not in _thc and (not _primary_chain_for_classify or _cn == _primary_chain_for_classify) -%}\n {% for _cat in ['vesting', 'multisig', 'treasury', 'airdrop_platform', 'cex', 'lp', 'unclassified'] -%}\n {%- set _n_top_holders_returned_ns.n = _n_top_holders_returned_ns.n + (((_thc.get(_cat) or {}).get('top') or []) | length) -%}\n {% endfor -%}\n {% endif -%}\n{% endfor -%}\n{# 阈值 \u003c 15: surf 通常返 20-30, BEAT-like thin case (\u003c 15) 才触发 #}\n{%- set _surf_top_thin = _n_top_holders_returned_ns.n \u003c 15 and meta.get('alpha_holders') and meta.alpha_holders > 1000 -%}\n{# fan-out tail 持仓估算: net_structured 减 overlap (in_top 部分已算 op_in_circ) #}\n{%- set _fanout_tail_tokens = _cex_fanout_net - _fanout_overlap_ns.tokens if _cex_fanout_net > _fanout_overlap_ns.tokens else 0 -%}\n\n{%- set _top_covered = _operator_topdown_in_circ + _cex_pool_tokens + _retail_topdown + _vest_unminted_ns.tokens -%}\n{%- set _tail_estimate = (_circ - _top_covered) if _circ > _top_covered else 0 -%}\n{%- set _retail_with_tail = _retail_topdown + _tail_estimate -%}\n{%- set _retail_with_tail_pct = (_retail_with_tail / _circ * 100) if _circ else 0 -%}\n{%- set _tail_pct = (_tail_estimate / _circ * 100) if _circ else 0 -%}\n{%- set _cex_pool_n = _cex_pool_ns.n -%}\n{%- set _cex_pool_pct = (_cex_pool_tokens / _circ * 100) if _circ else 0 -%}\n\n{# v0.8.4.9.3: Cascade unminted-reserve absorption — 优先级 vesting_in_m6\n → vesting_out_m6 → multisig_out_m6 → m6 tree (minus vesting_in_m6).\n 理由: vesting/multisig/m6 deployer 钱包都可能持已 mint 未 release supply\n (尤其 XCX m6 tree=129% / BTX multisig=374% case). 之前只减 vesting\n 导致 disjoint 桶 raw > 100% 流通.\n 优先级 from highest-likelihood-of-holding-unminted to lowest. #}\n{%- set _u_step1 = _sa_ns.vesting_in_m6 if _sa_ns.vesting_in_m6 \u003c _unminted_reserve_total else _unminted_reserve_total -%}\n{%- set _u_rem1 = _unminted_reserve_total - _u_step1 -%}\n{%- set _u_step2 = _sa_ns.vesting_out_m6 if _sa_ns.vesting_out_m6 \u003c _u_rem1 else _u_rem1 -%}\n{%- set _u_rem2 = _u_rem1 - _u_step2 -%}\n{%- set _u_step3 = _sa_ns.multisig_out_m6 if _sa_ns.multisig_out_m6 \u003c _u_rem2 else _u_rem2 -%}\n{%- set _u_rem3 = _u_rem2 - _u_step3 -%}\n{%- set _tree_minus_vest = (_tree_tok - _sa_ns.vesting_in_m6) if _tree_tok > _sa_ns.vesting_in_m6 else 0 -%}\n{%- set _u_step4 = _tree_minus_vest if _tree_minus_vest \u003c _u_rem3 else _u_rem3 -%}\n\n{%- set _vesting_in_circ = _sa_ns.vesting_out_m6 - _u_step2 -%}\n{%- set _multisig_tok = _sa_ns.multisig_out_m6 - _u_step3 -%}\n{%- set _tree_in_circ = _tree_minus_vest - _u_step4 -%}\n{% if _vesting_in_circ \u003c 0 %}{%- set _vesting_in_circ = 0 %}{% endif -%}\n{% if _multisig_tok \u003c 0 %}{%- set _multisig_tok = 0 %}{% endif -%}\n{% if _tree_in_circ \u003c 0 %}{%- set _tree_in_circ = 0 %}{% endif -%}\n{%- set _cex_operator_tok = _sa_ns.cex_operator_tok -%}\n{%- set _cex_operator_n = _sa_ns.cex_operator_n -%}\n{# v0.8.4.8: CEX 全归中性中转池, raw 加总不再含 _cex_operator_tok. #}\n{%- set _operator_total_raw = _vesting_in_circ + _multisig_tok + _tree_in_circ + _operator_a2 + _lp_token_side + _other_op_tok + _mint_reserve_tok + _fake_mining_addr_minted + _suspected_tok -%}\n{%- set _operator_total = _operator_total_raw if _operator_total_raw \u003c _circ else _circ -%}\n{%- set _retail_tok = (_circ - _operator_total_raw) if _circ and _operator_total_raw \u003c _circ else 0 -%}\n{%- set _retail_pct_circ = (_retail_tok / _circ * 100) if _circ else 0 -%}\n{%- set _operator_pct_raw = (_operator_total_raw / _circ * 100) if _circ else 0 -%}\n{%- set _has_bucket_leak = _operator_total_raw > _circ -%}\n{#- v0.8.4.9: strip 前面累积的 jinja 块空白行 -#}\n### {{ t(\"render.chip_h3\") }}\n\n{# v1.1.0 (CAP): multi-chain supply-overhang banner. When the Alpha listing is a\n MIRROR and the canonical supply lives on another chain, the buckets BELOW are\n only the Alpha-chain slice (CAP BSC 1.4%) and understate operator control. The\n banner surfaces the corrected 真实派发 measured on the canonical supply chain\n (operator/relay/non_operator % of circulating + locked overhang) so the reader\n sees the truth first. Headline (一屏结论) reads the same supply_chain_overhang\n buckets — parity. The Alpha-chain detail stays below, explicitly contextualised\n as the mirror slice. #}\n{% if supply_chain_overhang is defined and supply_chain_overhang\n and supply_chain_overhang.get('split') and not supply_chain_overhang.get('_error')\n and supply_chain_overhang.get('operator_pct') is not none -%}\n{{ t(\"render.chip_supply_overhang_banner\",\n alpha_chain=(supply_chain_overhang.get('alpha_chain') or '')|upper,\n supply_chain=(supply_chain_overhang.get('supply_chain_label') or supply_chain_overhang.get('supply_chain') or ''),\n supply_pct=\"%.1f\"|format(supply_chain_overhang.get('supply_pct_of_total') or 0),\n op=\"%.1f\"|format(supply_chain_overhang.get('operator_pct') or 0),\n relay=\"%.1f\"|format(supply_chain_overhang.get('relay_pct') or 0),\n nonop=\"%.1f\"|format(supply_chain_overhang.get('non_operator_pct') or 0),\n locked=\"%.1f\"|format(supply_chain_overhang.get('locked_pct_of_total') or 0)) }}\n\n{# adversarial review CRIT#1: switch the 真实派发 speed-read + 3-bucket table to the\n canonical supply-chain buckets so the DETAIL matches the banner + the 一屏结论\n headline (parity). The single-chain Alpha-slice math above this point is\n discarded for the display; the raw sub-breakdown further below stays as the\n Alpha-chain raw reference (it is explicitly the listing-chain classification). #}\n{%- set _circ = supply_chain_overhang.get('circulating_classified_tokens') or _circ -%}\n{%- set _operator_topdown_in_circ_pct = supply_chain_overhang.get('operator_pct') or 0 -%}\n{%- set _operator_topdown_in_circ = (_operator_topdown_in_circ_pct / 100.0) * _circ -%}\n{%- set _cex_pool_pct = supply_chain_overhang.get('relay_pct') or 0 -%}\n{%- set _cex_pool_tokens = (_cex_pool_pct / 100.0) * _circ -%}\n{%- set _cex_pool_n = supply_chain_overhang.get('n_relay_cex') or 0 -%}\n{%- set _retail_with_tail_pct = supply_chain_overhang.get('non_operator_pct') or 0 -%}\n{%- set _retail_with_tail = (_retail_with_tail_pct / 100.0) * _circ -%}\n{%- set _retail_topdown_pct = _retail_with_tail_pct -%}\n{%- set _retail_topdown = _retail_with_tail -%}\n{%- set _operator_topdown_has_unminted = false -%}\n{%- set _tail_pct = 0.0 -%}\n{%- set _n_fanout_in_tail = 0 -%}\n{# adversarial review re-audit MED: the Alpha-slice raw sub-breakdown (_vesting_in_circ /\n _multisig_tok / _operator_total_raw …) is computed from the listing-chain\n buckets; dividing those Alpha token amounts by the now-canonical _circ would\n be a mixed denominator. Suppress the raw leak note + the \u003cdetails> fold below\n when split (the canonical 3 buckets above are self-contained). #}\n{%- set _has_bucket_leak = false -%}\n{%- set _chip_is_supply_override = true -%}\n{% endif -%}\n{%- if _chip_is_supply_override is not defined -%}{%- set _chip_is_supply_override = false -%}{%- endif -%}\n\n{# v0.8.4.8: 速读 3 个数字 — 庄家弹药 / 交易所中转池 / 可验证非庄家方抛压.\n 交易所池中性 (散户 vs 项目方托管无法区分), 不并入任一侧. #}\n> **{{ t(\"render.chip_speedread_prefix\") }}**: {{ t(\"render.chip_speedread_circ\", circ=\"%.1f\"|format(_circ / 1_000_000)) }} · **{{ t(\"render.chip_nonop_pressure\") }} {% if _circ %}{{ \"%.1f\"|format(_retail_with_tail_pct) }}%{% else %}—{% endif %}** ({{ \"{:,.0f}\".format(_retail_with_tail) }} tokens = {{ t(\"render.chip_top100_verifiable\", pct=\"%.1f\"|format(_retail_topdown_pct)) }}{% if _tail_pct > 0.1 %} + {{ t(\"render.chip_tail\", pct=\"%.1f\"|format(_tail_pct)) }}{% if _n_fanout_in_tail > 5 %} {{ t(\"render.chip_tail_fanout_warn\", n=_n_fanout_in_tail) }}{% endif %}{% endif %}) · {{ t(\"render.chip_op_ammo\") }} {% if _circ %}{{ \"%.1f\"|format(_operator_topdown_in_circ_pct) }}%{% else %}—{% endif %}{% if _operator_topdown_has_unminted %} {{ t(\"render.chip_op_ammo_unminted\", alpha_circ=\"%.0f\"|format(_circ_alpha / 1_000_000), chain_circ=\"%.0f\"|format(_circ / 1_000_000), ratio=\"%.2f\"|format(_implied_vs_alpha_ratio)) }}{% if _implied_vs_alpha_ratio \u003c 0.9 %}{{ t(\"render.chip_op_ammo_overest\") }}{% else %}{{ t(\"render.chip_op_ammo_underest\") }}{% endif %}){% endif %} · {{ t(\"render.chip_cex_pool\") }} {% if _circ %}{{ \"%.1f\"|format(_cex_pool_pct) }}%{% else %}—{% endif %} ({{ t(\"render.chip_cex_pool_n\", n=_cex_pool_n) }}) · {{ t(\"render.chip_confirmed_realized\", usd=\"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd or 0), pct=\"%.1f\"|format(dump_tracking.confirmed_total_pct or 0)) }}{% if (dump_tracking.confirmed_net_sellout_usd or 0) == 0 %} {{ t(\"render.chip_no_seller_detected\") }}{% endif %}.\n{% if primary_sales is defined and primary_sales and primary_sales.get('pools') %}{% set _psa_top0 = primary_sales.get('pools')[0] %}>\n> **{{ t(\"render.psa_speedread_prefix\") }}**: {{ t(\"render.psa_speedread_line\", pct=\"%.1f\"|format(_psa_top0.pct_of_circulating or 0), tokens=\"{:,.0f}\".format(_psa_top0.tokens_distributed or 0), n=_psa_top0.n_recipients or 0, top10=\"%.1f\"|format(_psa_top0.top10_pct or 0)) }}{% if (_psa_top0.n_named_recipients or 0) > 0 %} {{ t(\"render.psa_speedread_kol\", n=_psa_top0.n_named_recipients) }}{% endif %}\n{% endif %}\n\n{# v0.8.6.8: 三桶 sanity check. 三桶 (op/cex/retail) 任一 = 0% 都不正常,\n 说明数据 thin 或 detector 漏算. 自动加排查 banner. 用户 review\n (2026-06-12 BEAT v0.8.6.7): \"庄家弹药 / 交易所 / 非庄家 都不太可能是 0,\n 如果是 0 就要主动排查\". #}\n{%- set _zero_buckets = [] -%}\n{% if _operator_topdown_in_circ_pct \u003c 0.5 -%}\n {%- set _zero_buckets = _zero_buckets + ['op_ammo'] -%}\n{% endif -%}\n{% if _cex_pool_pct \u003c 0.5 -%}\n {%- set _zero_buckets = _zero_buckets + ['cex_pool'] -%}\n{% endif -%}\n{% if _retail_with_tail_pct \u003c 0.5 -%}\n {%- set _zero_buckets = _zero_buckets + ['nonop'] -%}\n{% endif -%}\n{% if _zero_buckets | length > 0 %}\n> {{ t(\"render.chip_sanity_title\") }}\n{% for _b in _zero_buckets %}\n> - **{{ t(\"render.chip_bucket_\" + _b) }} 0%**: {% if _b == 'op_ammo' %}{{ t(\"render.chip_sanity_op_ammo\") }}\n{% elif _b == 'cex_pool' %}{{ t(\"render.chip_sanity_cex_pool\") }}\n{% elif _b == 'nonop' %}{{ t(\"render.chip_sanity_nonop\", n=_n_top_holders_returned_ns.n, alpha_holders=\"{:,.0f}\".format(meta.get('alpha_holders') or 0), tail_n=((meta.get('alpha_holders') or 0) - _n_top_holders_returned_ns.n) | int) }}\n{% endif %}\n{% endfor %}\n{% endif %}\n> {{ t(\"render.chip_pump_explainer\") }}\n\n{# v0.8.7.1: 筹码三分法主表 + 11 子桶 fold. ChatGPT review (2026-06-12):\n 主表只显示 operator / cex / retail 三块 + 关键判断 3 句话, 子桶详情\n 全部进 \u003cdetails>. 不再让 raw 415% 第一眼吓用户. #}\n| {{ t(\"render.chip_table_col_bucket\") }} | {{ t(\"render.chip_table_col_tokens\") }} | {{ t(\"render.chip_table_col_pct_circ\") }} | {{ t(\"render.chip_table_col_interp\") }} |\n|---|---:|---:|---|\n| {{ t(\"render.chip_row_operator\") }} | **{{ \"{:,.0f}\".format(_operator_topdown_in_circ) }}** | {% if _circ %}**{{ \"%.1f\"|format(_operator_topdown_in_circ_pct) }}%**{% else %}—{% endif %} | {% if _operator_topdown_in_circ_pct >= 70 %}{{ t(\"render.chip_op_interp_high\") }}{% elif _operator_topdown_in_circ_pct >= 40 %}{{ t(\"render.chip_op_interp_mid\") }}{% else %}{{ t(\"render.chip_op_interp_low\") }}{% endif %} |\n| {{ t(\"render.chip_row_retail\") }} | **{{ \"{:,.0f}\".format(_retail_with_tail) }}** | {% if _circ %}**{{ \"%.1f\"|format(_retail_with_tail_pct) }}%**{% else %}—{% endif %} | {% if _retail_with_tail_pct \u003c 5 %}{{ t(\"render.chip_retail_interp_light\") }}{% elif _retail_with_tail_pct \u003c 20 %}{{ t(\"render.chip_retail_interp_mid\") }}{% else %}{{ t(\"render.chip_retail_interp_heavy\") }}{% endif %} {{ t(\"render.chip_retail_interp_note\") }} |\n| {{ t(\"render.chip_row_cex\") }} | **{{ \"{:,.0f}\".format(_cex_pool_tokens) }}** | {% if _circ %}**{{ \"%.1f\"|format(_cex_pool_pct) }}%**{% else %}—{% endif %} | {{ t(\"render.chip_cex_interp\", n=_cex_pool_n) }} |\n\n### {{ t(\"render.chip_key_judgment_h3\") }}\n\n{% if _operator_topdown_in_circ_pct >= 70 and _retail_with_tail_pct \u003c 10 %}{{ t(\"render.chip_judgment_high\") }}\n{% elif _operator_topdown_in_circ_pct >= 50 %}{{ t(\"render.chip_judgment_mid\") }}\n{% else %}{{ t(\"render.chip_judgment_low\") }}\n{% endif %}{% if _has_bucket_leak %}\n{{ t(\"render.chip_raw_leak_note\", raw=\"%.0f\"|format(_operator_pct_raw), ammo=\"%.1f\"|format(_operator_topdown_in_circ_pct)) }}\n{% endif %}\n\n{% if not _chip_is_supply_override -%}\n\u003cdetails>\n\u003csummary>{{ t(\"render.chip_details_summary\") }}\u003c/summary>\n\n| {{ t(\"render.chip_subbucket_col_name\") }} | {{ t(\"render.chip_table_col_tokens\") }} | {{ t(\"render.chip_subbucket_col_pct\") }} | {{ t(\"render.chip_subbucket_col_note\") }} |\n|---|---:|---:|---|\n| {{ t(\"render.chip_sb_1a\") }} | {{ \"{:,.0f}\".format(_vesting_in_circ) }} | {% if _circ %}{{ \"%.1f\"|format(_vesting_in_circ / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_1a_note\", reserve=\"{:,.0f}\".format(_unminted_reserve_total)) }} |\n| {{ t(\"render.chip_sb_1b\") }} | {{ \"{:,.0f}\".format(_multisig_tok) }} | {% if _circ %}{{ \"%.1f\"|format(_multisig_tok / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_1b_note\") }} |\n| {{ t(\"render.chip_sb_2\") }} | {{ \"{:,.0f}\".format(_tree_in_circ) }} | {% if _circ %}{{ \"%.1f\"|format(_tree_in_circ / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_2_note\", tree=\"{:,.0f}\".format(_tree_tok), locked=\"{:,.0f}\".format(_sa_ns.vesting_in_m6), pure=\"{:,.0f}\".format(_pure_tok)) }} |\n{% if _a2_tok is not none and not _a2_stale %}| {{ t(\"render.chip_sb_3\") }} | {{ \"{:,.0f}\".format(_a2_tok) }} | {% if _circ %}{{ \"%.1f\"|format(_a2_tok / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_3_note\") }} |\n{% elif _a2_stale or _a2_tok is none %}| {{ t(\"render.chip_sb_3_stale\") }} | {% if _cfh_sum_top.total_cex_inflow_tokens %}{{ \"{:,.0f}\".format(_cfh_sum_top.total_cex_inflow_tokens) }}{% else %}—{% endif %} | — | {{ t(\"render.chip_sb_3_stale_note\") }} {% if _cfh_sum_top.total_fanout_recipients %}{{ _cfh_sum_top.total_fanout_recipients }} fan-out recipients{% endif %} |\n{% endif %}{% if _lp_token_side > 0 %}| {{ t(\"render.chip_sb_4\") }} | {{ \"{:,.0f}\".format(_lp_token_side) }} | {% if _circ %}{{ \"%.1f\"|format(_lp_token_side / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_4_note\") }} |\n{% endif %}{% if _other_op_tok > 0 %}| {{ t(\"render.chip_sb_5\") }} | {{ \"{:,.0f}\".format(_other_op_tok) }} | {% if _circ %}{{ \"%.1f\"|format(_other_op_tok / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_5_note\", n=_other_op_n) }} |\n{% endif %}{% if _mint_reserve_tok > 0 %}| {{ t(\"render.chip_sb_6\") }} | {{ \"{:,.0f}\".format(_mint_reserve_tok) }} | {% if _circ %}{{ \"%.1f\"|format(_mint_reserve_tok / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_6_note\") }} |\n{% endif %}{% if _fake_mining_addr_minted > 0 %}| {{ t(\"render.chip_sb_6p\") }} | {{ \"{:,.0f}\".format(_fake_mining_addr_minted) }} | {% if _circ %}{{ \"%.1f\"|format(_fake_mining_addr_minted / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_6p_note\") }} |\n{% endif %}{% if _suspected_tok > 0 %}| {{ t(\"render.chip_sb_6pp\") }} | {{ \"{:,.0f}\".format(_suspected_tok) }} | {% if _circ %}{{ \"%.1f\"|format(_suspected_tok / _circ * 100) }}%{% else %}—{% endif %} | {{ t(\"render.chip_sb_6pp_note\", n=_suspected_n) }} |\n{% endif %}{% if _unminted_reserve_total > 0 %}| {{ t(\"render.chip_sb_unminted\") }} | {{ \"{:,.0f}\".format(_unminted_reserve_total) }} | _{{ t(\"render.chip_sb_unminted_pct\", pct=\"%.1f\"|format(_unminted_reserve_total / (meta.total_supply or 1) * 100)) }}_ | {{ t(\"render.chip_sb_unminted_note\") }} |\n{% endif %}| ━━━━━ | ━━━━━ | ━━━━━ | ━━━━━ |\n| {{ t(\"render.chip_sb_raw_total\") }} | {{ \"{:,.0f}\".format(_operator_total_raw) }} | {% if _circ %}{{ \"%.1f\"|format(_operator_pct_raw) }}%{% else %}—{% endif %} | {% if _has_bucket_leak %}{{ t(\"render.chip_sb_raw_total_leak\", ammo=\"%.1f\"|format(_operator_topdown_in_circ_pct)) }}{% else %}{{ t(\"render.chip_sb_raw_total_noleak\") }}{% endif %} |\n\n**{{ t(\"render.chip_explainer_h\") }}**:\n- {{ t(\"render.chip_explainer_1\") }}\n- {{ t(\"render.chip_explainer_2\") }}\n- {{ t(\"render.chip_explainer_3\") }}\n- {{ t(\"render.chip_explainer_4\") }}\n- {{ t(\"render.chip_explainer_5\") }}\n\n\u003c/details>\n\n#### {{ t(\"render.chip_nonop_detail_h4\") }}\n\n> {{ t(\"render.chip_nonop_detail_intro\") }}\n\n| {{ t(\"render.chip_nonop_col_n\") }} | {{ t(\"render.chip_nonop_col_wallet\") }} | {{ t(\"render.chip_nonop_col_balance\") }} | {{ t(\"render.chip_nonop_col_pct\") }} | {{ t(\"render.chip_nonop_col_type\") }} | {{ t(\"render.chip_nonop_col_arkham\") }} |\n|---:|---|---:|---:|---|---|\n{% set _CAT_ZH = {'vesting': t(\"render.chip_cat_vesting\"), 'multisig': t(\"render.chip_cat_multisig\"), 'treasury': t(\"render.chip_cat_treasury\"), 'airdrop_platform': t(\"render.chip_cat_airdrop\"), 'cex': t(\"render.chip_cat_cex\"), 'lp': t(\"render.chip_cat_lp\"), 'unclassified': t(\"render.chip_cat_unclassified\")} %}\n{% for _w in _retail_top_wallets[:20] %}| {{ loop.index }} | [`{{ _w.addr[:14] }}`](https://bscscan.com/address/{{ _w.addr }}) | {{ \"{:,.0f}\".format(_w.bal) }} | {{ \"%.2f\"|format(_w.bal / _circ * 100) if _circ else 0 }}% | {{ _CAT_ZH.get(_w.category, _w.category) }} | {{ _w.label or '—' }} |\n{% endfor %}\n{% if _retail_topdown_n > 20 %}| ... | {{ t(\"render.chip_nonop_remainder\", n=(_retail_topdown_n - 20), tokens=\"{:,.0f}\".format(_retail_topdown - (_retail_top_wallets[:20] | sum(attribute='bal')))) }} | | | | |\n{% endif %}| ━━━━━━━━━━━━━━━━━ | ━━━━━━━━━━━━━━━━━ | ━━━━━━━━━ | ━━━━━━━ | ━━━━━━━━━ | ━━━━━━━━━ |\n| **{{ t(\"render.chip_nonop_total_label\") }}** | {{ t(\"render.chip_nonop_total_wallets\", n=_retail_topdown_n) }} | **{{ \"{:,.0f}\".format(_retail_topdown) }}** | **{{ \"%.1f\"|format(_retail_topdown_pct) }}%** | — | — |\n\n> {{ t(\"render.chip_nonop_nature_note\") }}\n\n{% set _24h_chg = meta.alpha_percent_change_24h or 0 %}\n{% if _24h_chg > 30 and _retail_pct_circ > 40 %}> {{ t(\"render.chip_pump_mismatch\", chg=\"%.0f\"|format(_24h_chg), retail=\"%.0f\"|format(_retail_pct_circ)) }}\n\n{% endif %}{% endif %}---\n\n{# v1.2.0: primary-sale (CCA / IDO / launchpad) attribution. Self-gated — the\n pipeline only populates primary_sales.pools when an operator-funded high-fan-out\n distribution pool is detected, so the whole section is absent for tokens with no\n public sale. Placed right after the 真实派发 (chip / supply-overhang) section. #}\n{% if primary_sales is defined and primary_sales and primary_sales.get('pools') -%}\n{% set _psa_explorer = {'ethereum':'https://etherscan.io/address/', 'bsc':'https://bscscan.com/address/', 'base':'https://basescan.org/address/', 'arbitrum':'https://arbiscan.io/address/', 'polygon':'https://polygonscan.com/address/', 'optimism':'https://optimistic.etherscan.io/address/'}.get(primary_sales.get('chain'), 'https://etherscan.io/address/') -%}\n### {{ t(\"render.psa_h3\") }}\n\n> {{ t(\"render.psa_intro\") }}\n\n{% for _ps in primary_sales.get('pools', []) -%}\n**{{ t(\"render.psa_pool_title\", n=loop.index) }}** [`{{ _ps.pool_addr[:12] }}…`]({{ _psa_explorer }}{{ _ps.pool_addr }}){% if _ps.label %} · {{ _ps.label }}{% endif %}\n\n{{ t(\"render.psa_pool_line\",\n tokens=\"{:,.0f}\".format(_ps.tokens_distributed or 0),\n pct=\"%.1f\"|format(_ps.pct_of_circulating or 0),\n n=_ps.n_recipients or 0,\n top5=\"%.1f\"|format(_ps.top5_pct or 0),\n top10=\"%.1f\"|format(_ps.top10_pct or 0),\n op=\"%.1f\"|format(_ps.operator_overlap_pct or 0)) }}\n\n{% if _ps.named_recipients -%}\n{{ t(\"render.psa_kol_intro\", n=_ps.n_named_recipients or 0, pct=((\"≥\" if _ps.named_pct_is_lower_bound else \"\") ~ (\"%.1f\"|format(_ps.named_pct_of_pool or 0)))) }}\n\n| # | {{ t(\"render.psa_kol_col_name\") }} | {{ t(\"render.psa_kol_col_addr\") }} | {{ t(\"render.psa_kol_col_tokens\") }} | {{ t(\"render.psa_kol_col_pct\") }} |\n|---:|---|---|---:|---:|\n{% for _kr in _ps.named_recipients -%}\n| {{ loop.index }} | {% if _kr.kind == 'entity' %}🅴 {% endif %}{{ _kr.name }} | [`{{ _kr.addr[:12] }}…`]({{ _psa_explorer }}{{ _kr.addr }}) | {{ \"{:,.0f}\".format(_kr.tokens or 0) }} | {{ \"%.1f\"|format(_kr.pct_of_pool or 0) }}% |\n{% endfor %}\n{% endif %}\n\n{% endfor -%}\n{% set _ps_social = primary_sales.get('social') or {} -%}\n{% if _ps_social.get('event_hits') -%}\n> {{ t(\"render.psa_social_intro\") }}\n>\n{% for _h in _ps_social.get('event_hits', [])[:3] -%}\n> - **[{{ _h.event }}]** @{{ _h.handle }} ({{ _h.likes }}♥){% if _h.announced_pct %} · {{ t(\"render.psa_social_alloc\", pct=_h.announced_pct) }}{% endif %}: {{ _h.excerpt }}\n{% endfor %}>\n> _{{ t(\"render.psa_social_caveat\") }}_\n{% endif %}\n---\n\n{% endif -%}\n{# v0.7.23.4: surface mining-fed wallet stock + flow directly in the\n dump_tracker table when funding_attribution.mining_fed_outflows produced\n data. mining-fed wallets ARE insiders in the operator sense (received\n tokens via mint contract → dumped via DEX); they should count toward\n tracked-wallet + holdings + (a)(b) rows rather than being relegated to\n a separate (c) row. _mfo / _mfo_sum aliases for compact template. #}\n{% set _mfo = (funding_attribution.mining_fed_outflows\n if funding_attribution is defined and funding_attribution\n and funding_attribution.mining_fed_outflows is defined\n and funding_attribution.mining_fed_outflows\n else None) -%}\n{% set _mfo_sum = (_mfo.summary if _mfo and _mfo.summary else None) -%}\n{# v0.8.4.9.2: AOP cold-start case — mfo.summary is {} from pipeline\n when no mining-fed wallets found. Use .get() to avoid StrictUndefined. #}\n{% set _mfo_n = (_mfo_sum.get('n_addrs') or 0) if _mfo_sum else 0 -%}\n{% set _mfo_bal = (_mfo_sum.get('total_current_balance') or 0) if _mfo_sum else 0 -%}\n{% set _mfo_sold = (_mfo_sum.get('total_dex_sold_tokens') or 0) if _mfo_sum else 0 -%}\n{% set _mfo_sold_usd = (_mfo_sum.get('total_dex_sold_usd') or 0) if _mfo_sum else 0 -%}\n{% set _mfo_swaps = (_mfo_sum.get('n_addrs_with_dex_sells') or 0) if _mfo_sum else 0 -%}\n{# Jinja {% set %} inside {% for %} only mutates within loop scope; use\n namespace() to accumulate across iterations. v0.7.23.4 first revision\n had this bug → (b) showed \"0 笔\" instead of 10,569. #}\n{% set _ns = namespace(swaps=0) -%}\n{% if _mfo and _mfo.per_addr -%}\n {% for _k, _v in _mfo.per_addr.items() -%}\n {% set _ns.swaps = _ns.swaps + (_v.dex_sells.n_swaps or 0) -%}\n {%- endfor -%}\n{%- endif -%}\n{% set _mfo_n_total_swaps = _ns.swaps -%}\n{# Floating-point cleanup: in - out completely balanced can yield ~-3e-8\n from numerical drift; clamp tiny |bal| to 0 so display reads cleanly. #}\n{% set _mfo_bal_clamped = (0 if (_mfo_sum and ((_mfo_sum.get('total_current_balance') or 0) | abs) \u003c 1) else ((_mfo_sum.get('total_current_balance') or 0) if _mfo_sum else 0)) -%}\n{% set _has_mfo = _mfo_sum and (_mfo_sold > 0 or _mfo_bal > 0) -%}\n{% set _ts = meta.total_supply if meta is defined and meta and meta.total_supply else 0 -%}\n{# Use clamped balance for pct calc — keep tiny float noise out of display. #}\n{% set _mfo_bal_eff = (0 if (_mfo_bal | abs) \u003c 1 else _mfo_bal) -%}\n{% set _mfo_bal_pct = (_mfo_bal_eff / _ts * 100) if (_ts and _has_mfo) else 0 -%}\n{# v0.7.24a: mint authority dumps — same shape as mining_fed_outflows so\n we can augment the dump_tracker table in parallel. mint_authorities is\n a NEW concept (the contracts that mint, not the wallets that receive\n mint) so their dump is additive to mining_fed dump. #}\n{% set _mad = (funding_attribution.mint_authority_dumps\n if funding_attribution is defined and funding_attribution\n and funding_attribution.mint_authority_dumps is defined\n and funding_attribution.mint_authority_dumps\n else None) -%}\n{% set _mad_sum = (_mad.summary if _mad and _mad.summary else None) -%}\n{# v0.8.4.9.2: AOP cold-start case — partial mad.summary dict #}\n{% set _mad_n = (_mad_sum.get('n_addrs') or 0) if _mad_sum else 0 -%}\n{% set _mad_bal = (_mad_sum.get('total_current_balance') or 0) if _mad_sum else 0 -%}\n{% set _mad_bal_eff = (0 if (_mad_bal | abs) \u003c 1 else _mad_bal) -%}\n{% set _mad_sold = (_mad_sum.get('total_dex_sold_tokens') or 0) if _mad_sum else 0 -%}\n{% set _mad_sold_usd = (_mad_sum.get('total_dex_sold_usd') or 0) if _mad_sum else 0 -%}\n{% set _has_mad = _mad_sum and (_mad_sold > 0 or _mad_bal_eff > 0) -%}\n{% set _ns_mad = namespace(swaps=0) -%}\n{% if _mad and _mad.per_addr -%}\n {% for _k, _v in _mad.per_addr.items() -%}\n {% set _ns_mad.swaps = _ns_mad.swaps + (_v.dex_sells.n_swaps or 0) -%}\n {%- endfor -%}\n{%- endif -%}\n{% set _mad_n_swaps = _ns_mad.swaps -%}\n{% set _mad_bal_pct = (_mad_bal_eff / _ts * 100) if (_ts and _has_mad) else 0 -%}\n| {{ t(\"render.market_col_item\") }} | {{ t(\"render.market_col_value\") }} |\n|---|---|\n{# v0.8.4.9: 主数字用 m6 谱系 raw count (lineage.m6.rows|length) 跟下面\n m6 表/关键解读统一. 旧 dump_tracking.insider_n_wallets 排除 CEX/DEX\n infra 给出 48 当 secondary, footnote 解释口径差.\n 矿币 case (JCT): m6 谱系空 → fallback 用 insider_standard. #}\n{%- set _m6_raw_count_only = lineage.m6.rows | length if lineage is defined and lineage and lineage.m6 is defined else 0 -%}\n{%- set _insider_standard = dump_tracking.insider_n_wallets + (_mfo_n if _has_mfo else 0) + (_mad_n if _has_mad else 0) -%}\n{%- set _m6_raw_count = _m6_raw_count_only if _m6_raw_count_only > 0 else _insider_standard -%}\n{% if _m6_raw_count_only > 0 %}| {{ t(\"render.dt_tracked_wallets_m6\") }} | **{{ _m6_raw_count }}** ({{ t(\"render.dt_standard_insider\", n=_insider_standard) }}{% if _has_mfo %} + **{{ t(\"render.dt_mining_fed\", n=_mfo_n) }}**{% endif %}{% if _has_mad %} + **{{ t(\"render.dt_mint_authority\", n=_mad_n) }}**{% endif %}{% if _m6_raw_count > _insider_standard %} + {{ t(\"render.dt_custody_dust\", n=(_m6_raw_count - _insider_standard)) }}{% endif %}) |\n{% else %}| {{ t(\"render.dt_tracked_wallets\") }} | **{{ _insider_standard }}** ({{ t(\"render.dt_standard_insider\", n=dump_tracking.insider_n_wallets) }}{% if _has_mfo %} + **{{ t(\"render.dt_mining_fed\", n=_mfo_n) }}**{% endif %}{% if _has_mad %} + **{{ t(\"render.dt_mint_authority\", n=_mad_n) }}**{% endif %}; {{ t(\"render.dt_no_m6_lineage\") }}) |\n{% endif %}\n{# v0.7.19.4: defensive `.get()` access so the template renders cleanly\n against a pre-v0.7.19.4 skeleton.json (old skeletons only carry\n insider_holds_*, not the new split fields). `.get()` returns None on\n missing key (no StrictUndefined raise), and `is none` filters cleanly.\n `is defined` does NOT work for dict attribute presence under\n StrictUndefined — use `.get()` + `is not none`. #}\n{% set _pure_pct = dump_tracking.get('pure_insider_holds_pct_supply') %}\n{% set _pure_tok = dump_tracking.get('pure_insider_holds_tokens') %}\n{% set _tree_pct = dump_tracking.get('tree_holds_pct_supply') %}\n{% set _tree_tok = dump_tracking.get('tree_holds_tokens') %}\n{% set _alias_pct = dump_tracking.get('insider_holds_pct_supply') %}\n{% set _alias_tok = dump_tracking.get('insider_holds_tokens') %}\n{# v0.7.23.4: 纯内幕 / 内幕树 augment with 挖矿机制取得钱包 current\n balance (stock). For mining-token model the mining-fed operators ARE\n the insider set; their unwound balance ≈ 0 means dumped clean, balance\n > 0 means still sitting on un-sold mint. #}\n{% set _augmented_pure_tok = (_pure_tok or 0) + (_mfo_bal_eff if _has_mfo else 0) + (_mad_bal_eff if _has_mad else 0) -%}\n{% set _augmented_pure_pct = (_pure_pct or 0) + _mfo_bal_pct + _mad_bal_pct -%}\n{% if _pure_pct is not none or _has_mfo or _has_mad %}\n| **{{ t(\"render.dt_pure_insider_label\") }}** | **{{ t(\"render.dt_pure_insider_value\", pct=\"%.4f\"|format(_augmented_pure_pct), tokens=\"{:,.0f}\".format(_augmented_pure_tok)) }}{% if _has_mfo %}{{ t(\"render.dt_pure_incl_mfo\") }}{% endif %}{% if _has_mad %}{{ t(\"render.dt_pure_incl_mad\") }}{% endif %}){% if _has_mfo and _mfo_bal_eff == 0 %} {{ t(\"render.dt_mfo_dumped_clean\") }}{% elif _has_mfo and _mfo_bal_eff > 0 %} {{ t(\"render.dt_mfo_still_sit\", tokens=\"{:,.0f}\".format(_mfo_bal_eff)) }}{% endif %}{% if _has_mad and _mad_bal_eff == 0 %} {{ t(\"render.dt_mad_dumped_clean\") }}{% elif _has_mad and _mad_bal_eff > 0 %} {{ t(\"render.dt_mad_still_sit\", tokens=\"{:,.0f}\".format(_mad_bal_eff)) }}{% endif %} |\n{% endif %}\n{% set _augmented_tree_tok = ((_tree_tok if _tree_tok is not none else (_alias_tok or 0))) + (_mfo_bal_eff if _has_mfo else 0) + (_mad_bal_eff if _has_mad else 0) -%}\n{% set _augmented_tree_pct = ((_tree_pct if _tree_pct is not none else _alias_pct) or 0) + _mfo_bal_pct + _mad_bal_pct -%}\n| {{ t(\"render.dt_tree_label\") }} | {{ t(\"render.dt_tree_value\", pct=\"%.4f\"|format(_augmented_tree_pct), tokens=\"{:,.0f}\".format(_augmented_tree_tok)) }}{% if _has_mfo or _has_mad %}{{ t(\"render.dt_tree_mining\") }}{% if _has_mad %}{{ t(\"render.dt_tree_mining_mad\") }}{% endif %}{{ t(\"render.dt_tree_mining_tail\") }}{% else %}{{ t(\"render.dt_tree_standard\") }}{% endif %}) |\n{# v0.7.23.4: (a) CEX deposit row. Mining-fed wallets may also push to CEX;\n when we get destination labels from surf_labels_probe we can decompose\n top_destinations into CEX-deposit subset + DEX-router subset + unlabeled.\n v0.7.23.4 minimal: keep dump_tracker (a) figure (insider→CEX subset =\n 0 for mining model where insider_addrs is empty), no augment here since\n we don't have CEX destination labels in mining_fed_outflows yet. #}\n{# v0.7.24a.1: augment (a) CEX row with destination label evidence.\n destination_label_summary aggregates top destinations across mining-fed\n + mint-authority dumps and classifies via Arkham. If cex_deposit_tokens\n > 0 we add it to (a); otherwise we surface \"verified 0\" with the\n inline reasoning (operator chose DEX path, not CEX deposit).\n Eleve note: the CEX 提币 phase Eleve described is CEX → 钱包\n (off-chain side), opposite direction from (a) which tracks\n wallet → CEX. Off-chain CEX withdraws are structurally outside the\n on-chain forensic. #}\n{% set _dest_sum = (funding_attribution.destination_label_summary\n if funding_attribution is defined and funding_attribution\n and funding_attribution.destination_label_summary is defined\n else None) -%}\n{% set _aug_cex_tok = (dump_tracking.confirmed_cex_tokens or 0) + (_dest_sum.cex_deposit_tokens if _dest_sum else 0) -%}\n| {{ t(\"render.dt_row_a_label\") }} | {{ \"{:,.0f}\".format(_aug_cex_tok) }}{% if dump_tracking.confirmed_cex_labels %} → {{ dump_tracking.confirmed_cex_labels|join(\", \") | md_cell }}{% endif %}{% if _dest_sum and _dest_sum.cex_deposit_tokens > 0 %} {{ t(\"render.dt_row_a_incl_cex\", tokens=\"{:,.0f}\".format(_dest_sum.cex_deposit_tokens)) }}{% elif (_has_mfo or _has_mad) and _aug_cex_tok == 0 %} {{ t(\"render.dt_row_a_all_dex\") }}{% endif %} |\n{# Caveat: explain why this row might still understate vs Twitter forensic\n when the dump phase included CEX withdrawals (the reverse direction). #}\n{% if (_has_mfo or _has_mad) %}\n> {{ t(\"render.dt_row_a_caveat\") }}\n{% endif %}\n{# v0.7.23.4: (b) DEX swap (本钱包) — 挖矿机制取得 wallets ARE 本钱包 in\n the operator-insider sense. Merge their dex_sells into (b) row instead\n of relegating to a separate (c) row (v0.7.23.3 design mistake — they're\n not a new category, they're the actual content of (b) for mining\n tokens). #}\n{% set _augmented_dex_tok = dump_tracking.confirmed_dex_tokens + _mfo_sold + _mad_sold -%}\n{% set _augmented_dex_swaps = dump_tracking.confirmed_dex_swaps + _mfo_n_total_swaps + _mad_n_swaps -%}\n| {{ t(\"render.dt_row_b_label\") }} | {{ \"{:,.0f}\".format(_augmented_dex_tok) }} ({{ t(\"render.dt_row_b_swaps\", n=\"{:,}\".format(_augmented_dex_swaps)) }}{% if _has_mfo and _mfo_sold > 0 %}{{ t(\"render.dt_row_b_mfo\", tokens=\"{:,.0f}\".format(_mfo_sold), swaps=\"{:,}\".format(_mfo_n_total_swaps), usd=\"{:,.0f}\".format(_mfo_sold_usd)) }}{% endif %}{% if _has_mad and _mad_sold > 0 %}{{ t(\"render.dt_row_b_mad\", tokens=\"{:,.0f}\".format(_mad_sold), swaps=\"{:,}\".format(_mad_n_swaps), usd=\"{:,.0f}\".format(_mad_sold_usd)) }}{% endif %}{% if _has_mfo or _has_mad %} {{ t(\"render.dt_row_b_via_dex\") }}{% endif %}) |\n| **{{ t(\"render.dt_gross_label\") }}** | **{% if dump_tracking.confirmed_capped %}≈{% else %}≥{% endif %} {{ \"{:,.0f}\".format(dump_tracking.confirmed_total_tokens + _mfo_sold + _mad_sold) }}{% if dump_tracking.confirmed_total_pct is not none or _has_mfo or _has_mad %} {{ t(\"render.dt_gross_circ\", pct=\"%.1f\"|format((dump_tracking.confirmed_total_pct or 0) + ((((_mfo_sold + _mad_sold) / (meta.total_supply or 1)) * 100) if (meta.total_supply and (_has_mfo or _has_mad)) else 0))) }}{% else %} {{ t(\"render.dt_gross_circ_unknown\") }}{% endif %}**{% if (dump_tracking.confirmed_est_profit_usd is not none) or _mfo_sold_usd > 0 or _mad_sold_usd > 0 %}, USD ≈ ${{ \"{:,.0f}\".format((dump_tracking.confirmed_est_profit_usd or 0) + _mfo_sold_usd + _mad_sold_usd) }}{% endif %} |\n{# v0.7.21.10: 净卖出 row — apparatus' time-weighted estimate.\n Only render when the helper produced a value; on holder-snapshot chains\n (Solana) and dump_tracker failures the field is None and we skip. #}\n{# v0.8.4.9.8: 净卖出 fallback — confirmed_net_sellout_usd 是 None (矿币 m6\n 谱系空时), 用 ht_dumpers throughput 算只给参考 (不当真数, 含 wash). #}\n{%- set _ht_dumpers_list = (funding_attribution.high_throughput_dumpers.dumpers if funding_attribution is defined and funding_attribution and funding_attribution.high_throughput_dumpers is defined and funding_attribution.high_throughput_dumpers else []) -%}\n{%- set _ht_ns = namespace(total_out=0.0) -%}\n{% for _d in (_ht_dumpers_list or []) -%}{%- set _ht_ns.total_out = _ht_ns.total_out + (_d.get('total_out') or 0) -%}{% endfor -%}\n{%- set _ht_median_px = dump_tracking.get('median_price_usd') or 0 -%}\n{%- set _ht_throughput_usd = _ht_ns.total_out * _ht_median_px if _ht_median_px else 0 -%}\n| **{{ t(\"render.dt_net_label\") }}** | {% if dump_tracking.get('confirmed_net_sellout_usd') is not none %}**${{ \"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd) }}**{% if dump_tracking.get('apparatus_dex_twap_usd_per_token') is not none %} {{ t(\"render.dt_net_twap\", twap=\"%.4g\"|format(dump_tracking.apparatus_dex_twap_usd_per_token)) }}{% endif %}{% if dump_tracking.get('confirmed_dex_real_usd') is not none and dump_tracking.get('confirmed_cex_estimated_usd') is not none %}{{ t(\"render.dt_net_dex_cex\", dex_usd=\"{:,.0f}\".format(dump_tracking.confirmed_dex_real_usd), cex_usd=\"{:,.0f}\".format(dump_tracking.confirmed_cex_estimated_usd)) }}{% endif %}{% if dump_tracking.get('net_above_gross_pct') is not none and dump_tracking.net_above_gross_pct > 5 %} {{ t(\"render.dt_net_above_gross\", pct=\"%.0f\"|format(dump_tracking.net_above_gross_pct)) }}{% endif %}{% else %}{{ t(\"render.dt_net_unreliable\", tokens=\"{:,.0f}\".format(_ht_ns.total_out), circ_x=\"%.1fx\"|format(_ht_ns.total_out / (meta.circulating_supply or 1))) }}{% endif %} |\n\n{% if dump_tracking.confirmed_capped %}\n> {{ t(\"render.dt_capped_note\") }}\n{% endif %}\n\n{# v0.9.2: per-window 时间窗拆分 — EmberCN-style 短期 alarm 对账. SQL\n CASE WHEN 取 7d/30d/累计 三个窗口, 0 额外 surf credit. 累计 = dump\n tracker 用的 date_floor (老 token surf_safe 364d, 新 token listing\n date). #}\n{% set _has_window = dump_tracking.get('confirmed_net_sellout_usd_7d') is not none or dump_tracking.get('confirmed_total_tokens_7d') %}\n{% if _has_window %}\n\n> {{ t(\"render.dt_window_intro\") }}\n\n| {{ t(\"render.dt_window_col_window\") }} | {{ t(\"render.dt_window_col_gross\") }} | {{ t(\"render.dt_window_col_pct\") }} | {{ t(\"render.dt_window_col_net\") }} | {{ t(\"render.dt_window_col_dex\") }} | {{ t(\"render.dt_window_col_cex\") }} |\n|---|---:|---:|---:|---:|---:|\n| **{{ t(\"render.dt_window_7d\") }}** | {{ \"{:,.0f}\".format(dump_tracking.get('confirmed_total_tokens_7d') or 0) }} | {% if dump_tracking.get('confirmed_total_pct_7d') is not none %}{{ \"%.2f\"|format(dump_tracking.confirmed_total_pct_7d) }}%{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_net_sellout_usd_7d') is not none %}**${{ \"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd_7d) }}**{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_dex_real_usd_7d') is not none %}${{ \"{:,.0f}\".format(dump_tracking.confirmed_dex_real_usd_7d) }}{% else %}—{% endif %} | {{ \"{:,.0f}\".format(dump_tracking.get('confirmed_cex_tokens_7d') or 0) }} |\n| {{ t(\"render.dt_window_30d\") }} | {{ \"{:,.0f}\".format(dump_tracking.get('confirmed_total_tokens_30d') or 0) }} | {% if dump_tracking.get('confirmed_total_pct_30d') is not none %}{{ \"%.2f\"|format(dump_tracking.confirmed_total_pct_30d) }}%{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_net_sellout_usd_30d') is not none %}${{ \"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd_30d) }}{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_dex_real_usd_30d') is not none %}${{ \"{:,.0f}\".format(dump_tracking.confirmed_dex_real_usd_30d) }}{% else %}—{% endif %} | {{ \"{:,.0f}\".format(dump_tracking.get('confirmed_cex_tokens_30d') or 0) }} |\n| {{ t(\"render.dt_window_cumulative\") }} | {{ \"{:,.0f}\".format(dump_tracking.confirmed_total_tokens or 0) }} | {% if dump_tracking.confirmed_total_pct is not none %}{{ \"%.2f\"|format(dump_tracking.confirmed_total_pct) }}%{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_net_sellout_usd') is not none %}${{ \"{:,.0f}\".format(dump_tracking.confirmed_net_sellout_usd) }}{% else %}—{% endif %} | {% if dump_tracking.get('confirmed_dex_real_usd') is not none %}${{ \"{:,.0f}\".format(dump_tracking.confirmed_dex_real_usd) }}{% else %}—{% endif %} | {{ \"{:,.0f}\".format(dump_tracking.get('confirmed_cex_tokens') or 0) }} |\n\n> {{ t(\"render.dt_window_explainer\") }}\n\n{% endif %}\n\n{# v0.8.3: 隐藏庄家弹药的历史动作 — 用 hidden_operator_activity probe 跑\n (a)/(b) 同算法但 input 集合换 suspected_operator_reserve +\n fake_mining_cluster_member 钱包. 只在有变现行为时显示 (避免噪音).\n v0.8.3.1 audit fixes:\n - HIGH #2: 用 .get() 不直接 .attr, 避免 partial dict 触发 StrictUndefined\n - MED #3: 叠加 claim 软化, 加 \"需扣除重叠\" 注解\n - MED #4: USD 算法 source 注明 (hidden_own_dex / m6_insider_twap)\n - LOW #7: 时间窗用实际 date_floor, 不写\"365d\"\n#}\n{% set _hoa = hidden_operator_activity if hidden_operator_activity is defined else None %}\n{% set _hoa_n = (_hoa.get('n_hidden_wallets_tracked') if _hoa else 0) or 0 %}\n{% set _hoa_total = (_hoa.get('confirmed_total_tokens') if _hoa else 0) or 0 %}\n{% set _hoa_err = (_hoa.get('_error') if _hoa else None) %}\n{% set _hoa_floor = (_hoa.get('date_floor') if _hoa else None) %}\n{% if _hoa and _hoa_total > 0 %}{% set _hoa_src = _hoa.get('confirmed_est_usd_source') or 'none' %}\n> {{ t(\"render.hoa_observed_prefix\", n=_hoa_n, cex_tokens=\"{:,.0f}\".format(_hoa.confirmed_cex_tokens), n_cex=_hoa.n_distinct_cex_destinations, cex_brands=(_hoa.cex_destination_brands | join(\", \") if _hoa.cex_destination_brands else \"—\"), dex_tokens=\"{:,.0f}\".format(_hoa.confirmed_dex_tokens), n_swaps=_hoa.n_dex_swaps, total=\"{:,.0f}\".format(_hoa_total)) }}{% if _hoa.confirmed_total_pct_circ is not none %} {{ t(\"render.hoa_pct_circ\", pct=\"%.2f\"|format(_hoa.confirmed_total_pct_circ)) }}{% endif %}{% if _hoa.confirmed_est_usd %}, USD ≈ **${{ \"{:,.0f}\".format(_hoa.confirmed_est_usd) }}**{% if _hoa_src == 'hidden_own_dex' %} {{ t(\"render.hoa_usd_src_own_dex\") }}{% elif _hoa_src == 'hidden_own_dex_only' %} {{ t(\"render.hoa_usd_src_own_dex_only\") }}{% elif _hoa_src == 'm6_insider_twap' %} {{ t(\"render.hoa_usd_src_m6_twap\") }}{% endif %}{% endif %}. {{ t(\"render.hoa_observed_tail\") }}{% if _hoa_floor %} {{ t(\"render.hoa_time_window\", floor=_hoa_floor) }}{% endif %}\n{% elif _hoa and _hoa_n > 0 and not _hoa_err %}\n> {{ t(\"render.hoa_none_prefix\", n=_hoa_n) }} **{% if _hoa_floor %}{{ t(\"render.hoa_none_since\", floor=_hoa_floor) }}{% else %}{{ t(\"render.hoa_none_window\") }}{% endif %}{{ t(\"render.hoa_none_tail\") }}**{{ t(\"render.hoa_none_accumulating\") }}\n{% elif _hoa and _hoa_err %}\n> {{ t(\"render.hoa_err\", err=(_hoa_err | md_cell)) }}\n{% endif %}\n{# v0.7.23.3: distinguish mining-token mode from real surf failure inside\n the dump_tracker section body. _dq_mining_model is computed at the top\n of the template; reuse it here so the in-section banner doesn't mislead\n the reader with \"surf 失败 重跑\" when the actual issue is that the\n token's distribution model bypasses rule_11 (and the (c) row above\n shows what we recovered via the mining-fed path). #}\n{% if dump_tracking.buckets_complete == false %}\n{% if _dq_mining_model %}\n> {{ t(\"render.dt_mining_model_note_prefix\") }} ({% if _mfo_sum and _mfo_sum.total_dex_sold_usd > 0 %}{{ t(\"render.dt_mining_model_note_usd\", usd=\"{:,.0f}\".format(_mfo_sum.total_dex_sold_usd)) }}{% else %}{{ t(\"render.dt_mining_model_note_seedetail\") }}{% endif %}). {{ t(\"render.dt_mining_model_note_norerun\") }}\n{% else %}\n> {{ t(\"render.dt_incomplete_note\") }}\n{% endif %}\n{% endif %}\n{% if dump_tracking.wash_dominated %}\n> {{ t(\"render.dt_wash_dominated_note\", n_sellers=dump_tracking.n_dex_sellers, total_swaps=\"{:,}\".format(dump_tracking.total_dex_swaps), top_swaps=\"{:,}\".format(dump_tracking.top_seller_swaps)) }}\n{% endif %}\n\n{% endif %}\n\n\u003ca id=\"section-recent-anomaly\">\u003c/a>\n## {{ t(\"render.risk_signal_h2\") }}\n\n### {{ t(\"section.anomaly.detector_summary_h3\") }}\n\n| {{ t(\"section.anomaly.detector_table_header_emoji\") }} | {{ t(\"section.anomaly.detector_table_header_label\") }} | {{ t(\"section.anomaly.detector_table_header_count\") }} | {{ t(\"section.anomaly.detector_table_header_detail\") }} |\n|---|---|---|---|\n{% for d in anomaly.detector_summary %}| {{ d.emoji }} | {{ d.label | md_cell }} | {{ d.count }} | {{ d.detail | md_cell }} |\n{% endfor %}\n\n### {{ t(\"section.anomaly.rhythm_h3\") }}\n\n**{{ anomaly.rhythm.title | md_cell }}**\n\n{% for w in anomaly.rhythm.waves -%}\n- {{ w.name | md_cell }} ({{ w.ts_text | md_cell }}): {{ w.detail | md_cell }}\n{% endfor %}\n\n{# v0.7.25: hide 异常.verdict_impact (LLM-filled). 同 判定.one_liner\n 原因 — LLM 填的文案含 \"触发 EXIT_IF_HOLDING\" / \"建议...\" 类交易判断词,\n 违反 \"no advice\" 约束. 检测器汇总 + 节奏识别表已 确定性 给出\n 全部链上事实数字, verdict_impact 段不增加 链上侦测价值. v0.7.26+\n 会用 确定性 behavior_classifier 输出替代. #}\n\n{# v0.7.22 P0 #3: 完整异动事件列表默认折叠 (散户 不会逐行读). 顶部加 1 行\n 汇总 让 散户 看到分波统计; 详细 evt_xxx 表 click 展开. #}\n\u003cdetails{% if mode == 'deep' %} open{% endif %}>\n\u003csummary>📂 \u003cstrong>{{ t(\"section.anomaly.title\") }}\u003c/strong> — {% set _ev_ns = namespace(n=0) %}{% for w in anomaly.waves %}{% set _ev_ns.n = _ev_ns.n + (w.events|length) %}{% endfor %}{{ t(\"render.anomaly_summary\", n_waves=anomaly.waves|length, n_events=_ev_ns.n) }}\u003c/summary>\n\n{% for wave in anomaly.waves %}\n### {{ wave.emoji }} {{ wave.title | md_cell }} ({{ wave.ts_range | md_cell }}, **{{ wave.status_text | md_cell }}**)\n\n| {{ t(\"section.anomaly.table_header_evt\") }} | {{ t(\"section.anomaly.table_header_utc\") }} | {{ t(\"section.anomaly.table_header_ago\") }} | {{ t(\"section.anomaly.table_header_from_to\") }} | {{ t(\"section.anomaly.table_header_nature\") }} | {{ t(\"section.anomaly.table_header_amount\") }} |\n|---|---|---|---|---|---|\n{% for ev in wave.events %}| `{{ ev.evt_ref }}` | {{ ev.ts | md_cell }} | {{ ev.hours_ago_text | md_cell }} | {{ ev.from_to | md_cell }} | {{ ev.nature | md_cell }} | {{ ev.amount | md_cell }} |\n{% endfor %}\n\n{% endfor %}\n\n\u003c/details>\n\n{# v0.7.16: detector_summary + rhythm + verdict_impact moved up to\n \"📊 风险信号聚合\" right after 真实派发, so traders see aggregated risk\n signals next to the verdict, not buried under the events dump. #}\n\n## {{ t(\"section.multi_chain.title\") }}\n\n| {{ t(\"section.multi_chain.table_header_item\") }} | {{ t(\"section.multi_chain.table_header_value\") }} |\n|---|---|\n{% for row in multi_chain.rows %}| {{ row.item | md_cell }} | {{ row.value | md_cell }} |\n{% endfor %}\n\n{{ multi_chain.gate_note | md_cell }}\n\n**{{ t(\"section.multi_chain.interpretation_label\") }}**: {{ multi_chain.interpretation | md_cell }}\n\n## {{ t(\"section.tge.title\") }}\n\n| {{ t(\"section.tge.table_header_anchor\") }} | {{ t(\"section.tge.table_header_utc\") }} | {{ t(\"section.tge.table_header_price\") }} | {{ t(\"section.tge.table_header_vs_current\") }} |\n|---|---|---|---|\n{% for row in tge.rows %}| {{ row.label | md_cell }} | {{ row.time | md_cell }} | {{ row.price | md_cell }} | {{ row.vs_current | md_cell }} |\n{% endfor %}\n\n**{{ t(\"section.multi_chain.interpretation_label\") }}**: {{ tge.interpretation | md_cell }}\n\n\u003ca id=\"section-alloc\">\u003c/a>\n## {{ t(\"section.alloc.title\") }}\n\n| {{ t(\"section.alloc.table_header_item\") }} | {{ t(\"section.alloc.table_header_value\") }} | {{ t(\"section.alloc.table_header_source\") }} |\n|---|---|---|\n{% for row in alloc.rows %}| {{ row.item | md_cell }} | {{ row.value | md_cell }} | {{ row.source | md_cell }} |\n{% endfor %}\n\n**{{ t(\"section.multi_chain.interpretation_label\") }}**: {{ alloc.interpretation | md_cell }}\n\n{# v0.7.16: 🔴 真实派发 moved up to right after 判定 — see top of report. #}\n## {{ t(\"section.cex_trace.title\") }}\n\n| {{ t(\"section.cex_trace.table_header_exchange\") }} | {{ t(\"section.cex_trace.table_header_status\") }} | {{ t(\"section.cex_trace.table_header_ts\") }} | {{ t(\"section.cex_trace.table_header_since\") }} |\n|---|---|---|---|\n{% for row in cex_trace.rows %}| {{ row.exchange | md_cell }} | {{ row.status | md_cell }} | {{ row.ts | md_cell }} | {{ row.since | md_cell }} |\n{% endfor %}\n\n{{ cex_trace.new_catalyst | md_cell }}. {{ cex_trace.tier_explanation | md_cell }}.\n\n**{{ t(\"section.multi_chain.interpretation_label\") }}**: {{ cex_trace.interpretation | md_cell }}\n\n\u003ca id=\"section-liq\">\u003c/a>\n## {{ t(\"section.liq.title\") }}\n\n| {{ t(\"section.liq.table_header_anchor\") }} | {{ t(\"section.liq.table_header_value\") }} | {{ t(\"section.liq.table_header_note\") }} |\n|---|---|---|\n{% for row in liq.rows %}| {{ row.anchor | md_cell }} | {{ row.value | md_cell }} | {{ row.note | md_cell }} |\n{% endfor %}\n\n**{{ t(\"section.multi_chain.interpretation_label\") }}**: {{ liq.interpretation | md_cell }}\n\n{# v0.7.16: verdict / decision_anchors / decision_action_block moved up to\n right after 决策摘要 — see top of report. #}\n\u003ca id=\"section-holdings\">\u003c/a>\n## {{ t(\"section.holdings.title\") }}\n\n{# v0.7.21.6: detect \"Alpha API totalSupply 滞后 / inflationary token\" case.\n OLAS surfaced this — its top-50 holders sum 504.97M > Alpha-API-reported\n 473.48M total_supply, producing a per-role pct > 100% that confused\n users. When the sum of all role balances exceeds the locked total_supply\n by > 5%, banner a warning before the bars so the reader knows the % is\n a Alpha-snapshot artefact, not a real overflow. #}\n{% set _alpha_supply = meta.total_supply if meta is defined and meta and meta.total_supply else 0 %}\n{# Jinja2 gotcha: variables set inside {% for %} are scoped to the loop\n and disappear after; using `namespace()` lets us mutate across iterations. #}\n{% set ns = namespace(holders_sum=0) %}\n{% for row in holdings_distribution.role_rows %}{% set ns.holders_sum = ns.holders_sum + (row.total_balance or 0) %}{% endfor %}\n{% if _alpha_supply and ns.holders_sum > _alpha_supply * 1.05 %}\n> {{ t(\"render.holdings_inflationary_banner\", holders_sum=\"{:,.0f}\".format(ns.holders_sum), alpha_supply=\"{:,.0f}\".format(_alpha_supply), pct=\"%.2f\"|format(ns.holders_sum / _alpha_supply * 100)) }}\n\n{% endif %}\n{# v0.8.0.4 methodology fix: ASCII bar chart was rendered inside ```...```\n code block with `{%- for` newline-stripping = all bars collapsed\n onto one unreadable line. User explicitly asked for table form.\n The \"持仓分布表\" below already presents the same data in clean\n markdown table form; dropping the ASCII chart avoids both redundancy\n and the {% for newline bug. Future re-introduction of a visual\n chart should use markdown table or embedded image, not pre-block\n ASCII art (the latter is unrenderable on monospace-stripping\n viewers like GitHub mobile / Notion). #}\n{{ t(\"section.holdings.table_h3\") }}:\n\n| {{ t(\"section.holdings.table_header_role\") }} | {{ t(\"section.holdings.table_header_n_wallets\") }} | {{ t(\"section.holdings.table_header_balance\") }} | {{ t(\"section.holdings.table_header_pct\") }} | {{ t(\"section.holdings.table_header_top_wallet\") }} |\n|---|---|---|---|---|\n{% for row in holdings_distribution.role_rows %}| {{ row.role_label | md_cell }} | {{ row.n_wallets }} | {{ \"{:,.0f}\".format(row.total_balance) }} | {{ \"%.4f\"|format(row.pct_of_total) if row.pct_of_total else \"—\" }}% | {% if row.top_addr_full %}[`{{ row.top_addr_short | md_cell }}`]({{ explorer_url(row.top_addr_full) }}){% else %}—{% endif %} |\n{% endfor %}\n\n{{ t(\"section.holdings.key_takeaways_h3\") }}:\n{# v0.8.4.9: 用 render-time 真实 m6 raw count, 不用 LLM key_takeaways\n (LLM 数 43+5+1=49 漏 2 个 vesting custody / dust wallet, 跟下面表里\n 51 行 m6_xxx 对不上). 真实 = lineage.m6.rows|length. #}\n{%- set _m6_total = lineage.m6.rows | length if lineage is defined and lineage and lineage.m6 is defined else 0 -%}\n{%- set _dt_safe = dump_tracking if dump_tracking is defined and dump_tracking else {} -%}\n{%- set _tree_pct = (_dt_safe.get('tree_holds_pct_supply') or 0) -%}\n{%- set _net_sellout_usd = (_dt_safe.get('confirmed_net_sellout_usd') or 0) -%}\n{%- set _sell_pct_circ = (_dt_safe.get('confirmed_total_pct') or 0) -%}\n{%- set _wash_summary = (wash_infrastructure.get('summary') if wash_infrastructure is defined and wash_infrastructure else None) -%}\n{%- set _wash_share_pct = ((_wash_summary.get('top_bot_share') or 0) * 100) if _wash_summary else 0 -%}\n{%- set _m6_other_top = _m6_total - _m6_full - _m6_partial - _m6_quiet -%}\n{# v0.8.4.9.1: 矿币 fallback (JCT 类) — m6 谱系空时关键解读用 insider_standard 数字 #}\n{% if _m6_total > 0 %}- {{ t(\"render.holdings_takeaway_m6\", total=_m6_total, tree_pct=\"%.1f\"|format(_tree_pct), full=_m6_full, partial=_m6_partial, quiet=_m6_quiet) }}{% if _m6_other_top > 0 %} {{ t(\"render.holdings_takeaway_m6_other\", n=_m6_other_top) }}{% endif %}.\n{% else %}- {{ t(\"render.holdings_takeaway_mining\", n=_insider_standard, tree_pct=\"%.1f\"|format(_tree_pct)) }}{% if _has_mfo %}{{ t(\"render.holdings_takeaway_mining_mfo\", n=_mfo_n) }}{% endif %}{% if _has_mad %}{{ t(\"render.holdings_takeaway_mining_mad\", n=_mad_n) }}{% endif %} {{ t(\"render.holdings_takeaway_mining_tail\") }}\n{% endif %}\n- {{ t(\"render.holdings_takeaway_outflow\", usd=\"{:,.1f}\".format(_net_sellout_usd / 1_000_000), pct=\"%.2f\"|format(_sell_pct_circ)) }}\n{% if _wash_share_pct > 0 %}- {{ t(\"render.holdings_takeaway_wash\", pct=\"%.1f\"|format(_wash_share_pct)) }}\n{% endif %}\n\n{# v0.7.22 P0 #3: lineage 谱系默认折叠. 散户 不需要逐行看 41 个内幕钱包,\n 汇总 行给\"项目方 → N 个一级钱包 + N 已分完 / N 分发中 / N 潜伏\"概要. #}\n{# v1.2.2: separate NEUTRAL infra (CEX custody / DEX pool) receivers from genuine\n insider wallets. The rows carry is_cex_custody / is_dex_infra; count only the\n insiders for the \"内幕钱包\" headline, and flag the infra rows in the table so a\n Binance-listing / liquidity-pool receiver isn't presented as an insider. #}\n{%- set _li = namespace(infra=0) -%}\n{%- for r in lineage.m6.rows -%}{%- if r.get('is_cex_custody') or r.get('is_dex_infra') -%}{%- set _li.infra = _li.infra + 1 -%}{%- endif -%}{%- endfor -%}\n{%- set _m6_insiders = (lineage.m6.rows | length) - _li.infra -%}\n\u003cdetails{% if mode == 'deep' %} open{% endif %}>\n\u003csummary>📂 \u003cstrong>{{ t(\"section.lineage.title\") }}\u003c/strong> — {{ t(\"render.lineage_summary\", n=_m6_insiders, full=lineage.m6.n_full_dumper or 0, partial=lineage.m6.n_partial_dumper or 0, quiet=lineage.m6.n_quiet or 0) }}{% if _li.infra > 0 %} {{ t(\"render.lineage_infra_suffix\", n=_li.infra) }}{% endif %}\u003c/summary>\n\n{{ t(\"section.lineage.table_h3\") }}:\n\n| {{ t(\"section.lineage.table_header_id\") }} | {{ t(\"section.lineage.table_header_addr\") }} | {{ t(\"section.lineage.table_header_recv\") }} | {{ t(\"section.lineage.table_header_balance\") }} | {{ t(\"section.lineage.table_header_dumped\") }} |\n|---|---|---|---|---|\n{% for r in lineage.m6.rows %}| `{{ r.m6_ref }}` | [`{{ r.addr_short | md_cell }}`]({{ explorer_url(r.addr_full) }}){% if r.get('is_cex_custody') %} 🏦{{ r.get('arkham_label') or 'CEX' }}{% elif r.get('is_dex_infra') %} 💧{{ r.get('arkham_label') or 'DEX' }}{% endif %} | {{ \"{:,.0f}\".format(r.received_from_deployer) }} | {{ \"{:,.0f}\".format(r.current_balance) if r.current_balance is not none else \"—\" }} | {{ (\"%.1f\"|format(r.dumped_pct) ~ \"%\") if r.dumped_pct is not none else \"—\" }} |\n{% endfor %}\n\n{%- set _m6_total_lineage = _m6_insiders -%}\n{%- set _m6_other = _m6_insiders - (lineage.m6.n_quiet or 0) - (lineage.m6.n_partial_dumper or 0) - (lineage.m6.n_full_dumper or 0) -%}\n_{{ t(\"render.lineage_stats\", total=_m6_total_lineage, quiet=lineage.m6.n_quiet or 0, partial=lineage.m6.n_partial_dumper or 0, full=lineage.m6.n_full_dumper or 0) }}{% if _m6_other > 0 %}{{ t(\"render.lineage_stats_other\", n=_m6_other) }}{% endif %}_\n\n{# v0.8.4.9.1: m4_notes 是 LLM fill 生成, 可能含跟 v0.8.4.9 数字统一\n 口径不符的 wallet count (老 fill 写 49 vs 真实 51). render-time\n regex 替换\"N 个内幕钱包\"为 hardcoded _m6_total_lineage. #}\n{{ t(\"section.lineage.m4_notes_h3\") }}:\n{% for n in lineage.m4_notes %}\n- {{ (n | md_cell) | regex_replace('\\\\d+ 个内幕钱包', (_m6_total_lineage|string) + ' 个内幕钱包') | regex_replace('\\\\d+ 个内幕地址', (_m6_total_lineage|string) + ' 个内幕地址') }}\n{% endfor %}\n\n{# v0.9.6 Fix #3 (adversarial review Windows EVAA 2026-06-15 feedback): surface\n step4 dumpers skipped due to chunk errors. Pre-v0.9.6 these were\n only logged to stderr; users had no in-report signal that part of\n the trace was incomplete. The skip itself is correct (any chunk\n error → drop dumper to avoid undercount), but silently losing data\n without showing it in the report violates the \"user knows what's\n in vs out\" principle. #}\n{# v0.9.6 Fix #3: guard with `.get(..., 0)` so old fixtures / skeletons\n missing the field render cleanly. #}\n{%- set _n_step4_skipped = (lineage.get('n_step4_skipped_dumpers', 0) if lineage is mapping else 0) or 0 -%}\n{% if _n_step4_skipped > 0 %}\n\n{{ t(\"render.step4_gap_intro\", n=_n_step4_skipped) }}\n{% for sd in lineage.get('step4_skipped_dumpers', []) %}\n- [`{{ sd.addr[:10] }}…`]({{ explorer_url(sd.addr) }}) — d{{ sd.depth }} — {{ sd.errored_chunks }}/{{ sd.total_chunks }} chunks errored\n{% endfor %}\n_{{ t(\"render.step4_gap_action\") }}_\n{% endif %}\n\n{# v0.9.7 adversarial review R1 Finding 6 (HIGH): step4 budget-cap truncation. When\n the dispersal tree is wider than MAX_PROMOTED_SUBDUMPERS (default 12),\n the largest N sub-dumpers are expanded + the rest skipped. The verdict\n is then a LOWER BOUND — disclose the skipped count + aggregate token\n size so the reader knows the operator tree is under-counted (NOT a\n surf failure — an intentional budget cap; rerun with\n BINANCE_ALPHA_STEP4_MAX_DUMPERS=40 for full depth). #}\n{%- set _n_cap_skipped = (lineage.get('n_sub_dumpers_skipped', 0) if lineage is mapping else 0) or 0 -%}\n{%- set _cap_skipped_tokens = (lineage.get('n_sub_dumpers_skipped_tokens', 0) if lineage is mapping else 0) or 0 -%}\n{% if _n_cap_skipped > 0 %}\n\n{{ t(\"render.step4_cap_intro\", n=_n_cap_skipped, tokens=\"{:,.0f}\".format(_cap_skipped_tokens)) }}\n_{{ t(\"render.step4_cap_action\") }}_\n{% endif %}\n\n\u003c/details>\n\n{# v0.8.4.9: cross_sym 段整段删除. 用户 review (2026-06-11): 抓到的多是\n router / MEV bot / 套利 bot, 真\"跨币种庄家\"罕见 (\u003c 5%). 用户决策\n 不依赖此段. cross_sym_registry refresh 也从 pipeline disable.\n 节省 ~30 credits/run + cold-start 190 credits (7 天 TTL). #}\n{# v0.8.4.9.9: wash_infra 段整段删除. 用户 review (2026-06-11): Alpha 类\n token 默认含 wash, 用户 expect, \"对敲占 X%\" 数字对决策无价值. 庄家\n 筹码状况 / 内幕变现 / mint_authority 持仓全跟 wash 无关. 节省\n ~350 credits/token (~70% wash budget) ≈ $1.75. detector 也从\n pipeline disable. 跟 v0.8.4.9 删 flow_operators + cross_sym 同 pattern. #}\n{# v0.8.4.9: flow_operators 段整段删除. 用户 review (2026-06-11): 实际\n 抓到的 wallet 多是 PancakeSwap router 上的 MEV / sandwich bot (单一\n 签名 + 高频 router 交互 + 0 持仓), 不是项目方操盘, 对用户决策无价值.\n real \"跨币种 operator\" case 占比 \u003c 5%. detector 也从 pipeline disable. #}\n{# audit M2 fix: render gate must fire on error / skipped state too,\n otherwise surf-failed / Solana-skipped runs silently drop the entire\n section and retail loses the failure banner. The inner branches handle\n error / skipped / has-data dispatch. #}\n{% if funding_attribution is defined and funding_attribution and funding_attribution.get('summary') %}\n{% set _fa = funding_attribution %}\n{% set _fa_sum = _fa.summary %}\n{% if (_fa_sum.get('n_addrs_with_data') or 0) > 0 or _fa.get('_error') or _fa.get('_skipped') %}\n{# v0.7.23.1: reverse-direction funding source attribution table. Surfaces\n the high-value addresses with their mint% vs dex_buy% vs p2p% so the\n reader can tell mining-fed wallets (= operators / sockpuppet airdrop\n farms) apart from dex-fed wallets (= real retail buyers). Works the\n same for standard deployer-anchored tokens and for mining / bridge /\n airdrop tokens where rule_11's standard m6 trace returns empty. #}\n## {{ t(\"section.funding_attribution.title\") }}\n\n{# v0.8.0.2 render fix — same as wash_infra/flow_operators above. #}\n{{ t(\"section.funding_attribution.how_to_read\") | safe }}\n\n{% if _fa.get('_error') %}\n> {{ t(\"render.fa_error\", err=(_fa._error | md_cell), n=_fa_sum.n_addrs_queried) }}\n\n{% elif _fa.get('_skipped') %}\n> {{ t(\"render.fa_skipped\", reason=(_fa._skipped | md_cell)) }}\n\n{% else %}\n> {{ t(\"section.funding_attribution.summary_line\", n_queried=_fa_sum.n_addrs_queried, n_data=_fa_sum.n_addrs_with_data, n_mining=_fa_sum.n_mining_fed, n_dex=_fa_sum.n_dex_fed, n_p2p=_fa_sum.n_p2p_fed) }}\n\n{# audit M3 fix: ratio threshold instead of absolute count. The\n intent is \"mining is the dominant funding mode\" — for a 6-addr token\n 3/6 mining-fed (50%) should warn; for a 60-addr token 5/60 (8%) should\n not. Threshold: mining-fed share of addr-with-data ≥ 30%. #}\n{% set _ratio = (_fa_sum.n_mining_fed * 1.0) / (_fa_sum.n_addrs_with_data) if _fa_sum.n_addrs_with_data else 0 %}\n{% if _ratio >= 0.30 %}\n> {{ t(\"render.fa_mining_dominant\", n_mining=_fa_sum.n_mining_fed, n_data=_fa_sum.n_addrs_with_data, pct=\"%.0f\"|format(_ratio * 100)) }}\n{% endif %}\n\n| {{ t(\"section.funding_attribution.col_addr\") }} | {{ t(\"section.funding_attribution.col_role\") }} | {{ t(\"section.funding_attribution.col_total\") }} | {{ t(\"section.funding_attribution.col_mint_pct\") }} | {{ t(\"section.funding_attribution.col_dex_pct\") }} | {{ t(\"section.funding_attribution.col_p2p_pct\") }} |\n|---|---|---:|---:|---:|---:|\n{% set _ns = namespace(rows=0) %}\n{% for _addr, _v in (_fa.attributions.items() | sort(attribute='1.total', reverse=true)) %}\n{% if _v.total > 0 and _ns.rows \u003c 30 %}\n{% set _ns.rows = _ns.rows + 1 %}\n| [`{{ _addr[:10] }}…`]({{ explorer_url(_addr) }}) | {{ t(\"section.funding_attribution.primary_label.\" + (_v.primary_source or 'unknown')) }} | {{ \"{:,.0f}\".format(_v.total) }} | {% if _v.mint_pct is not none %}{{ \"%.1f\"|format(_v.mint_pct * 100) }}%{% else %}—{% endif %} | {% if _v.dex_buy_pct is not none %}{{ \"%.1f\"|format(_v.dex_buy_pct * 100) }}%{% else %}—{% endif %} | {% if _v.p2p_pct is not none %}{{ \"%.1f\"|format(_v.p2p_pct * 100) }}%{% else %}—{% endif %} |\n{% endif %}\n{% endfor %}\n\n{% if _fa._debug and (_fa._debug.get('sql_truncated_addr_n') or _fa._debug.get('pipeline_truncated_n')) %}\n> _{{ t(\"render.fa_scan_cap\", total=(_fa._debug.pipeline_input_total if _fa._debug.get('pipeline_input_total') else (_fa_sum.n_addrs_queried + (_fa._debug.get('sql_truncated_addr_n') or 0))), queried=_fa_sum.n_addrs_queried, truncated=(_fa._debug.get('pipeline_truncated_n') or _fa._debug.get('sql_truncated_addr_n') or 0)) }}_\n{% endif %}\n\n> _{{ t(\"render.fa_cex_withdraw_note\") }}_\n\n{# v0.7.23.2: mining-fed wallets' actual DEX dump + top outflow destinations.\n This is the data dump_tracker (a)(b) would have produced if rule_11's\n m6 trace had been populated; for mining/bridge tokens (rule_11 m6 empty)\n the dump signal lives here instead. #}\n{% if _fa.mining_fed_outflows is defined and _fa.mining_fed_outflows and _fa.mining_fed_outflows.get('per_addr') %}\n{% set _mfo = _fa.mining_fed_outflows %}\n{% set _mfo_sum = _mfo.summary %}\n\n### {{ t(\"render.mfo_h3\") }}\n\n{% if _mfo.get('_error') %}\n> {{ t(\"render.mfo_error\", err=(_mfo._error | md_cell)) }}\n\n{% else %}\n> {{ t(\"render.mfo_intro\", n=_mfo.per_addr | length, sold_tokens=\"{:,.0f}\".format(_mfo_sum.total_dex_sold_tokens), sold_usd=\"{:,.0f}\".format(_mfo_sum.total_dex_sold_usd), outflow=\"{:,.0f}\".format(_mfo_sum.total_outflow_tokens)) }}\n\n{% for _addr, _v in (_mfo.per_addr.items() | sort(attribute='1.dex_sells.sold_usd', reverse=true)) %}\n{% if _v.dex_sells.sold_usd > 0 or _v.total_outflow_tokens > 0 %}\n**[`{{ _addr[:14] }}…`]({{ explorer_url(_addr) }})** —\n{% if _v.dex_sells.sold_usd > 0 %} {{ t(\"render.mfo_addr_sold\", tokens=\"{:,.0f}\".format(_v.dex_sells.tokens_sold), swaps=\"{:,}\".format(_v.dex_sells.n_swaps), usd=\"{:,.0f}\".format(_v.dex_sells.sold_usd)) }}{% else %} {{ t(\"render.mfo_addr_no_sell\") }}{% endif %}.\n{% if _v.top_destinations %} {{ t(\"render.mfo_top_outflow\") }}\n{% for _d in _v.top_destinations %}\n> - [`{{ _d.dest[:14] }}…`]({{ explorer_url(_d.dest) }}) ← {{ \"{:,.0f}\".format(_d.amt) }} tokens {{ t(\"render.dest_n_tx\", n=_d.n_tx) }}{% if _d.arkham_entity_name %} · 🏷️ **{{ _d.arkham_entity_name | md_cell }}**{% if _d.arkham_label %} ({{ _d.arkham_label | md_cell }}){% endif %}{% elif _d.arkham_label %} · 🏷️ {{ _d.arkham_label | md_cell }}{% endif %}\n{% endfor %}\n{% endif %}\n\n{% endif %}\n{% endfor %}\n{% endif %}{# end mfo _error branch #}\n{% endif %}{# end mining_fed_outflows defined #}\n\n{# v0.7.24a: 🌉 Bridge / Mint Authority self-dump subsection. Parallel to\n ⛏️ Mining-fed: mint authorities are the bridge/staking/airdrop contracts\n that physically issue new supply (receive from 0x0). They are missed by\n mining-fed pathway because they DON'T receive from anyone — they ARE the\n mint source. For H: 0x6aa22cb8 minted 132.5B (1325% nominal supply!)\n and self-DEX-dumped 19.8B over 30d, fully invisible until v0.7.24a. #}\n{% if _fa.mint_authorities is defined and _fa.mint_authorities and _fa.mint_authorities.get('authorities') %}\n{% set _mauth = _fa.mint_authorities %}\n{% set _mauth_sum = _mauth.summary %}\n{% set _mad = _fa.mint_authority_dumps if _fa.mint_authority_dumps is defined else None %}\n{% set _mad_sum = _mad.summary if _mad and _mad.summary else None %}\n\n\u003ca id=\"section-bridge-mint\">\u003c/a>\n### {{ t(\"render.mauth_h3\") }}\n\n{% if _mauth.get('_error') %}\n> {{ t(\"render.mauth_error\", err=(_mauth._error | md_cell)) }}\n\n{% else %}\n> {{ t(\"render.mauth_intro\", n=_mauth_sum.n_authorities) }}\n\n{% set _ns_mauth = namespace(rows=0) %}\n| {{ t(\"render.mauth_col_addr\") }} | {{ t(\"render.mauth_col_arkham\") }} | {{ t(\"render.mauth_col_mint\") }} | {{ t(\"render.mauth_col_pct\") }} | {{ t(\"render.mauth_col_selfdex\") }} | USD ≈ |\n|---|---|---:|---:|---|---:|\n{% for _a in _mauth.authorities %}\n{% if not _a.is_excluded and _ns_mauth.rows \u003c 10 %}\n{% set _ns_mauth.rows = _ns_mauth.rows + 1 %}\n{% set _v = _mad.per_addr.get(_a.addr) if _mad and _mad.per_addr else None %}\n| [`{{ _a.addr[:14] }}…`]({{ explorer_url(_a.addr) }}) | {% if _a.arkham_entity_name %}{{ _a.arkham_entity_name | md_cell }}{% if _a.arkham_label %} ({{ _a.arkham_label | md_cell }}){% endif %}{% elif _a.arkham_label %}{{ _a.arkham_label | md_cell }}{% else %}_{{ t(\"render.label_unlabeled_new\") }}{% endif %} | {{ \"{:,.0f}\".format(_a.total_minted) }} | {% if _a.mint_pct_supply is not none %}{{ \"%.2f\"|format(_a.mint_pct_supply) }}%{% else %}—{% endif %} | {% if _v and _v.dex_sells.n_swaps > 0 %}{{ t(\"render.mauth_dex_swaps\", tokens=\"{:,.0f}\".format(_v.dex_sells.tokens_sold), swaps=\"{:,}\".format(_v.dex_sells.n_swaps)) }}{% else %}{{ t(\"render.mauth_no_selfdex\") }}{% endif %} | {% if _v and _v.dex_sells.sold_usd > 0 %}**${{ \"{:,.0f}\".format(_v.dex_sells.sold_usd) }}**{% else %}—{% endif %} |\n{% endif %}\n{% endfor %}\n\n{% if _mauth_sum.total_minted_aggregate > meta.total_supply %}\n> {{ t(\"render.mauth_inflationary\", minted=\"{:,.0f}\".format(_mauth_sum.total_minted_aggregate), supply=\"{:,.0f}\".format(meta.total_supply)) }}\n{% endif %}\n\n{% if _mad_sum and _mad_sum.total_dex_sold_usd > 0 %}\n> {{ t(\"render.mauth_selfdex_summary\", tokens=\"{:,.0f}\".format(_mad_sum.total_dex_sold_tokens), usd=\"{:,.0f}\".format(_mad_sum.total_dex_sold_usd)) }}\n{% endif %}\n\n{# Per-authority top destinations — keep brief, only show authorities with\n meaningful DEX self-dump activity #}\n{% if _mad and _mad.per_addr %}\n{% for _addr, _v in (_mad.per_addr.items() | sort(attribute='1.dex_sells.sold_usd', reverse=true)) %}\n{% if _v.dex_sells.sold_usd > 0 and _v.top_destinations %}\n\n**[`{{ _addr[:14] }}…`]({{ explorer_url(_addr) }})** {{ t(\"render.mauth_selfdex_per_addr\", usd=\"{:,.0f}\".format(_v.dex_sells.sold_usd)) }}\n{% for _d in _v.top_destinations[:3] %}\n> - [`{{ _d.dest[:14] }}…`]({{ explorer_url(_d.dest) }}) ← {{ \"{:,.0f}\".format(_d.amt) }} tokens {{ t(\"render.dest_n_tx\", n=_d.n_tx) }}{% if _d.arkham_entity_name %} · 🏷️ **{{ _d.arkham_entity_name | md_cell }}**{% if _d.arkham_label %} ({{ _d.arkham_label | md_cell }}){% endif %}{% elif _d.arkham_label %} · 🏷️ {{ _d.arkham_label | md_cell }}{% endif %}\n{% endfor %}\n{% endif %}\n{% endfor %}\n{% endif %}\n{% endif %}{# end mint_authorities _error branch #}\n{% endif %}{# end mint_authorities defined #}\n\n{# v0.7.24b: 🌊 High-throughput dump trace — operator wallets that ingested\n a meaningful allocation (1M-5% nominal supply) and cleared out (balance\n ≈ 0) via >= 1000 txs. Catches the sss_crypto Twitter thread profile\n (0x47a6e4e1: 30M / 79k tx cleared) which all v0.7.23.x detectors miss\n (60d window flow_operators / inflated balance threshold mining-fed /\n wash_infra not atomic). Already-labeled infra (DEX_POOL / CEX_HOT) is\n filtered upstream in forensic_pipeline. #}\n{% if _fa.high_throughput_dumpers is defined and _fa.high_throughput_dumpers and _fa.high_throughput_dumpers.get('dumpers') %}\n{% set _htd = _fa.high_throughput_dumpers %}\n{% set _htd_sum = _htd.summary %}\n\n\u003ca id=\"section-high-throughput\">\u003c/a>\n### {{ t(\"render.htd_h3\") }}\n\n{% if _htd.get('_error') %}\n> {{ t(\"render.htd_error\", err=(_htd._error | md_cell)) }}\n\n{% else %}\n> {{ t(\"render.htd_intro\", n=_htd_sum.n_dumpers) }}\n\n{# v0.7.28 selective dedupe: if a dumper's primary_role is NOT\n high_throughput_operator (i.e. it lives in CEX fan-out hub or\n mint authority as primary), show a compact cross-link row with\n key metrics (balance / throughput) instead of the full operator\n detail row. This trims ~15-20% off JCT-class reports where ~3-5\n HT entries are also fan-out hubs that already have full cards\n in the fan-out section. address_role_index lookup is O(1). #}\n{% set _ari = address_role_index | default({}) %}\n{% set _ns_htd = namespace(rows=0, dedup=0) %}\n| {{ t(\"render.htd_col_addr\") }} | {{ t(\"render.htd_col_role\") }} | {{ t(\"render.mauth_col_arkham\") }} | {{ t(\"render.htd_col_in\") }} | {{ t(\"render.htd_col_out\") }} | {{ t(\"render.htd_col_balance\") }} | {{ t(\"render.htd_col_ntx\") }} |\n|---|---|---|---:|---:|---:|---:|\n{% for _d in _htd.dumpers %}\n{% if not _d.get('is_excluded') and not _d.get('is_infra') and _ns_htd.rows \u003c 20 %}\n{% set _ns_htd.rows = _ns_htd.rows + 1 %}\n{% set _addr_lower = (_d.addr or '') | lower %}\n{% set _ari_e = _ari.get(_addr_lower) %}\n{% set _is_primary_here = (not _ari_e) or (_ari_e.primary_role == 'high_throughput_operator') %}\n{% if _is_primary_here %}| [`{{ _d.addr[:14] }}…`]({{ explorer_url(_d.addr) }}) | {{ t(\"render.htd_role_primary\") }} | {% if _d.get('arkham_entity_name') %}{{ _d.arkham_entity_name | md_cell }}{% if _d.get('arkham_label') %} ({{ _d.arkham_label | md_cell }}){% endif %}{% elif _d.get('arkham_label') %}{{ _d.arkham_label | md_cell }}{% else %}_{{ t(\"render.label_unlabeled_eoa\") }}{% endif %} | {{ \"{:,.0f}\".format(_d.total_in) }} | {{ \"{:,.0f}\".format(_d.total_out) }} | {{ \"{:,.0f}\".format(_d.balance) }} | {{ \"{:,}\".format(_d.n_tx) }} |\n{% else %}{% set _ns_htd.dedup = _ns_htd.dedup + 1 %}| [`{{ _d.addr[:14] }}…`]({{ explorer_url(_d.addr) }}) | [{{ t(\"render.htd_role_seeprimary\", label=(_ari_e.primary_section_label_zh | md_cell)) }}](#{{ _ari_e.primary_section_anchor }}) | _{{ t(\"render.htd_dedupe\") }}_ | {{ \"{:,.0f}\".format(_d.total_in) }} | _{{ t(\"render.htd_see_primary\") }}_ | {{ \"{:,.0f}\".format(_d.balance) }} | {{ \"{:,}\".format(_d.n_tx) }} |\n{% endif %}\n{% endif %}\n{% endfor %}\n{% if _ns_htd.dedup > 0 %}\n\n> _{{ t(\"render.htd_dedup_note\", n=_ns_htd.dedup) }}_\n{% endif %}\n\n{% if _htd_sum.n_dumpers > 20 %}\n> _{{ t(\"render.htd_top20_note\", n=_htd_sum.n_dumpers) }}_\n{% endif %}\n\n> {{ t(\"render.htd_footer\", throughput=\"{:,.0f}\".format(_htd_sum.total_throughput)) }}\n\n{% endif %}{# end high_throughput _error branch #}\n{% endif %}{# end high_throughput_dumpers defined #}\n\n{# v0.7.24e: 🎯 CEX 提币 大规模分发 控筹 detection. sss_crypto Twitter thread\n profile (BEAT 2026-05-21: Gate hot wallet → 1 hub → 10 sub-wallets each\n 0.1%+ supply, designed to disperse top100 concentration signal). The hub\n pattern: 5-50 fan-out recipients each receiving ≥ 100K tokens, where the\n hub's biggest sender is Arkham-confirmed CEX_DEPOSIT / CEX_HOT_WALLET. #}\n{% if _fa.cex_fanout_hubs is defined and _fa.cex_fanout_hubs and _fa.cex_fanout_hubs.get('hubs') %}\n{% set _cfh = _fa.cex_fanout_hubs %}\n{% set _cfh_sum = _cfh.summary %}\n\n\u003ca id=\"section-cex-fanout\">\u003c/a>\n### {{ t(\"render.cfh_h3\") }}\n\n{% if _cfh.get('_error') %}\n> {{ t(\"render.cfh_error\", err=(_cfh._error | md_cell)) }}\n\n{% else %}\n> {{ t(\"render.cfh_intro\", n=_cfh_sum.n_confirmed_hubs) }}\n\n> {{ t(\"render.cfh_threshold\", n_candidate=_cfh_sum.n_candidate_hubs, n_confirmed=_cfh_sum.n_confirmed_hubs) }}\n\n{% if _cfh_sum.net_structured_fanout_tokens_total is defined or _cfh_sum.net_fanout_tokens_total is defined %}{% set _net_total = _cfh_sum.net_structured_fanout_tokens_total if _cfh_sum.net_structured_fanout_tokens_total is defined else _cfh_sum.net_fanout_tokens_total %}{% set _net_recip = _cfh_sum.net_structured_unique_recipients if _cfh_sum.net_structured_unique_recipients is defined else _cfh_sum.net_fanout_unique_recipients %}\n> {{ t(\"render.cfh_net_total_prefix\", tokens=\"{:,.0f}\".format(_net_total)) }}{% if meta.circulating_supply and meta.circulating_supply > 0 %} {{ t(\"render.cfh_pct_circ\", pct=\"%.2f\"|format(_net_total / meta.circulating_supply * 100)) }}{% endif %}{% if meta.total_supply and meta.total_supply > 0 %} {{ t(\"render.cfh_pct_supply\", pct=\"%.2f\"|format(_net_total / meta.total_supply * 100)) }}{% endif %} {{ t(\"render.cfh_net_total_tail\", n=_net_recip) }}\n\n{% endif %}{% for _h in _cfh.hubs %}\n\n#### {{ t(\"render.cfh_hub_h4\") }} [`{{ _h.addr[:14] }}…`]({{ explorer_url(_h.addr) }})\n\n| {{ t(\"render.field_label\") }} | {{ t(\"render.field_value\") }} |\n|---|---|\n| {{ t(\"render.cfh_field_cex_source\") }} | **{{ _h.cex_source_entity or _h.cex_source_label or _h.cex_source }}** ({{ _h.cex_source_classification }}) — [`{{ _h.cex_source[:14] }}…`]({{ explorer_url(_h.cex_source) }}) |\n| {{ t(\"render.cfh_field_cex_inflow\") }} | **{{ \"{:,.0f}\".format(_h.cex_source_inflow_tokens) }} tokens** ({{ t(\"render.cfh_n_tx\", n=_h.cex_source_n_tx) }}) |\n| {{ t(\"render.cfh_field_recipients\") }} | {{ t(\"render.cfh_field_recipients_val\", n=_h.n_recipients) }} |\n{% if _h.net_fanout_tokens is defined %}| **{{ t(\"render.cfh_field_net\") }}** | **{{ \"{:,.0f}\".format(_h.net_fanout_tokens) }} tokens**{% if meta.circulating_supply and meta.circulating_supply > 0 %} {{ t(\"render.cfh_pct_circ\", pct=\"%.2f\"|format(_h.net_fanout_tokens / meta.circulating_supply * 100)) }}{% endif %}{% if meta.total_supply and meta.total_supply > 0 %} {{ t(\"render.cfh_pct_supply_paren\", pct=\"%.2f\"|format(_h.net_fanout_tokens / meta.total_supply * 100)) }}{% endif %} {{ t(\"render.cfh_net_recipients\", n=_h.net_fanout_recipients) }} |\n{% if _h.loopback_to_cex_tokens > 0 %}| {{ t(\"render.cfh_field_loopback\") }} | {{ t(\"render.cfh_field_loopback_val\", tokens=\"{:,.0f}\".format(_h.loopback_to_cex_tokens)) }} |\n{% endif %}{% if _h.inter_hub_shuffle_tokens > 0 %}| {{ t(\"render.cfh_field_shuffle\") }} | {{ t(\"render.cfh_field_shuffle_val\", tokens=\"{:,.0f}\".format(_h.inter_hub_shuffle_tokens)) }} |\n{% endif %}| {{ t(\"render.cfh_field_phase1_deprecated\") }} | ~~{{ \"{:,.0f}\".format(_h.total_out_tokens) }} tokens~~ {{ t(\"render.cfh_phase1_deprecated_note\") }} |\n{% else %}| {{ t(\"render.cfh_field_phase1_broken\") }} | **{{ \"{:,.0f}\".format(_h.total_out_tokens) }} tokens** {{ t(\"render.cfh_phase1_broken_val\", avg=\"{:,.0f}\".format(_h.avg_per_recipient), min=\"{:,.0f}\".format(_h.min_per_recipient)) }} |\n{% endif %}\n\n**{{ t(\"render.cfh_recipients_h\", n=_h.top_recipients | length) }}**:\n\n| {{ t(\"render.cfh_recip_col_wallet\") }} | {{ t(\"render.cfh_recip_col_received\") }} | n_tx |\n|---|---:|---:|\n{% for _r in _h.top_recipients %}| [`{{ _r.addr[:14] }}…`]({{ explorer_url(_r.addr) }}) | {{ \"{:,.0f}\".format(_r.amt) }} | {{ _r.n_tx }} |\n{% endfor %}\n\n{% endfor %}\n\n> _{{ t(\"render.cfh_retail_note\") }}_\n{% endif %}{# end cex_fanout _error branch #}\n{% endif %}{# end cex_fanout_hubs defined #}\n\n{# v0.7.24c: 🔗 Multi-chain dump trace. For tokens deployed on multiple\n chains (H = BSC + Ethereum), surface per-chain mint authorities + high-\n throughput dumpers from the OTHER chains. Catches Eleve's H finding:\n 0x9e995952 on Ethereum dumped 140M H. Primary chain (= the one whose\n results are in the main funding_attribution body) is not repeated here. #}\n{% if _fa.multi_chain is defined and _fa.multi_chain %}\n\n### {{ t(\"render.mc_h3\") }}\n\n> {{ t(\"render.mc_intro\", chain=meta.chain) }}\n\n{% for _chain_name, _mc in _fa.multi_chain.items() %}\n\n#### 🔗 {{ _chain_name | upper }} (CA: [`{{ _mc.ca[:14] }}…`]({{ explorer_url(_mc.ca) }}))\n\n{% if _mc.get('_error') %}\n> {{ t(\"render.mc_chain_error\", chain=_chain_name, err=(_mc._error | md_cell)) }}\n\n{% elif _mc.get('_skipped') %}\n> {{ t(\"render.mc_chain_skipped\", chain=_chain_name, reason=(_mc._skipped | md_cell)) }}\n\n{% else %}\n{% set _mc_auth = _mc.mint_authorities %}\n{% set _mc_ht = _mc.high_throughput_dumpers %}\n{% set _mc_auth_sum = _mc_auth.summary if _mc_auth else None %}\n{% set _mc_ht_sum = _mc_ht.summary if _mc_ht else None %}\n\n{% if _mc_auth_sum and _mc_auth_sum.n_authorities > 0 %}\n**{{ t(\"render.mc_auth_h\", n=_mc_auth_sum.n_authorities, minted=\"{:,.0f}\".format(_mc_auth_sum.total_minted_aggregate)) }}**\n\n{% set _ns_mc_auth = namespace(rows=0) %}\n| {{ t(\"render.mauth_col_addr\") }} | {{ t(\"render.mauth_col_arkham\") }} | {{ t(\"render.mc_auth_col_mint\") }} | {{ t(\"render.mauth_col_pct\") }} |\n|---|---|---:|---:|\n{% for _a in _mc_auth.authorities %}\n{% if not _a.is_excluded and _ns_mc_auth.rows \u003c 5 %}\n{% set _ns_mc_auth.rows = _ns_mc_auth.rows + 1 %}\n| [`{{ _a.addr[:14] }}…`]({{ explorer_url(_a.addr) }}) | {% if _a.get('arkham_entity_name') %}{{ _a.arkham_entity_name | md_cell }}{% if _a.get('arkham_label') %} ({{ _a.arkham_label | md_cell }}){% endif %}{% elif _a.get('arkham_label') %}{{ _a.arkham_label | md_cell }}{% else %}_{{ t(\"render.label_unlabeled\") }}{% endif %} | {{ \"{:,.0f}\".format(_a.total_minted) }} | {% if _a.mint_pct_supply is not none %}{{ \"%.2f\"|format(_a.mint_pct_supply) }}%{% else %}—{% endif %} |\n{% endif %}\n{% endfor %}\n{% endif %}\n\n{% if _mc_ht_sum and _mc_ht_sum.n_dumpers > 0 %}\n**{{ t(\"render.mc_ht_h\", n=_mc_ht_sum.n_dumpers, throughput=\"{:,.0f}\".format(_mc_ht_sum.total_throughput)) }}**\n\n{% set _ns_mc_ht = namespace(rows=0) %}\n| {{ t(\"render.htd_col_addr\") }} | {{ t(\"render.mauth_col_arkham\") }} | {{ t(\"render.mc_ht_col_in\") }} | {{ t(\"render.htd_col_balance\") }} | n_tx |\n|---|---|---:|---:|---:|\n{% for _d in _mc_ht.dumpers %}\n{% if not _d.get('is_excluded') and not _d.get('is_infra') and _ns_mc_ht.rows \u003c 10 %}\n{% set _ns_mc_ht.rows = _ns_mc_ht.rows + 1 %}\n| [`{{ _d.addr[:14] }}…`]({{ explorer_url(_d.addr) }}) | {% if _d.get('arkham_entity_name') %}{{ _d.arkham_entity_name | md_cell }}{% if _d.get('arkham_label') %} ({{ _d.arkham_label | md_cell }}){% endif %}{% elif _d.get('arkham_label') %}{{ _d.arkham_label | md_cell }}{% else %}_{{ t(\"render.label_unlabeled\") }}{% endif %} | {{ \"{:,.0f}\".format(_d.total_in) }} | {{ \"{:,.0f}\".format(_d.balance) }} | {{ \"{:,}\".format(_d.n_tx) }} |\n{% endif %}\n{% endfor %}\n{% endif %}\n\n{% if (not _mc_auth_sum or _mc_auth_sum.n_authorities == 0) and (not _mc_ht_sum or _mc_ht_sum.n_dumpers == 0) %}\n> _{{ t(\"render.mc_no_detection\") }}_\n{% endif %}\n{% endif %}{# end _mc._error / _skipped #}\n\n{% endfor %}\n\n> _{{ t(\"render.mc_footer\") }}_\n{% endif %}{# end multi_chain defined #}\n\n{% endif %}{# end _fa._error / _skipped branches #}\n{% endif %}{# end render gate (data-or-error-or-skipped) #}\n{% endif %}{# end funding_attribution defined #}\n\n{# v0.8.6.7: wallet_cluster_graph 单独段 — Bubblemaps-style cluster\n detector. 显示项目方 wallet ↔ wallet 直接转账形成的 cluster (非 CEX\n route / 非 mint route / 非 m6 lineage). v0.8.6.5.0 detector 已跑,\n 但之前没单独段, 用户看不到具体 cluster wallets 是哪些. 现在加. #}\n{% if wallet_cluster_graph is defined and wallet_cluster_graph\n and (wallet_cluster_graph.get('clusters') or []) %}\n\u003ca id=\"section-wallet-cluster-graph\">\u003c/a>\n## {{ t(\"render.wcg_h2\") }}\n\n> {{ t(\"render.wcg_intro\") }}\n\n{%- set _wcg_sum = wallet_cluster_graph.get('summary') or {} -%}\n| {{ t(\"render.wcg_col_metric\") }} | {{ t(\"render.field_value\") }} |\n|---|---|\n| {{ t(\"render.wcg_n_clusters\") }} | **{{ _wcg_sum.get('n_clusters') or 0 }}** |\n| {{ t(\"render.wcg_n_cluster_addrs\") }} | **{{ _wcg_sum.get('n_cluster_addrs_total') or 0 }}** |\n| {{ t(\"render.wcg_n_candidates\") }} | {{ _wcg_sum.get('n_candidates_input') or 0 }} |\n| {{ t(\"render.wcg_n_filtered_l1\") }} | {{ _wcg_sum.get('n_filtered_by_l1') or 0 }} |\n| {{ t(\"render.wcg_n_edges\") }} | {{ _wcg_sum.get('n_edges_total') or 0 }} |\n| {{ t(\"render.wcg_n_new\") }} | {{ _wcg_sum.get('n_new_in_op_union') or 0 }} |\n| {{ t(\"render.wcg_n_chunks\") }} | {{ _wcg_sum.get('n_chunks_run') or 0 }} |\n\n{% for _c in (wallet_cluster_graph.get('clusters') or []) %}\n#### {{ t(\"render.wcg_cluster_h4\", n=loop.index, wallets=_c.get('addrs') | length) }}\n\n| {{ t(\"render.field_label\") }} | {{ t(\"render.field_value\") }} |\n|---|---|\n| {{ t(\"render.wcg_cluster_wallets\") }} | {{ t(\"render.wcg_n_wallets\", n=_c.get('addrs') | length) }} |\n| {{ t(\"render.wcg_cluster_edges\") }} | {{ _c.get('n_edges') }} |\n| {{ t(\"render.wcg_total_weight\") }} | {{ \"{:,.0f}\".format(_c.get('total_weight_tokens') or 0) }} tokens |\n| {{ t(\"render.wcg_max_edge\") }} | {{ \"{:,.0f}\".format(_c.get('max_edge_weight_tokens') or 0) }} tokens |\n| {{ t(\"render.wcg_cluster_balance\") }} | {{ \"{:,.0f}\".format(_c.get('cluster_balance_total_tokens') or 0) }} tokens |\n| Arkham UNLABELED % | {{ \"%.0f\"|format(_c.get('arkham_unlabeled_pct') or 0) }}% |\n{% if _c.get('time_window_days') is not none %}| {{ t(\"render.wcg_time_window\") }} | {{ t(\"render.wcg_time_window_val\", days=_c.get('time_window_days'), min_t=_c.get('min_block_time'), max_t=_c.get('max_block_time')) }} |\n{% endif %}{% if _c.get('source_overlap_counts') %}| {{ t(\"render.wcg_source_dist\") }} | {% for _s, _n in _c.get('source_overlap_counts').items() %}{{ _s }}: {{ _n }}{% if not loop.last %}, {% endif %}{% endfor %} |\n{% endif %}\n\n**{{ t(\"render.wcg_wallets_h\") }}**:\n\n| {{ t(\"render.wcg_wallet_col_n\") }} | {{ t(\"render.cfh_recip_col_wallet\") }} | {{ t(\"render.wcg_wallet_col_balance\") }} |\n|---:|---|---:|\n{% for _a in (_c.get('addrs') or [])[:30] %}| {{ loop.index }} | [`{{ _a[:14] }}`]({{ explorer_url(_a) }}) | {{ \"{:,.0f}\".format((_c.get('addr_balances') or {}).get(_a) or 0) }} |\n{% endfor %}{% if _c.get('addrs') | length > 30 %}| ... | {{ t(\"render.wcg_remainder\", n=(_c.get('addrs') | length - 30)) }} | |\n{% endif %}\n\n{% endfor %}\n{% endif %}\n\u003ca id=\"section-monitoring\">\u003c/a>\n## {{ t(\"section.monitoring.title\") }}\n\n{# v0.7.27 monitoring_ranker summary — 4-tier level tally from\n deterministic Python score formula. Helps retail prioritize the\n paste.json entries (CRITICAL > HIGH > NORMAL > NOT_TRACKED) instead\n of treating 50+ addresses as equal-weight. NOT_TRACKED hidden by\n default (infra / public CEX). See helpers/monitoring_ranker.py for\n formula + ChatGPT v0.8 spec section 4. #}\n{% if monitoring_summary is defined and monitoring_summary and monitoring_summary.level_counts %}\n{% set _lc = monitoring_summary.level_counts %}\n{% set _critical_n = _lc.get(\"CRITICAL\") or 0 %}\n{% set _high_n = _lc.get(\"HIGH\") or 0 %}\n{% set _normal_n = _lc.get(\"NORMAL\") or 0 %}\n{% set _nottracked_n = _lc.get(\"NOT_TRACKED\") or 0 %}\n{% set _active_n = _critical_n + _high_n + _normal_n %}\n\n> {{ t(\"render.mon_priority_line1\", critical=_critical_n, high=_high_n, normal=_normal_n) }}{% if _nottracked_n > 0 %} {{ t(\"render.mon_priority_nottracked\", n=_nottracked_n) }}{% endif %}\n>\n> {{ t(\"render.mon_priority_line2\", critical_high=(_critical_n + _high_n), normal=_normal_n) }}\n\n{% endif %}\n{% if monitoring_wallets|length > 10 -%}\n_{{ t(\"render.mon_top10_note\", n=monitoring_wallets|length) }}_\n{% endif %}\n\n{# v0.7.28 monitoring section: keep ALL entries (retail needs to import\n the whole list to set up tracking) but add a primary_role badge so\n \"wait, I've seen this address in another section\" can be answered at\n a glance. Address_role_index lookup is O(1). #}\n{% set _ari_mon = address_role_index | default({}) %}\n| {{ t(\"section.monitoring.table_header_n\") }} | {{ t(\"render.mon_col_level\") }} | {{ t(\"section.monitoring.table_header_wallet\") }} | {{ t(\"section.monitoring.table_header_role\") }} | {{ t(\"render.mon_col_primary_section\") }} | {{ t(\"render.mon_col_trigger\") }} | {{ t(\"section.monitoring.table_header_status\") }} |\n|---|:-:|---|---|---|---|---|\n{% for w in monitoring_wallets[:10] %}\n{% set _ml = w.monitor_level | default(\"NORMAL\") %}\n{% set _ml_emoji = \"🚨\" if _ml == \"CRITICAL\" else (\"🔥\" if _ml == \"HIGH\" else (\"👀\" if _ml == \"NORMAL\" else \"💤\")) %}\n{% set _addr_low = (w.addr_full or '') | lower %}\n{% set _ari_e = _ari_mon.get(_addr_low) %}\n{# v0.7.28.1 adversarial review MED fix: don't run md_cell on the assembled\n `[label](#anchor)` link — md_cell escapes `[` and `]` so the link\n would render as literal text. Instead apply md_cell to the label\n ONLY, then build markdown link syntax inline at render time. #}\n| {{ w.n }} | {{ _ml_emoji }} {{ _ml | md_cell }} | [`{{ w.addr_short | md_cell }}`]({{ explorer_url(w.addr_full) }}) | {{ w.role | md_cell }} | {% if _ari_e %}[{{ _ari_e.primary_section_label_zh | md_cell }}](#{{ _ari_e.primary_section_anchor }}){% else %}—{% endif %} | {{ (w.trigger_summary or w.alert) | md_cell }} | {{ w.status_emoji }} |\n{% endfor %}\n\n{{ monitoring_footer | md_cell }}\n\n{# v0.7.28.1 adversarial review MED fix: LLM safety-net was previously ONLY in the\n machine_readable_json fence (which LLM 二次解读 might skip). Render a\n compact markdown \"role overlap index\" for addresses appearing in 2+\n detector segments, so a reader scrolling the prose body also sees the\n cross-reference. Capped at 30 entries (sorted by all_roles length\n desc) to keep markdown body bounded. #}\n{% set _ari_render = address_role_index | default({}) %}\n{% set _multi_role_entries = [] %}\n{% for _addr, _info in _ari_render.items() -%}\n{% if (_info.all_roles | length) > 1 -%}\n{% set _ = _multi_role_entries.append((_addr, _info)) -%}\n{%- endif -%}\n{% endfor %}\n{% if _multi_role_entries | length > 0 %}\n## {{ t(\"render.mra_h2\") }}\n\n> {{ t(\"render.mra_intro\") }}\n\n| {{ t(\"render.cfh_recip_col_wallet\") }} | {{ t(\"render.mra_col_primary\") }} | {{ t(\"render.mra_col_all_roles\") }} |\n|---|---|---|\n{% set _ROLE_ZH_SHORT = {\n \"mint_authority\": t(\"render.role_short_mint_authority\"),\n \"cex_fanout_hub\": t(\"render.role_short_cex_fanout_hub\"),\n \"fanout_recipient\": t(\"render.role_short_fanout_recipient\"),\n \"direct_dumper\": t(\"render.role_short_direct_dumper\"),\n \"high_throughput_operator\": t(\"render.role_short_ht_operator\"),\n \"anomaly_participant\": t(\"render.role_short_anomaly\"),\n \"wash_operator\": t(\"render.role_short_wash\"),\n \"cross_alpha_whale\": t(\"render.role_short_cross_alpha_whale\"),\n \"top_holder\": t(\"render.role_short_top_holder\"),\n \"public_cex_hot_wallet\": t(\"render.role_short_cex_hot\"),\n \"public_cex_deposit\": t(\"render.role_short_cex_deposit\"),\n} %}\n{% for _addr, _info in (_multi_role_entries | sort(attribute='1.all_roles', reverse=true))[:30] -%}\n{# Sort key falls back lexicographically when length ties; cap 30 for size. #}\n{% set _short = _info.addr_short or (_addr[:10] if _addr.startswith('0x') else _addr[:8]) %}\n{% set _roles_zh = _info.all_roles | map('lower') | list %}\n{% set _roles_display = [] %}\n{% for _r in _info.all_roles %}{% set _ = _roles_display.append(_ROLE_ZH_SHORT.get(_r, _r)) %}{% endfor %}\n| [`{{ _short | md_cell }}`]({{ explorer_url(_addr) }}) | [{{ _info.primary_section_label_zh | md_cell }}](#{{ _info.primary_section_anchor }}) | {{ _roles_display | join(\", \") | md_cell }} |\n{% endfor %}\n\n> _{{ t(\"render.mra_footer\", n=_multi_role_entries | length) }}_\n\n{% endif %}\n## {{ t(\"section.machine_readable.title\") }}\n\n```json\n{{ machine_readable_json }}\n```\n\n---\n\n**{{ t(\"section.footer.disclaimer\", n=evidence_count) }}**\n\"\"\"\n\n\n# ============================================================\n# Security helpers (verbatim from v0.5)\n# ============================================================\n\ndef escape_md_cell(s):\n \"\"\"Escape markdown table-cell sensitive chars (pipe + newlines).\"\"\"\n if s is None:\n return \"\"\n if not isinstance(s, str):\n s = str(s)\n return s.replace(\"|\", \"\\\\|\").replace(\"\\n\", \" \").replace(\"\\r\", \" \")\n\n\ndef html_escape_finalize(s):\n \"\"\"Default finalize for jinja2 Environment — HTML escape + markdown-link\n neutralize on every `{{ ... }}` expression output. Stops user-controlled\n fields rendering as HTML, clickable links, or images.\n\n v0.8.0.5 fix: pass `Markup`-wrapped strings through unchanged. The\n template uses `{{ t(\"...\") | safe }}` to inject i18n strings that\n contain markdown blockquote `>` literals. Without this guard, the\n chained `Markup.replace(\">\", \">\")` re-escaped the `&` in `>`\n to `&gt;` (double escape, broke flow_operators / wash_infra /\n funding_attribution how_to_read_block sections).\n \"\"\"\n if s is None:\n return \"\"\n # Markup-marked strings (via `| safe`) are explicitly safe; never re-escape.\n from markupsafe import Markup\n if isinstance(s, Markup):\n return str(s)\n if not isinstance(s, str):\n s = str(s)\n return (\n s.replace(\"&\", \"&\")\n .replace(\"\u003c\", \"<\")\n .replace(\">\", \">\")\n .replace(\"[\", \"\\\\[\")\n .replace(\"]\", \"\\\\]\")\n .replace(\"!\", \"\\\\!\")\n )\n\n\ndef escape_mermaid_label(s):\n \"\"\"Neutralize mermaid label injection: `\"`, `|`, backtick, `[]`, `\u003c>`,\n `;`, edge tokens, newlines.\n \"\"\"\n if s is None:\n return \"\"\n if not isinstance(s, str):\n s = str(s)\n out = (\n s.replace('\"', \"'\")\n .replace(\"|\", \"·\")\n .replace(\"`\", \"\")\n .replace(\"[\", \"(\")\n .replace(\"]\", \")\")\n .replace(\"\u003c\", \"‹\")\n .replace(\">\", \"›\")\n .replace(\";\", \",\")\n .replace(\"\\n\", \" \")\n .replace(\"\\r\", \" \")\n )\n for token in (\"-->\", \"---\", \"==>\", \"===\"):\n out = out.replace(token, \" \")\n return out\n\n\ndef _normalize_for_width(s):\n \"\"\"Strip zero-width / control / format chars before width calc.\n\n adversarial review beta.6 9th-audit MED: combining marks (U+0300+), variation\n selectors (U+FE0F emoji modifier), zero-width joiners (U+200D in ZWJ\n sequences), and control chars miscount when each codepoint is treated\n independently. We don't implement full grapheme clustering (would need\n `regex` package); instead strip the obvious zero-width codepoints\n before measuring. Known limitation: ZWJ emoji sequences like 👨‍👩‍👧\n (5 codepoints, 1 grapheme) will still over-measure as 6 cols vs true\n 2 cols. Acceptable for our use case (Chinese labels rarely have ZWJ).\n \"\"\"\n import unicodedata\n out = []\n for ch in s or \"\":\n cat = unicodedata.category(ch)\n # Cc=Control, Cf=Format (incl. ZWJ U+200D), Mn=NonspacingMark (combining)\n if cat in (\"Cc\", \"Cf\", \"Mn\", \"Me\"):\n continue\n # Variation selectors (U+FE00-U+FE0F, U+E0100-U+E01EF) — width 0\n if 0xFE00 \u003c= ord(ch) \u003c= 0xFE0F or 0xE0100 \u003c= ord(ch) \u003c= 0xE01EF:\n continue\n out.append(ch)\n return \"\".join(out)\n\n\ndef _display_width(s):\n \"\"\"Visual column width — CJK + emoji count as 2, others as 1.\n\n v0.6.0-beta.6 fix: progress bar label `%-32s` used char count, but\n Chinese chars render as 2 columns in monospace → labels misaligned.\n \"\"\"\n import unicodedata\n width = 0\n for ch in _normalize_for_width(s):\n ea = unicodedata.east_asian_width(ch)\n if ea in (\"F\", \"W\") or unicodedata.category(ch) == \"So\":\n width += 2\n else:\n width += 1\n return width\n\n\ndef pad_display(s, target_width):\n \"\"\"Left-align by display column width (CJK-safe).\"\"\"\n if s is None:\n s = \"\"\n s = str(s)\n pad = max(0, int(target_width) - _display_width(s))\n return s + (\" \" * pad)\n\n\ndef truncate_display(s, max_width):\n \"\"\"Truncate to max display columns (CJK-safe), add ellipsis.\n\n `…` (U+2026) is itself 1 column. Reserve room. adversarial review beta.6\n 9th-audit MED: if remaining budget is 1 and we're about to add a\n 2-col char, we'd append `…` then return — but the result is still\n within budget. Verified: `out + \"…\"` length stays ≤ max_width\n because we exit BEFORE appending the 2-col char. OK.\n \"\"\"\n if s is None:\n return \"\"\n s = str(s)\n if _display_width(s) \u003c= max_width:\n return s\n if max_width \u003c 1:\n return \"\"\n out = \"\"\n w = 0\n for ch in _normalize_for_width(s):\n cw = _display_width(ch)\n # Reserve 1 col for `…`. If adding this char would push us past\n # max_width-1 (= room for `…`), stop and append ellipsis.\n if w + cw > max_width - 1:\n return out + \"…\"\n out += ch\n w += cw\n return out + \"…\" # consumed all chars but still over budget edge case\n\n\ndef role_to_cn(s):\n \"\"\"Translate role enum → localized flowchart node label.\n\n v0.6.2 (was v0.6.0-beta.8): now reads from i18n YAML\n (`role.\u003cENUM>.flowchart_node_label`) instead of hardcoded dict.\n Fallback chain: flowchart_node_label → short → label. Some roles\n (DEPLOYER, DEX_POOL, OTHER) don't have a dedicated flowchart label\n in i18n; we use their `short` field. Unmapped UPPERCASE enum →\n \"未知角色\" / \"Unknown role\" (fail-closed).\n \"\"\"\n if s is None:\n return \"\"\n key = str(s)\n # Try i18n lookup chain\n for field in (\"flowchart_node_label\", \"short\", \"label\"):\n val = t(f\"role.{key}.{field}\")\n if not val.startswith(\"[MISSING:\"):\n return val\n # All 3 fields missing → check if it looks like an enum\n import re\n if re.match(r\"^[A-Z][A-Z0-9_]{2,}$\", key):\n return t(\"role.unknown.label\")\n # Free-text already-localized — pass through.\n return key\n\n\ndef escape_mermaid_id(s):\n \"\"\"Mermaid node IDs must be alphanumeric-ish.\"\"\"\n if s is None:\n return \"\"\n if not isinstance(s, str):\n s = str(s)\n cleaned = \"\".join(c for c in s if c.isalnum() or c in \"_\")\n return cleaned or \"_node\"\n\n\n# ============================================================\n# Machine-readable footer\n# ============================================================\n\ndef _derive_chain_state_neutral_5tier(data: dict) -> dict:\n \"\"\"v0.7.25: Python mirror of the jinja2 5-tier derivation (see render\n template ~line 322-365). Same trigger rules — produces neutral\n forensic-state label for machine-readable JSON footer.\n\n Returns: {\"subtype\": str, \"label\": str, \"risk\": int}\n where subtype ∈ {CLEAN, WATCH, OPERATOR_DUMPED, DORMANT_INSIDER_RISK,\n RECENT_DISTRIBUTION}.\n \"\"\"\n lineage = data.get(\"lineage\") or {}\n m6 = lineage.get(\"m6\") or {}\n m6_quiet = m6.get(\"n_quiet\") or 0\n m6_full = m6.get(\"n_full_dumper\") or 0\n\n anomaly = data.get(\"anomaly\") or {}\n recent_n = 0\n for d in anomaly.get(\"detector_summary\") or []:\n lbl = d.get(\"label\") or \"\"\n if \"72h\" in lbl or \"近期\" in lbl:\n recent_n = d.get(\"count\") or 0\n break\n\n dump_tracking = data.get(\"dump_tracking\") or {}\n wash = dump_tracking.get(\"wash_dominated\") or False\n pure_pct = dump_tracking.get(\"pure_insider_holds_pct_supply\") or 0\n\n risk = 0\n subtype = \"CLEAN\"\n label = t(\"render.chain_state_clean_label\")\n\n if m6_quiet > 0 and pure_pct > 0.5:\n risk += 4\n subtype = \"DORMANT_INSIDER_RISK\"\n label = t(\"render.chain_state_dormant_label\", n=m6_quiet, pct=f\"{pure_pct:.1f}\")\n\n if recent_n >= 10:\n risk += 4\n subtype = \"RECENT_DISTRIBUTION\"\n label = t(\"render.chain_state_recent_label\", n=recent_n)\n elif recent_n >= 3:\n risk += 2\n if subtype == \"CLEAN\":\n subtype = \"WATCH\"\n label = t(\"render.chain_state_watch_label\", n=recent_n)\n\n if m6_full >= 3:\n risk += 3\n if subtype == \"CLEAN\":\n subtype = \"OPERATOR_DUMPED\"\n label = t(\"render.chain_state_dumped_label\", n=m6_full)\n\n if wash:\n risk += 2\n if pure_pct > 5:\n risk += 2\n risk = min(risk, 10)\n return {\"subtype\": subtype, \"label\": label, \"risk\": risk}\n\n\ndef compute_machine_readable_json(data: dict) -> str:\n \"\"\"v0.6 machine-readable footer: counts + verdict pulled from data,\n no hardcoded \"guarantees\" (audit v0.5 alpha.1 Critical 2 fix).\n \"\"\"\n meta = data.get(\"meta\", {})\n verdict = data.get(\"verdict\", {})\n tier = data.get(\"tier_classification\", {})\n chain_state = _derive_chain_state_neutral_5tier(data)\n counts = {\n \"anomaly_waves\": len((data.get(\"anomaly\") or {}).get(\"waves\") or []),\n \"evidence_graph_entries\": len(data.get(\"evidence_graph\") or {}),\n \"holdings_role_rows\": len((data.get(\"holdings_distribution\") or {}).get(\"role_rows\") or []),\n \"holdings_progress_bars\": len((data.get(\"holdings_distribution\") or {}).get(\"progress_bars\") or []),\n \"monitoring_wallets\": len(data.get(\"monitoring_wallets\") or []),\n \"lineage_flowchart_nodes\": len((data.get(\"lineage\") or {}).get(\"flowchart_nodes\") or []),\n \"lineage_flowchart_edges\": len((data.get(\"lineage\") or {}).get(\"flowchart_edges\") or []),\n \"m6_rows\": len(((data.get(\"lineage\") or {}).get(\"m6\") or {}).get(\"rows\") or []),\n \"decision_anchors\": len(data.get(\"decision_anchors\") or []),\n \"decision_re_entry_conditions\": len(\n (data.get(\"decision_action_block\") or {}).get(\"re_entry_conditions\") or []\n ),\n }\n out = {\n \"schema_version\": data.get(\"_schema_version\"),\n \"symbol\": meta.get(\"symbol\"),\n # v0.7.25: legacy 3-tier verdict (EXIT_IF_HOLDING etc) kept for\n # downstream schema compat. v0.8.0 will deprecate; prefer\n # `chain_state` / `chain_state_label` (neutral 5-tier) for new\n # parsers. AI parsing should treat `verdict` as legacy.\n \"verdict\": verdict.get(\"enum\"),\n \"verdict_zh\": verdict.get(\"cn_label\"),\n \"verdict_downgrade_applied\": verdict.get(\"downgrade_applied\", 0),\n # v0.7.25: render-side derived neutral 5-tier label, deterministic\n # from raw signals (m6_quiet / pure_pct / recent_anomaly_n / m6_full).\n # Pure on-chain state description, no buy/sell judgment.\n # 5 tiers: CLEAN / WATCH / OPERATOR_DUMPED / DORMANT_INSIDER_RISK /\n # RECENT_DISTRIBUTION.\n \"chain_state\": chain_state[\"subtype\"],\n \"chain_state_label\": chain_state[\"label\"],\n \"chain_state_risk_score\": chain_state[\"risk\"],\n \"alpha_listing_tier\": tier.get(\"tier\"),\n \"any_anomaly_firing\": counts[\"anomaly_waves\"] > 0,\n \"render_provenance\": {\n \"rendered_by\": \"render_report.py (v0.6, jinja2)\",\n \"data_source\": \"report_data.json (LLM-filled, Python-validated)\",\n \"deterministic\": True,\n },\n \"structural_counts\": counts,\n # v0.7.28 LLM safety-net: dump address_role_index so AI\n # 二次解读 can cross-reference duplicate-rendered addresses\n # without following markdown anchors. Compact form per entry\n # (skip the addr_short / primary_section_label_zh duplicates\n # of what's in the dict key) — just primary_role + all_roles\n # + anchor. For large reports (JCT 162 addrs), this is ~5KB.\n \"address_role_index\": {\n addr: {\n \"primary_role\": v.get(\"primary_role\"),\n \"all_roles\": v.get(\"all_roles\") or [],\n \"primary_section_anchor\": v.get(\"primary_section_anchor\"),\n }\n for addr, v in (data.get(\"address_role_index\") or {}).items()\n },\n }\n return json.dumps(out, ensure_ascii=False, indent=2)\n\n\n# ============================================================\n# Path / data hardening (verbatim from v0.5)\n# ============================================================\n\ndef _guard_out_path(out_path: str) -> Path:\n refuse_backslash = (os.name != \"nt\") and (\"\\\\\" in out_path)\n if \"..\" in out_path or refuse_backslash or \"\\x00\" in out_path:\n print(\n f\"REFUSED: out_path={out_path!r} contains '..', a POSIX-illegal \"\n \"'\\\\\\\\', or NUL byte.\",\n file=sys.stderr,\n )\n sys.exit(1)\n out_p = Path(out_path)\n if out_p.is_symlink():\n print(f\"REFUSED: out_path={out_path!r} is itself a symlink.\", file=sys.stderr)\n sys.exit(1)\n if out_p.exists() and out_p.is_file():\n try:\n if out_p.stat().st_nlink > 1:\n print(\n f\"REFUSED: out_path={out_path!r} is a hardlink \"\n \"(st_nlink > 1).\",\n file=sys.stderr,\n )\n sys.exit(1)\n except OSError:\n pass\n return out_p\n\n\ndef _load_json_hardened(path: str, label: str) -> dict:\n if not Path(path).is_file():\n print(\n f\"REFUSED: {label}={path!r} is not a regular file \"\n \"(FIFO/socket/dir blocked).\",\n file=sys.stderr,\n )\n sys.exit(1)\n raw_bytes = Path(path).read_bytes()\n try:\n raw_text = raw_bytes.decode(\"utf-8\")\n except UnicodeDecodeError as e:\n print(f\"REFUSED: {label} is not valid UTF-8 — {e}\", file=sys.stderr)\n sys.exit(1)\n try:\n raw_text.encode(\"utf-8\", errors=\"strict\")\n except UnicodeEncodeError as e:\n print(\n f\"REFUSED: {label} contains unpaired surrogate / invalid codepoint — {e}\",\n file=sys.stderr,\n )\n sys.exit(1)\n try:\n data = json.loads(raw_text)\n except json.JSONDecodeError as e:\n print(\n f\"REFUSED: {label} not valid JSON — {e.msg} at line {e.lineno} col {e.colno}.\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n # Iterative depth-cap walk\n MAX_NEST_DEPTH = 64\n stack = [(data, \"$\", 0)]\n while stack:\n node, p, depth = stack.pop()\n if depth > MAX_NEST_DEPTH:\n print(\n f\"REFUSED: {label} exceeds max nesting depth ({MAX_NEST_DEPTH}) at {p}.\",\n file=sys.stderr,\n )\n sys.exit(1)\n if isinstance(node, str):\n for ch in node:\n cp = ord(ch)\n if 0xD800 \u003c= cp \u003c= 0xDFFF:\n print(\n f\"REFUSED: {label} contains unpaired surrogate U+{cp:04X} at {p}.\",\n file=sys.stderr,\n )\n sys.exit(1)\n elif isinstance(node, dict):\n for k, v in node.items():\n stack.append((v, f\"{p}.{k}\", depth + 1))\n elif isinstance(node, list):\n for i, v in enumerate(node):\n stack.append((v, f\"{p}[{i}]\", depth + 1))\n return data\n\n\n# ============================================================\n# v0.7.2 smoke fixture fingerprint\n# ============================================================\n\n# NATO phonetic alphabet (lowercase) — exactly the set used by\n# tests/smoke_fill.py to suffix per-index variants like \"(alpha variant)\".\n_SMOKE_NATO_WORDS = frozenset((\n \"alpha\", \"bravo\", \"charlie\", \"delta\", \"echo\", \"foxtrot\",\n \"golf\", \"hotel\", \"india\", \"juliet\", \"kilo\", \"lima\",\n \"mike\", \"november\", \"oscar\", \"papa\", \"quebec\", \"romeo\",\n \"sierra\", \"tango\", \"uniform\", \"victor\", \"whiskey\", \"xray\",\n \"yankee\", \"zulu\",\n))\n\n# Match the literal \"(word variant)\" pattern smoke_fill writes.\n# v0.7.2 hardening (adversarial-review audit finding #1): tolerate adversarial\n# formatting variants — uppercase, mixed case, extra/missing whitespace\n# around the parens and word, NBSP, Unicode confusables. Strings are\n# NFKC-normalized + casefolded before regex match, so:\n# \"(Alpha variant)\" → matches alpha\n# \"(ALPHA VARIANT)\" → matches alpha\n# \"( alpha\\tvariant )\" → matches alpha\n# \"(αlpha variant)\" (Greek) → NFKC keeps the Greek α, regex still\n# fails to match — but `_NORMALIZE`\n# treats it as the same visual class\n# via the confusable substitution map.\n# Regex itself uses \\s+ for whitespace and is case-insensitive (paranoid\n# belt+suspenders even though casefold() already lowered).\nimport re as _re # noqa: E402 (local re alias keeps the symbol private)\nimport unicodedata as _unicodedata # noqa: E402\n\n_SMOKE_NATO_RE = _re.compile(\n r\"\\(\\s*([a-z]+)\\s+variant\\s*\\)\",\n _re.IGNORECASE,\n)\n\n# Confusable letter substitutions — homoglyphs commonly used to evade\n# pattern matching. Maps Unicode visual-equivalents to their ASCII Latin\n# counterparts so e.g. \"αlpha\" (Greek α + \"lpha\") collapses to \"alpha\".\n# Curated to the lookalikes that actually appear in adversarial text\n# generators; not exhaustive, but defends the obvious bypass class.\n_SMOKE_CONFUSABLE_MAP = str.maketrans({\n \" \": \" \", # NBSP → space\n \" \": \" \", # figure space\n \" \": \" \", # thin space\n \"​\": \"\", # zero-width space → strip\n \"‌\": \"\", # ZWNJ → strip\n \"‍\": \"\", # ZWJ → strip\n \"\": \"\", # BOM → strip\n # Latin lookalikes for ASCII letters that appear in NATO words.\n \"α\": \"a\", # Greek small alpha\n \"а\": \"a\", # Cyrillic small a\n \"е\": \"e\", # Cyrillic small e\n \"ο\": \"o\", # Greek small omicron\n \"о\": \"o\", # Cyrillic small o\n \"і\": \"i\", # Cyrillic small i\n \"ι\": \"i\", # Greek small iota\n \"р\": \"p\", # Cyrillic small er\n \"с\": \"c\", # Cyrillic small es\n \"х\": \"x\", # Cyrillic small ha\n \"у\": \"y\", # Cyrillic small u\n})\n\n\ndef _normalize_for_fingerprint(s: str) -> str:\n \"\"\"NFKC + casefold + confusable substitution. Idempotent.\"\"\"\n return _unicodedata.normalize(\"NFKC\", s).translate(_SMOKE_CONFUSABLE_MAP).casefold()\n\n\ndef _count_nato_smoke_fingerprints(obj) -> int:\n \"\"\"Walk `obj` recursively and count *distinct* NATO words that appear\n inside a `(\u003cword> variant)` suffix. Returns the count.\n\n De-duping by word means a single repeated stub doesn't trip the gate\n (e.g. 5 copies of `(alpha variant)` count as 1); the gate fires when\n multiple distinct NATO words appear, which is the signature of\n smoke_fill walking through indexed lists.\n\n v0.7.2 hardening: input strings are NFKC-normalized + casefolded +\n confusable-substituted before matching, so uppercase / whitespace /\n homoglyph / NBSP bypasses are caught. Only dict VALUES are walked;\n dict keys are skipped (smoke_fill never targets keys).\n \"\"\"\n found: set[str] = set()\n stack = [obj]\n while stack:\n node = stack.pop()\n if isinstance(node, str):\n normalized = _normalize_for_fingerprint(node)\n for word in _SMOKE_NATO_RE.findall(normalized):\n if word in _SMOKE_NATO_WORDS:\n found.add(word)\n elif isinstance(node, dict):\n stack.extend(node.values())\n elif isinstance(node, list):\n stack.extend(node)\n return len(found)\n\n\n# ============================================================\n# Render pipeline\n# ============================================================\n\n# v0.7.21.6: human-readable explanation per abort reason. Keys match the\n# `_reason` strings written by forensic_pipeline.build_skeleton — keep in\n# sync when adding new abort paths. Each entry is (title, body_md) where\n# body_md uses {ca} and {detail} placeholders.\n# v0.6.2 i18n: abort explainer titles + bodies are looked up at render time\n# via t() so they localize. Keyed by reason → (title_key, body_key).\n_ABORT_EXPLAINER_KEYS = {\n \"INVALID_CA\": (\"render.abort_invalid_ca_title\", \"render.abort_invalid_ca_body\"),\n \"SPOT_GRADUATED\": (\"render.abort_spot_graduated_title\", \"render.abort_spot_graduated_body\"),\n \"NEVER_ALPHA\": (\"render.abort_never_alpha_title\", \"render.abort_never_alpha_body\"),\n \"unsupported_chain\": (\"render.abort_unsupported_chain_title\", \"render.abort_unsupported_chain_body\"),\n \"missing_chain_id\": (\"render.abort_missing_chain_id_title\", \"render.abort_missing_chain_id_body\"),\n}\n\n\ndef _render_abort_report(skel: dict, out_p) -> int:\n \"\"\"v0.7.21.6: write a short human-readable report.md for abort skeletons.\n\n Skeleton schema: `_status=\"abort\", _reason=\u003ckey>, _detail=\u003ctext>,\n _timings={...}`. We map `_reason` through `_ABORT_EXPLAINER_KEYS` to\n i18n title + body keys, render markdown, prepend the UTF-8 BOM (same convention\n as the normal report path), and write to the output file.\n\n Returns 0 (success). The user gets a tangible file artifact even when\n the pipeline couldn't actually run forensic SQL.\n \"\"\"\n reason_key = skel.get(\"_reason\") or \"UNKNOWN\"\n detail = skel.get(\"_detail\") or \"(no detail available)\"\n timings = skel.get(\"_timings\") or {}\n title_key, body_key = _ABORT_EXPLAINER_KEYS.get(reason_key, (None, None))\n if title_key:\n title = t(title_key)\n body = t(body_key)\n else:\n title = t(\"render.abort_unknown_title\", reason=reason_key)\n body = t(\"render.abort_unknown_body\")\n\n lines = [\n f\"# 🚫 {title}\",\n \"\",\n f\"> {t('render.abort_intro')}\",\n \"\",\n f\"## {t('render.abort_detail_h2')}\",\n \"\",\n f\"- **{t('render.abort_reason_label')}**: `{reason_key}`\",\n f\"- **{t('render.abort_detail_label')}**: {detail}\",\n f\"- **{t('render.abort_section_a_time')}**: {timings.get('section_a', 0)*1000:.2f} ms\",\n \"\",\n f\"## {t('render.abort_interpretation_h2')}\",\n \"\",\n body,\n \"\",\n \"---\",\n \"\",\n f\"_{t('render.abort_footer')}_\",\n \"\",\n ]\n bom = \"\"\n md = bom + \"\\n\".join(lines)\n out_p.write_bytes(md.encode(\"utf-8\"))\n print(f\"OK: rendered abort report {out_p} ({len(md)} bytes, reason={reason_key})\")\n return 0\n\n\n# ============================================================\n# v0.8.0 TLDR mode template — minimal战情摘要 (4-6KB target).\n# Independent from the full TEMPLATE constant above; reuses the same\n# Jinja2 environment + filters. ChatGPT v0.8 spec section 7.4.\n# Sections (5):\n# 1. 一句话状态 (chain_state_label)\n# 2. Top 3 active behaviors (from behavior_profile, sorted by severity)\n# 3. 监控等级 汇总 (level_counts)\n# 4. Top 10 CRITICAL wallets (filtered from monitoring_wallets)\n# 5. 主要盲区 (1-line 汇总 of 链上侦测 limits)\n# Use case: Discord/Telegram bot forwarding, mobile preview, TL;DR.\n# Trade-off: omits all detector detail — readers needing forensic\n# evidence should use --mode=default.\n# ============================================================\n_TLDR_TEMPLATE = \"\"\"\\\n# {{ meta.symbol or \"—\" }} ({{ meta.name | md_cell }}) — {{ t(\"render.tldr_title_suffix\") }}\n\n_v{{ _schema_version }} · {{ meta.chain }} · {{ t(\"render.market_alpha_listed\") }} {{ meta.alpha_listing_date_utc or \"—\" }}_\n\n## {{ t(\"render.tldr_chain_state_h2\") }}\n\n**`{{ _v.subtype }}`** ({{ _v.risk }}/10) — {{ _v.label | md_cell }}\n\n{# v0.8.0 fix: use dict.get() instead of attribute access since\n StrictUndefined raises on absent keys (ZEST fixture only has subset\n of meta fields). Falls back gracefully to \"—\" when both rti +\n alpha_* are missing. #}\n{% set rti = meta.get('realtime_token_info') or {} %}\n{% set _price = rti.get('price_usd') or meta.get('alpha_price_usd') %}\n{% set _vol = rti.get('volume_24h_usd') or meta.get('alpha_vol_24h_usd') %}\n{% set _lp = rti.get('liquidity_usd') or meta.get('alpha_liquidity_usd') %}\n- {{ t(\"render.tldr_current_price\") }}: **${% if _price is not none %}{{ \"%.4g\" | format(_price) }}{% else %}—{% endif %}**\n- {{ t(\"render.tldr_vol\") }}: **${% if _vol is not none %}{{ \"{:,.0f}\".format(_vol) }}{% else %}—{% endif %}**\n- {{ t(\"render.tldr_lp\") }}: **${% if _lp is not none %}{{ \"{:,.0f}\".format(_lp) }}{% else %}—{% endif %}**\n\n## {{ t(\"render.tldr_behavior_h2\") }}\n\n{# v0.8.0.1 adversarial review MED fix: nested .get() guards for behavior_profile\n sub-dicts (active_labels / by_label / label_names_zh) so malformed\n profile doesn't crash render. #}\n{% if behavior_profile is defined and behavior_profile and behavior_profile.get('active_labels') %}\n{% set _top3 = behavior_profile.active_labels[:3] %}\n| {{ t(\"render.behavior_col_label\") }} | {{ t(\"render.behavior_col_severity\") }} | {{ t(\"render.behavior_col_fact\") }} |\n|---|:-:|---|\n{% for lid in _top3 %}\n{% set info = (behavior_profile.get('by_label') or {}).get(lid) or {} %}\n{% set sev_emoji = \"🔴\" if info.get('severity') == \"STRONG\" else (\"🟠\" if info.get('severity') == \"MEDIUM\" else \"🟡\") %}\n| `{{ lid }}` {{ (behavior_profile.get('label_names_zh') or {}).get(lid) or lid }} | {{ sev_emoji }} {{ info.get('severity') or \"—\" }} | {{ (info.get('human_summary_zh') or \"—\") | md_cell }} |\n{% endfor %}\n{% if behavior_profile.active_labels | length > 3 %}\n\n_{{ t(\"render.tldr_more_behaviors\", n=(behavior_profile.active_labels | length - 3)) }}_\n{% endif %}\n{% else %}\n> {{ t(\"render.tldr_behavior_none\") }}\n{% endif %}\n\n## {{ t(\"render.tldr_mon_h2\") }}\n\n{% if monitoring_summary is defined and monitoring_summary and monitoring_summary.level_counts %}\n{% set _lc = monitoring_summary.level_counts %}\n- 🚨 **CRITICAL**: {{ _lc.get(\"CRITICAL\") or 0 }} {{ t(\"render.tldr_mon_critical\") }}\n- 🔥 **HIGH**: {{ _lc.get(\"HIGH\") or 0 }} {{ t(\"render.tldr_mon_high\") }}\n- 👀 **NORMAL**: {{ _lc.get(\"NORMAL\") or 0 }} {{ t(\"render.tldr_mon_normal\") }}\n- 💤 **NOT_TRACKED**: {{ _lc.get(\"NOT_TRACKED\") or 0 }} {{ t(\"render.tldr_mon_nottracked\") }}\n\n_{{ t(\"render.tldr_mon_paste\") }}_\n{% else %}\n_{{ t(\"render.tldr_mon_nodata\") }}_\n{% endif %}\n\n## {{ t(\"render.tldr_critical_wallets_h2\") }}\n\n{# v0.8.0.1 adversarial review MED fix: defensive guard. StrictUndefined raises\n if monitoring_wallets is absent (e.g. malformed skeleton). #}\n{% set _critical_wallets = [] %}\n{% if monitoring_wallets is defined and monitoring_wallets %}\n{% for w in monitoring_wallets %}\n{% if (w.get(\"monitor_level\") or \"\") == \"CRITICAL\" %}\n{% set _ = _critical_wallets.append(w) %}\n{% endif %}\n{% endfor %}\n{% endif %}\n{% if _critical_wallets | length > 0 %}\n| {{ t(\"render.cfh_recip_col_wallet\") }} | {{ t(\"section.monitoring.table_header_role\") }} | {{ t(\"render.mon_col_trigger\") }} |\n|---|---|---|\n{% for w in _critical_wallets[:10] %}| [`{{ w.addr_short | md_cell }}`]({{ explorer_url(w.addr_full) }}) | {{ w.monitor_role_enum or w.role | md_cell }} | {{ (w.trigger_summary or w.alert) | md_cell }} |\n{% endfor %}\n{% if _critical_wallets | length > 10 %}\n\n_{{ t(\"render.tldr_more_critical\", n=(_critical_wallets | length - 10)) }}_\n{% endif %}\n{% else %}\n_{{ t(\"render.tldr_no_critical\") }}_\n{% endif %}\n\n## {{ t(\"render.tldr_blindspot_h2\") }}\n\n{# v0.8.0.1 adversarial review MED fix: defensive .get() chains throughout —\n `lineage.m6` may be explicitly None per adversarial review repro, not just absent. #}\n{% set _caveat_parts = [] %}\n{% if dump_tracking is defined and dump_tracking and dump_tracking.get('wash_dominated') %}\n{% set _ = _caveat_parts.append(t(\"render.tldr_caveat_wash\")) %}\n{% endif %}\n{% set _m6_for_caveat = (lineage.get('m6') or {}) if (lineage is defined and lineage) else {} %}\n{% if _m6_for_caveat and (_m6_for_caveat.get('rows') or []) | length == 0 and lineage.get('deployer_addr') %}\n{% set _ = _caveat_parts.append(t(\"render.tldr_caveat_mining\")) %}\n{% endif %}\n{% set _mc_chains_summary = [] -%}\n{% set _mc_for_caveat = (funding_attribution.get('multi_chain') or {}) if (funding_attribution is defined and funding_attribution) else {} -%}\n{% for _c in (_mc_for_caveat.keys() | list) if not _c.startswith(\"_\") -%}\n{% set _ = _mc_chains_summary.append(_c) -%}\n{%- endfor -%}\n{% if _mc_chains_summary %}\n{% set _ = _caveat_parts.append(t(\"render.tldr_caveat_cross_chain\", chains=(_mc_chains_summary | join(\", \")))) %}\n{% endif %}\n{% if _caveat_parts | length > 0 %}\n{% for c in _caveat_parts %}\n- {{ c | md_cell }}\n{% endfor %}\n{% else %}\n- {{ t(\"render.tldr_caveat_none\") }}\n{% endif %}\n\n{{ t(\"render.tldr_blindspot_footer\") }}\n\n---\n\n_{{ t(\"render.tldr_mode_footer\") }}_\n\n{# v0.8.0.1 adversarial review HIGH fix: disclaimer wording previously contained\n \"建议\" (banned word). Rephrased to neutral 链上侦测-only language. #}\n**{{ t(\"render.tldr_disclaimer\") }}**\n\"\"\"\n\n\ndef render(skeleton_path: str, filled_path: str, out_path: str, mode: str = \"default\") -> int:\n \"\"\"Render report.md from validated v0.6 filled report_data.json.\n\n Validator is MANDATORY pre-render — invokes validate_report_data.py\n as a subprocess (matches v0.5 audit pattern: validator is the gate,\n renderer trusts validated data).\n\n v0.8.0 mode parameter:\n - \"default\" (current behavior, full forensic 18-28KB)\n - \"tldr\" (5-section minimal 4-6KB)\n - \"deep\" (default template + `\u003cdetails open>` everywhere)\n \"\"\"\n # v0.8.0.1 adversarial review MED fix: argparse choices=() guards the CLI path,\n # but direct Python callers could pass a typo and silently fall\n # through to the default template. Fail loud on invalid mode.\n if mode not in (\"tldr\", \"default\", \"deep\"):\n raise ValueError(\n f\"render(mode={mode!r}) invalid; must be one of \"\n f\"'tldr' / 'default' / 'deep'\"\n )\n out_p = _guard_out_path(out_path)\n skel = _load_json_hardened(skeleton_path, \"skeleton\")\n\n # v0.7.21.6: short-circuit on abort skeleton. forensic_pipeline writes\n # a 200-byte abort marker for INVALID_CA / SPOT_GRADUATED / NEVER_ALPHA\n # / UNSUPPORTED_CHAIN cases (Solana base58 CAs go here today). Pre-\n # v0.7.21.6 the user got no report file at all — only a stdout banner\n # they'd have to scroll back to read. Now we always emit a short\n # human-readable explanation so the file artifact is the source of\n # truth. No filled.json required; no validator run; minimal cost.\n if isinstance(skel, dict) and skel.get(\"_status\") == \"abort\":\n return _render_abort_report(skel, out_p)\n\n filled = _load_json_hardened(filled_path, \"filled\")\n\n # v0.7.2 SMOKE FIXTURE GATE — bash-level enforcement (cannot be\n # bypassed by docs/SKILL.md guidance alone). Two layers:\n #\n # 1. Flag check: tests/smoke_fill.py writes `_smoke_test_fixture: true`\n # at top level. Render refuses (exit 3) if present.\n #\n # 2. Fingerprint check: even if the flag is stripped, smoke_fill stubs\n # use NATO-alphabet suffixes (e.g. \"(alpha variant)\", \"(bravo\n # variant)\") that are vanishingly unlikely in real narrative.\n # Threshold: ≥3 distinct NATO suffixes across narrative strings.\n # Strings are NFKC-normalized + casefolded + confusable-substituted\n # before matching, so case/whitespace/homoglyph bypasses fail.\n #\n # Override (adversarial-review audit finding #3 hardening): both env vars\n # required — single-flag override was too easy to set accidentally.\n # BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1\n # BINANCE_ALPHA_SMOKE_OVERRIDE_REASON=\"\u003cshort justification>\"\n # The reason text is logged to stderr verbatim so override invocations\n # are visible in audit trails. CI scripts must set both; the legacy\n # single-flag form now emits a \"REFUSED override missing reason\" error\n # rather than silently allowing the bypass.\n #\n # Motivation: cross-LLM acceptance testing on v0.7.1 revealed that\n # agents (adversarial review, claude) default to invoking tests/smoke_fill.py as\n # a production fill step instead of authoring narrative directly,\n # producing reports full of placeholder stubs that look \"filled\" but\n # carry no analytical content. SKILL.md guidance alone proved\n # insufficient — agents read code paths over prose. So we gate at\n # the renderer level instead.\n smoke_override_flag = os.environ.get(\"BINANCE_ALPHA_ALLOW_SMOKE_RENDER\") == \"1\"\n smoke_override_reason = os.environ.get(\"BINANCE_ALPHA_SMOKE_OVERRIDE_REASON\", \"\").strip()\n smoke_override = smoke_override_flag and bool(smoke_override_reason)\n if smoke_override_flag and not smoke_override_reason:\n print(\n \"REFUSED: BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1 was set without \"\n \"BINANCE_ALPHA_SMOKE_OVERRIDE_REASON. Both env vars are \"\n \"required to bypass the smoke gate (audit-trail enforcement).\",\n file=sys.stderr,\n )\n sys.exit(3)\n if filled.get(\"_smoke_test_fixture\") is True:\n if not smoke_override:\n print(\n \"REFUSED: filled.json is a smoke test fixture \"\n \"(_smoke_test_fixture=true). Render will not produce a \"\n \"production report from test fixtures.\",\n file=sys.stderr,\n )\n print(\n \" → If this is a real forensic run, author narrative into \"\n \"the writable slots of skeleton.json directly (do NOT \"\n \"invoke tests/smoke_fill.py).\",\n file=sys.stderr,\n )\n print(\n \" → If this is an intentional CI/E2E test, set BOTH \"\n \"BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1 and \"\n \"BINANCE_ALPHA_SMOKE_OVERRIDE_REASON='\u003creason>' in env.\",\n file=sys.stderr,\n )\n sys.exit(3)\n print(\n f\"WARNING: rendering smoke test fixture under override \"\n f\"(reason: {smoke_override_reason}; output is NOT a real report).\",\n file=sys.stderr,\n )\n else:\n # Fingerprint check — counts NATO-suffix patterns across all\n # narrative strings in `filled`. Real narrative effectively never\n # produces these.\n nato_hits = _count_nato_smoke_fingerprints(filled)\n if nato_hits >= 3 and not smoke_override:\n print(\n f\"REFUSED: filled.json appears to be smoke test output \"\n f\"with the _smoke_test_fixture flag stripped \"\n f\"({nato_hits} NATO suffix fingerprints detected, e.g. \"\n f\"'(alpha variant)', '(bravo variant)').\",\n file=sys.stderr,\n )\n print(\n \" → Stripping the flag does not bypass the gate. Author \"\n \"real narrative or set both override env vars \"\n \"(BINANCE_ALPHA_ALLOW_SMOKE_RENDER=1 + \"\n \"BINANCE_ALPHA_SMOKE_OVERRIDE_REASON='\u003creason>').\",\n file=sys.stderr,\n )\n sys.exit(3)\n\n # v0.6.2 audit HIGH #2: pipeline lang vs render lang must match.\n # Mismatch = visible-fail (refuse render) instead of silent\n # \"English shell + Chinese narrative core\" pollution.\n skel_lang = (skel.get(\"meta\") or {}).get(\"report_lang_locked\")\n cur_lang = get_lang()\n if skel_lang and skel_lang != cur_lang:\n print(\n f\"REFUSED: skeleton --lang ({skel_lang}) != render --lang ({cur_lang}). \"\n f\"Re-run pipeline with --lang {cur_lang} OR render with --lang {skel_lang}.\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n # v0.7.20.1: route the chain_router to the chain the pipeline ran on.\n # render_report.py is a fresh process — without this, `_active_chain`\n # is the module default (\"bsc\") and every `explorer_url(...)` call\n # in the jinja template links to BSCScan even for a Base / Arbitrum\n # token. We read meta.chain_id (Alpha API value, locked at Section\n # A time) rather than meta.primary_chain because chain_id is the\n # canonical chain_router input and round-trips through the same\n # mapping the pipeline used.\n meta_for_chain = filled.get(\"meta\", {})\n chain_id_from_meta = meta_for_chain.get(\"chain_id\")\n if chain_id_from_meta:\n try:\n from chain_router import set_active_chain, UnsupportedChainError\n set_active_chain(chain_id_from_meta)\n except UnsupportedChainError as e:\n print(\n f\"WARN: render unable to route to chain_id={chain_id_from_meta!r} \"\n f\"({e}); explorer links will fall back to BSC default.\",\n file=sys.stderr,\n )\n\n # MANDATORY validator — invoked on the EXACT in-memory objects that will\n # be rendered. Earlier alpha.8 ran the validator as a subprocess against\n # file paths AFTER loading skel/filled, which let an attacker swap the\n # file between load + validate and ship malicious content with a passing\n # validator — cross-LLM audit on alpha.8 caught this TOCTOU race.\n # Calling Validator() directly on `skel` / `filled` binds validation to\n # the exact bytes we will render. No opt-out path.\n validator = Validator()\n errors = validator.validate(skel, filled)\n narrative_warnings: list[str] = []\n if errors:\n # v0.7.1 architectural fix (was hard-abort blowing surf credits with\n # no report.md): split errors into structural (LLM tampered locked\n # data — never render) vs narrative quality (recoverable — render\n # with warning, agent can retry the specific slot if it wants).\n from validate_report_data import categorize_errors\n structural, narrative_warnings = categorize_errors(errors)\n if structural:\n print(\n f\"V_SEMANTIC_VALIDATION STRUCTURAL FAIL — render aborted: \"\n f\"{len(structural)} blocking issue(s):\",\n file=sys.stderr,\n )\n for i, e in enumerate(structural, 1):\n print(f\" {i}. {e}\", file=sys.stderr)\n sys.exit(1)\n # Only narrative-quality errors: print to stderr but proceed to render.\n if narrative_warnings:\n print(\n f\"V_SEMANTIC_VALIDATION NARRATIVE_QUALITY: {len(narrative_warnings)} \"\n f\"recoverable issue(s) — rendering with warning footer:\",\n file=sys.stderr,\n )\n for i, e in enumerate(narrative_warnings, 1):\n print(f\" {i}. {e}\", file=sys.stderr)\n\n filled[\"machine_readable_json\"] = compute_machine_readable_json(filled)\n filled[\"evidence_count\"] = len(filled.get(\"evidence_graph\") or {})\n # Pass warnings into template so renderer can add a footer if narrative\n # quality issues are present.\n filled[\"_narrative_warnings\"] = narrative_warnings\n\n # v0.7.19: trim_blocks=True strips the newline immediately following a\n # `{% %}` block tag, so a false `{% if %}{% endif %}` no longer leaves\n # a phantom blank line in the rendered markdown. lstrip_blocks=True\n # additionally strips leading whitespace on lines that contain only\n # a block tag. Together they fix the ~5 leading blank lines that\n # COLLECT / BEAT / GUA reports had at the top (5 banner blocks all\n # evaluating false stacks 5 blank lines under the H1 title).\n env = Environment(\n trim_blocks=True,\n lstrip_blocks=True,\n keep_trailing_newline=True,\n undefined=StrictUndefined,\n finalize=html_escape_finalize,\n )\n env.filters[\"md_cell\"] = escape_md_cell\n env.filters[\"mermaid_label\"] = escape_mermaid_label\n env.filters[\"mermaid_id\"] = escape_mermaid_id\n env.filters[\"role_to_cn\"] = role_to_cn\n env.filters[\"pad_display\"] = pad_display\n env.filters[\"truncate_display\"] = truncate_display\n # v0.8.4.9.1: regex_replace filter for in-template substitution of\n # stale LLM-emitted numbers (e.g. m4_notes \"49 个内幕钱包\" → \"51\").\n import re as _re\n env.filters[\"regex_replace\"] = lambda s, pattern, repl: _re.sub(pattern, repl, s) if isinstance(s, str) else s\n # v0.6.2: register t() as Jinja global so template can use {{ t(\"...\") }}\n # for lang-aware labels. set_lang() must be called earlier in main().\n #\n # v1.0.1 i18n-completion fix: the static curated i18n strings legitimately\n # contain raw markdown (blockquote `>`, `**bold**`, `[links]`, `\u003c / >`\n # comparators) that MUST render verbatim — exactly as the pre-i18n inline\n # template literals did. Under `finalize=html_escape_finalize`, a bare\n # `{{ t(\"...\") }}` would HTML-escape those (`>`→`>`), silently degrading\n # the report vs the pre-i18n output. Wrapping the result in Markup makes\n # the trusted i18n string render raw (finalize passes Markup through, see\n # html_escape_finalize). We still escape any STRING kwargs first, because\n # those are data (token symbol/name, chain ids, addresses) and remain\n # user-controlled — preserving the original behavior where data rendered\n # via `{{ var }}` was escaped. Only ~2 of 540 template t() calls had the\n # explicit `| safe`; this makes all of them faithful without per-call edits.\n from markupsafe import Markup as _Markup\n\n def _t_template(_key, **_kwargs):\n _safe = {\n _k: (html_escape_finalize(_v) if isinstance(_v, str) else _v)\n for _k, _v in _kwargs.items()\n }\n return _Markup(t(_key, **_safe))\n\n env.globals[\"t\"] = _t_template\n # v0.7.20.1: explorer URL helper. Templates call `explorer_url(addr)`\n # instead of hardcoding `https://bscscan.com/address/...`. The router\n # is set by forensic_pipeline → section_a chain_id, so a Base PLAY\n # report links to basescan.org, an Arbitrum token to arbiscan.io, etc.\n from chain_router import explorer_url, explorer_name\n env.globals[\"explorer_url\"] = explorer_url\n env.globals[\"explorer_name\"] = explorer_name\n # v0.8.0: TLDR mode uses independent compact template. DEEP mode\n # uses the same default TEMPLATE but with `mode == 'deep'` available\n # in the Jinja context so `\u003cdetails>` blocks can pass `open` attr.\n # Default mode is unchanged from v0.7.28 behavior.\n _context = dict(filled)\n _context[\"mode\"] = mode\n if mode == \"tldr\":\n template = env.from_string(_TLDR_TEMPLATE)\n # TLDR template references `_v.subtype/label/risk` for parity\n # with the full template's namespace shape. Pre-compute in\n # Python since TLDR template doesn't carry the jinja-side\n # namespace derivation block from the full TEMPLATE.\n _chain_state = _derive_chain_state_neutral_5tier(filled)\n _context[\"_v\"] = {\n \"subtype\": _chain_state[\"subtype\"],\n \"label\": _chain_state[\"label\"],\n \"risk\": _chain_state[\"risk\"],\n }\n else:\n template = env.from_string(TEMPLATE)\n rendered = template.render(**_context)\n\n # v0.6.4: prepend UTF-8 BOM (U+FEFF) so downstream viewers / wrappers\n # that auto-detect encoding don't misread UTF-8 as cp1252 / Latin-1.\n # Empirical: 3/3 cross-LLM testers produced\n # mojibake in their wrapper-rendered output when no BOM was present.\n # BOM is a no-op for any UTF-8-aware viewer (GitHub, VSCode, most\n # markdown renderers strip it silently) and a strong signal to\n # legacy / heuristic viewers to decode as UTF-8.\n bom = \"\"\n out_p.write_bytes((bom + rendered).encode(\"utf-8\"))\n print(f\"OK: rendered {out_path} ({len(rendered)} bytes, UTF-8 + BOM)\")\n\n # v0.6.0-beta.3: re-emit monitoring_wallets export with LLM-filled\n # alert text. Pipeline-time emit had placeholder masked to empty;\n # this pass writes the final usable files.\n meta = filled.get(\"meta\", {})\n mw_export = monitoring_export.write_all(\n symbol=meta.get(\"symbol\", \"UNKNOWN\"),\n chain=meta.get(\"chain\", \"BSC\"),\n contract_address=meta.get(\"contract_address\", \"\"),\n monitoring_wallets=filled.get(\"monitoring_wallets\", []),\n out_dir=out_p.parent,\n lang=get_lang(), # v0.6.2 audit MED #3: pass through current lang\n )\n print(\n f\"OK: monitoring re-emitted ({mw_export['n_wallets']} wallets) → \"\n f\"{mw_export['dir']}\"\n )\n\n # v0.7.1: caller signals via exit code whether render was clean (0) or\n # had narrative-quality warnings (2). Structural failures (exit 1)\n # already returned earlier via sys.exit(1).\n if narrative_warnings:\n return 2\n return 0\n\n\ndef main() -> int:\n # Beta.15: force UTF-8 stdout/stderr so prints with '→' / emoji /\n # 中文 work on Windows console (default cp1252 can't encode U+2192\n # and most non-Latin codepoints). Symptom: render succeeds (writes\n # report.md fine) then exit 1 inside the post-render `print(...)`\n # at line 635 because `→` doesn't round-trip through cp1252.\n # `errors=\"replace\"` falls back to `?` rather than crashing on\n # any remaining edge case.\n for stream in (sys.stdout, sys.stderr):\n if hasattr(stream, \"reconfigure\"):\n try:\n stream.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"--skeleton\", required=True, help=\"Pipeline output (pre-LLM-fill)\")\n ap.add_argument(\"--filled\", required=True, help=\"LLM-filled JSON\")\n ap.add_argument(\"--out\", required=True, help=\"Output report.md path\")\n ap.add_argument(\"--lang\", default=\"zh\", choices=(\"zh\", \"en\"),\n help=\"Report language. Default: zh. Must match pipeline --lang.\")\n # v0.8.0: 3-tier report mode (ChatGPT v0.8 spec section 7).\n # - tldr ~4-6KB minimal战情摘要 — Discord/移动端/preview 用 (5 段)\n # - default ~18-28KB full forensic简报 (current template, all 15+ sections)\n # - deep same as default but `\u003cdetails>` 块默认展开, no collapse —\n # 链上侦测 全展开 + AI 深挖友好\n ap.add_argument(\"--mode\", default=\"default\", choices=(\"tldr\", \"default\", \"deep\"),\n help=\"Report mode. tldr=minimal 4-6KB, default=full 18-28KB, \"\n \"deep=full + all \u003cdetails> expanded. Default: default.\")\n args = ap.parse_args()\n # v0.6.2: set lang BEFORE render() so all t() lookups use it.\n set_lang(args.lang)\n rc = render(args.skeleton, args.filled, args.out, mode=args.mode)\n return rc if rc is not None else 0\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":203831,"content_sha256":"0eb61056563305948b5442c6332e23f885a723d6f600c82e0f16eb54dbdc8805"},{"filename":"alpha/v06/validate_report_data.py","content":"#!/usr/bin/env python3\n\"\"\"validate_report_data.py — v0.6 content-level validator.\n\nVerifies a LLM-filled report_data.json against:\n1. Skeleton invariance: locked fields MUST equal pipeline output (no LLM tampering)\n2. R1-R12 content cross-field consistency rules\n3. Provenance: every narrative claim referencing evt_ref must be consistent\n with the underlying evidence_graph entry\n4. Causal Rule 11: m6.rows ↔ anomaly.waves coupling\n5. Semantic META check: writable fields must not match the blacklist\n embedding corpus (optional, falls back to substring match if embeddings\n not built)\n6. Field authority enforcement: derived from field_authority.yaml registry\n\n## Error categories (v0.7.1)\n\nv0.7.1 splits errors into two classes so render_report.py can decide:\n\n STRUCTURAL — LLM tampered locked data / pipeline regression / security\n gate. NEVER produce a report (caller paid surf credits but\n output cannot be trusted).\n\n NARRATIVE_QUALITY — narrative content didn't meet quality bar (too short,\n duplicated, doesn't cite locked evidence, etc.). Render the\n report anyway with warning at top; let caller decide to\n re-fill narrative and re-render.\n\nThis stops the v0.7.0 architecture bug where a fresh LLM that didn't cite\nenough evidence wasted user's Surf credits with no report.md emitted.\n\n## Usage\n\n```bash\npython3 validate_report_data.py --skeleton skeleton.json --filled filled.json\n# exits 0 if all pass\n# exits 1 if any STRUCTURAL fail (hard abort)\n# exits 2 if only NARRATIVE_QUALITY fails (caller may render anyway)\n```\n\nThe skeleton is the pipeline output BEFORE LLM-fill (contains\n\u003cLLM_NARRATIVE_PLACEHOLDER>). The filled JSON is what the LLM produced. We\ndiff locked fields between the two AND apply content checks to writable fields.\n\nv0.6 (2026-05-24, cross-LLM audit conditions)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport re\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).parent / \"helpers\"))\nfrom evidence_graph import EvidenceGraph\n\n\n# ============================================================\n# Helpers — path resolution\n# ============================================================\n\ndef _resolve_path(obj: Any, path: str) -> list[tuple[str, Any]]:\n \"\"\"Resolve a dot-path with [] array notation. Returns list of (concrete_path, value)\n tuples. `meta.symbol` returns 1 tuple; `anomaly.waves[].events[].nature` returns\n one tuple per actual array index.\n\n Returns empty list if path doesn't resolve (field absent — caller decides\n if that's an error).\n \"\"\"\n parts = re.split(r\"\\.\", path)\n results: list[tuple[str, Any]] = [(\"\", obj)]\n for part in parts:\n new_results: list[tuple[str, Any]] = []\n # Handle [] suffix\n if part.endswith(\"[]\"):\n key = part[:-2]\n for cp, val in results:\n if isinstance(val, dict) and key in val:\n arr = val[key]\n if isinstance(arr, list):\n for i, item in enumerate(arr):\n new_results.append((f\"{cp}.{key}[{i}]\" if cp else f\"{key}[{i}]\", item))\n # If key missing, silently produce nothing — caller checks length\n else:\n for cp, val in results:\n if isinstance(val, dict) and part in val:\n new_results.append((f\"{cp}.{part}\" if cp else part, val[part]))\n results = new_results\n return results\n\n\ndef _load_yaml_simple(path: Path) -> dict:\n \"\"\"Minimal YAML loader — no external dep. Supports our specific format:\n - top-level keys (locked / derived_locked / writable / deferred)\n - lists under those keys (strings OR dicts)\n - dict values flat (path, min_length, etc.)\n For full YAML semantics we'd use PyYAML, but this avoids a runtime dep\n and is enough for our restricted schema.\n \"\"\"\n text = path.read_text(encoding=\"utf-8\")\n out: dict = {}\n current_key = None\n current_list: list = []\n current_dict: dict | None = None\n for raw_line in text.split(\"\\n\"):\n # Strip comments + trailing whitespace; keep leading whitespace for indent\n if \"#\" in raw_line:\n raw_line = raw_line.split(\"#\", 1)[0]\n line = raw_line.rstrip()\n if not line.strip():\n continue\n # Top-level key (no leading whitespace, ends with ':')\n if not line.startswith((\" \", \"\\t\")) and \":\" in line:\n if current_key:\n if current_dict:\n current_list.append(current_dict)\n current_dict = None\n out[current_key] = current_list\n k, v = line.split(\":\", 1)\n v = v.strip()\n if v and v not in ('\"', \"'\"):\n # scalar top-level value (e.g. version: \"0.6.0-alpha.1\")\n out[k] = v.strip('\"').strip(\"'\")\n current_key = None\n current_list = []\n else:\n current_key = k\n current_list = []\n current_dict = None\n continue\n # List item under current key\n if line.lstrip().startswith(\"- \"):\n # Flush prior dict if any\n if current_dict:\n current_list.append(current_dict)\n current_dict = None\n content = line.lstrip()[2:].strip()\n # v0.6.0-alpha.13 (cross-LLM audit MEDIUM): explicitly reject\n # nested-list / non-scalar list-item shapes. Previously `- - foo`\n # parsed as the string \"- foo\" because we only stripped one \"- \"\n # prefix, which let malformed YAML pass the constructor's\n # isinstance(str) check. Fail closed at load time.\n if content.startswith(\"-\") or content.startswith(\"[\"):\n raise ValueError(\n f\"field_authority.yaml line {ln+1 if 'ln' in dir() else '?'}: \"\n f\"malformed list item {line.rstrip()!r}. Nested lists / \"\n f\"flow-style arrays are not supported by this loader.\"\n )\n if \":\" in content and not content.startswith('\"'):\n # Start of a dict item: `- path: foo`\n k, v = content.split(\":\", 1)\n current_dict = {k.strip(): v.strip().strip('\"')}\n else:\n # Plain string item: `- \"meta.symbol\"` or `- meta.symbol`\n current_list.append(content.strip('\"').strip(\"'\"))\n continue\n # Continuation of dict item (indented ` key: value`)\n if current_dict is not None and \":\" in line:\n stripped = line.strip()\n k, v = stripped.split(\":\", 1)\n current_dict[k.strip()] = v.strip().strip('\"')\n continue\n # Flush final\n if current_dict:\n current_list.append(current_dict)\n if current_key:\n out[current_key] = current_list\n return out\n\n\n# ============================================================\n# Validator class\n# ============================================================\n\nclass Validator:\n \"\"\"v0.6 validator. Reads field_authority.yaml + meta_blacklist_corpus.txt.\n\n Calling convention:\n v = Validator(yaml_path=..., corpus_path=...)\n errors = v.validate(skeleton_dict, filled_dict)\n \"\"\"\n\n def __init__(self, use_embeddings: bool = False):\n \"\"\"v0.6.0-alpha.3: paths to field_authority.yaml + meta_blacklist_corpus.txt\n are HARDCODED to the schema/ directory adjacent to this validator.\n\n Previously these were constructor args, but cross-LLM tester v0.6.0-alpha.2\n audit demonstrated a bypass: moving the action_enum locked line out\n of a custom yaml made the same filled JSON pass. Hardcoding closes\n that surface — same class of fix as v0.5.x --bypass-validation removal.\n \"\"\"\n self.errors: list[str] = []\n\n schema_dir = Path(__file__).parent / \"schema\"\n yaml_path = schema_dir / \"field_authority.yaml\"\n corpus_path = schema_dir / \"meta_blacklist_corpus.txt\"\n\n self.authority = _load_yaml_simple(yaml_path)\n\n # v0.6.0-alpha.11 (cross-LLM audit HIGH): reject unknown top-level\n # keys in field_authority.yaml. Previously, only locked / derived_locked\n # / writable were consumed; any other key (incl. typos like\n # `derived_lock`, or a future tier nobody plumbed in) was silently\n # ignored, downgrading enforcement to \"what we happened to recognize\".\n # Fail closed at construction time so config drift is a startup error,\n # not a runtime silent pass.\n _ALLOWED_TIERS = {\n \"locked\", \"derived_locked\", \"writable\",\n # Documentation-only keys (not iterated as tiers):\n \"version\", \"updated\", \"deferred\",\n }\n unknown_keys = set(self.authority.keys()) - _ALLOWED_TIERS\n if unknown_keys:\n raise ValueError(\n f\"field_authority.yaml has unknown top-level keys: \"\n f\"{sorted(unknown_keys)}. Allowed: {sorted(_ALLOWED_TIERS)}. \"\n f\"Refusing to load — silent typos here downgrade enforcement.\"\n )\n\n # v0.6.0-alpha.12 (cross-LLM audit HIGH): strict grammar check\n # on locked + derived_locked entries. Previously _check_locked_invariance\n # had `if not isinstance(path, str): continue` which silently skipped\n # any malformed entry (e.g., a dict `{'path': 'meta.symbol'}` produced\n # by a YAML refactor). Now reject at construction.\n for tier in (\"locked\", \"derived_locked\"):\n entries = self.authority.get(tier, [])\n if not isinstance(entries, list):\n raise ValueError(\n f\"field_authority.yaml: {tier!r} must be a list of \"\n f\"non-empty strings, got {type(entries).__name__}.\"\n )\n for i, entry in enumerate(entries):\n if not isinstance(entry, str) or not entry.strip():\n raise ValueError(\n f\"field_authority.yaml: {tier}[{i}] is not a \"\n f\"non-empty string (got {entry!r}). Locked-tier \"\n f\"entries must be plain dot-path strings; structured \"\n f\"entries with metadata belong in `writable`.\"\n )\n\n # Load META corpus (substring fallback for now; embedding mode is\n # phase 4.2 polish)\n self.meta_corpus: list[str] = []\n if corpus_path.exists():\n for line in corpus_path.read_text(encoding=\"utf-8\").split(\"\\n\"):\n line = line.strip()\n if line and not line.startswith(\"#\"):\n self.meta_corpus.append(line)\n\n self.use_embeddings = use_embeddings\n self._embeddings_cache = None # lazy load only if used\n\n # Build the evidence_graph helper for narrative checks\n self._eg: EvidenceGraph | None = None # set during validate()\n\n # ----- Core validation entry -----\n\n def validate(self, skeleton: dict, filled: dict) -> list[str]:\n \"\"\"Run all checks. Returns list of error strings (empty = pass).\"\"\"\n self.errors = []\n\n # Reconstruct EvidenceGraph from filled[\"evidence_graph\"] for lookups\n self._eg = self._reconstruct_eg(filled.get(\"evidence_graph\", {}))\n\n # 1. Schema-version sanity (cheap)\n self._check_schema_version(skeleton, filled)\n\n # 2. Locked field invariance (the core anti-LLM-tampering check)\n self._check_locked_invariance(skeleton, filled)\n\n # 3. Writable field content quality (length, no remaining placeholders)\n self._check_writable_quality(filled)\n\n # 4. Provenance: every narrative referencing evt_ref must be consistent\n self._check_provenance(filled)\n\n # 5. Causal Rule 11: m6.rows ↔ waves coupling\n self._check_causal_rule_11(filled)\n\n # 6. R1-R12 cross-field rules\n self._check_R1_detector_counts(filled)\n self._check_R2_m6_implies_detector(filled)\n self._check_R4_rule11_implies_waves(filled)\n self._check_R7_rhythm_matches_waves(filled)\n self._check_R10_detector_categories(filled)\n self._check_R11_decision_action_complete(filled)\n self._check_R12_no_placeholders_remain(filled)\n\n # 7. Semantic META check on writable narrative fields\n self._check_meta_language(filled)\n\n # 8. v0.6.1: Anti-duplication checks. Prevents lazy LLM filling N items\n # with same narrative string (cross-LLM regression test exposed this — 12 m6 rows\n # all got identical \"项目方派发的内幕派发方\" filler).\n self._check_no_narrative_duplication(filled)\n\n # 9. v0.6.4: Narrative-vs-locked semantic consistency. Catches LLM\n # writing narrative that directly contradicts what the pipeline-locked\n # data says — e.g. cex_trace.interpretation saying \"no perp catalyst\"\n # when locked tier == \"S2\" + Binance perp listing row is present.\n # Two independent LLMs (an LLM + adversarial review) made this mistake on STAR\n # 2026-05-25 cross-LLM test; user caught it on read.\n self._check_narrative_vs_locked_semantic(filled)\n\n # 10. v0.7: cross_sym whale narrative validation. Pipeline-derived\n # identity classification is locked; LLM cannot freelance the\n # identity (e.g. claim KOL when classifier says ARB_DESK), and\n # identity narrative must cite ≥2 locked signature evidence fields.\n self._check_cross_sym_whales(skeleton, filled)\n\n # 10b. v0.7.7: wash_infrastructure section. Pipeline emits the X/P/Q\n # triplet + 5-step structural metrics as locked. LLM may only write\n # the investigation_narrative + summary_narrative.\n self._check_wash_infrastructure(skeleton, filled)\n\n # 11-13. v0.7.3: narrative quality validators — catch dud narrative\n # not caught above. All three are NARRATIVE_QUALITY (soft-fail) so\n # render still produces a report with warning header.\n self._check_narrative_generic_phrases(filled)\n self._check_narrative_template_reuse(filled)\n self._check_narrative_numeric_hallucination(filled)\n\n return list(self.errors)\n\n def _reconstruct_eg(self, eg_dict: dict) -> EvidenceGraph:\n \"\"\"Rebuild an EvidenceGraph from a serialized dict (read-only use).\"\"\"\n eg = EvidenceGraph()\n eg._store = dict(eg_dict) # raw dict, no add_event() because we want exact IDs preserved\n return eg\n\n # ----- Individual checks -----\n\n def _check_schema_version(self, skeleton: dict, filled: dict) -> None:\n s_ver = skeleton.get(\"_schema_version\")\n f_ver = filled.get(\"_schema_version\")\n if s_ver != f_ver:\n self.errors.append(\n f\"V_SCHEMA_VERSION: skeleton={s_ver!r} != filled={f_ver!r}\"\n )\n # v0.8.4.9.4: accept 0.6.x AND 0.7.x AND 0.8.x. v0.9.0 加: 跟\n # 0.8 schema 兼容 (没 breaking change), 只是 architecture rearch\n # (folder 重组 + 单 skill router). schema 字段未动.\n # v1.2.0: accept ALL 1.x via a \"1.\" prefix — the per-minor enumeration meant\n # every _version.py bump (1.0→1.1→1.2…) silently broke the FIRST render with\n # a V_SCHEMA_VERSION hard-fail until someone re-listed it. The s_ver != f_ver\n # check above still catches a genuine skeleton/filled mismatch; within the 1.x\n # line the schema is forward-compatible, so a prefix accept is correct + stops\n # this recurring on every release.\n if s_ver and not (s_ver.startswith(\"0.6\") or s_ver.startswith(\"0.7\")\n or s_ver.startswith(\"0.8\") or s_ver.startswith(\"0.9\")\n or s_ver.startswith(\"1.\")):\n self.errors.append(\n f\"V_SCHEMA_VERSION: expected 0.6.x / 0.7.x / 0.8.x / 0.9.x / 1.x, \"\n f\"got {s_ver!r}\"\n )\n\n def _check_array_segments_present(self, skeleton: dict, path: str) -> list[str]:\n \"\"\"For an array-path like `foo[].bar[].baz`, verify every `[]`\n segment's parent is a list in skeleton. v0.6.0-alpha.13 fix for\n cross-LLM audit HIGH: previously only the FIRST `[]` parent\n was validated, leaving nested-array configs bypassable.\n\n Returns list of error strings. Empty lists at any level are\n legitimate (no further descent possible, but no missing-segment\n finding either — that's the count check's job).\n \"\"\"\n if \"[]\" not in path:\n return []\n parts = path.split(\"[]\")\n errors: list[str] = []\n current_targets: list = [skeleton]\n # Walk every `[]` segment except the post-final scalar tail (parts[-1]).\n for i in range(len(parts) - 1):\n segment = parts[i].lstrip(\".\")\n next_targets: list = []\n for t in current_targets:\n node = t\n if segment:\n for k in segment.split(\".\"):\n if isinstance(node, dict) and k in node:\n node = node[k]\n else:\n errors.append(\n f\"V_LOCKED_FIELD_ABSENT: path={path!r} \"\n f\"missing key {k!r} at array segment \"\n f\"index {i} ({segment!r}).\"\n )\n node = None\n break\n if node is None:\n continue\n if not isinstance(node, list):\n errors.append(\n f\"V_LOCKED_FIELD_ABSENT: path={path!r} expected \"\n f\"list at segment index {i} ({segment!r}), got \"\n f\"{type(node).__name__}. Likely `[]` typo on a \"\n f\"scalar field.\"\n )\n continue\n next_targets.extend(node)\n current_targets = next_targets\n if not current_targets:\n # All matched arrays are empty — legitimate (empty\n # monitoring_wallets[] on tokens with zero detected actors).\n # No further descent possible; stop without error.\n break\n return errors\n\n def _check_locked_invariance(self, skeleton: dict, filled: dict) -> None:\n \"\"\"For every locked OR derived_locked path, skeleton value == filled value.\n\n v0.6.0-alpha.10 (cross-LLM re-audit): previously this only\n iterated `locked`, leaving `derived_locked` fields (verdict.enum,\n verdict.cn_label, verdict.next_tier_*, decision_action_block.\n immediate_action.action_enum) unenforced. An LLM could rewrite\n verdict.enum from EXIT_IF_HOLDING → ENTER with the validator\n passing. Same class of trust-boundary gap as v0.5 --bypass-validation\n and v0.6 alpha.2 --yaml flag; closes that surface.\n\n The two tier names remain distinct in the registry (locked = derived\n from raw data; derived_locked = derived from other locked fields)\n but ENFORCEMENT is identical: LLM cannot touch either tier.\n \"\"\"\n locked_paths = list(self.authority.get(\"locked\", []))\n derived_locked_paths = list(self.authority.get(\"derived_locked\", []))\n all_paths = locked_paths + derived_locked_paths\n\n # v0.6.0-alpha.11 (cross-LLM audit HIGH): paths absent in BOTH\n # skeleton and filled previously passed silently (len(s_vals) == 0 ==\n # len(f_vals)). That weakens the trust boundary from \"must be pipeline\n # controlled\" to \"only checked if present\" — pipeline can regress and\n # silently stop emitting a locked field with no failure signal.\n #\n # Two classes of locked paths exist:\n # 1. Scalar leaves (e.g., meta.symbol) — MUST be present in skeleton.\n # 2. Array element paths (foo[].bar) — empty array is legitimate\n # pre-population state (e.g., monitoring_wallets[] for tokens with\n # zero detected actors). For these we only flag if filled adds\n # elements that skeleton doesn't have (covered by count-mismatch).\n #\n # The simple heuristic: array paths contain `[]`, scalar paths don't.\n # v0.6.0-alpha.12 (cross-LLM audit HIGH): the `[]` heuristic\n # was bypassable — a scalar path declared with a typo `meta.symbol[]`\n # would be classified as array, skip V_LOCKED_FIELD_ABSENT, and\n # _resolve_path returning [] in both skeleton + filled would pass\n # count check. Closed by requiring array paths to have a structurally\n # existing PARENT in skeleton (the dict/list at the parent key must\n # exist, even if the array is empty). Empty array is legitimate\n # pipeline state; absent parent dict is a config / pipeline bug.\n for path in all_paths:\n if not isinstance(path, str):\n # Defensive: constructor already rejects non-string entries,\n # but if a future refactor adds runtime mutation paths this\n # remains a fail-closed gate, NOT a silent skip.\n self.errors.append(\n f\"V_AUTHORITY_GRAMMAR: non-string locked entry \"\n f\"{type(path).__name__}({path!r}) — config bug\"\n )\n continue\n s_vals = _resolve_path(skeleton, path)\n f_vals = _resolve_path(filled, path)\n\n is_array_path = \"[]\" in path\n\n # Scalar locked path absent in both skeleton AND filled:\n # fail closed. Locked means \"pipeline must emit\"; absence is a\n # regression, not a no-op.\n if not is_array_path and not s_vals and not f_vals:\n self.errors.append(\n f\"V_LOCKED_FIELD_ABSENT: path={path!r} (scalar, \"\n f\"declared in field_authority {('derived_' if path in derived_locked_paths else '')}locked) \"\n f\"missing from both skeleton and filled — pipeline regression.\"\n )\n continue\n\n # Array locked path with zero matches in BOTH: verify EVERY `[]`\n # segment's parent list exists. v0.6.0-alpha.12 only checked the\n # FIRST `[]` parent — cross-LLM audit HIGH caught that for\n # nested paths like `foo[].bar[].baz`, if `foo` exists but each\n # `foo[i].bar` is missing, both s_vals and f_vals stay empty and\n # no V_LOCKED_FIELD_ABSENT fires. Now we walk every `[]` segment.\n if is_array_path and not s_vals and not f_vals:\n nested_errors = self._check_array_segments_present(skeleton, path)\n if nested_errors:\n self.errors.extend(nested_errors)\n # If all segments resolved to lists but the resulting array(s)\n # are empty, that's legitimate (empty monitoring_wallets[] on\n # a token with zero detected actors). Fall through; count\n # check below stays 0 == 0 as legitimate.\n\n if len(s_vals) != len(f_vals):\n self.errors.append(\n f\"V_LOCKED_FIELD_COUNT: path={path!r} skeleton has \"\n f\"{len(s_vals)} matches, filled has {len(f_vals)}\"\n )\n continue\n for (sp, sv), (fp, fv) in zip(s_vals, f_vals):\n if sv != fv:\n self.errors.append(\n f\"V_LOCKED_FIELD_MODIFIED: path={fp} was locked. \"\n f\"skeleton={_summary(sv)!r} but filled={_summary(fv)!r}\"\n )\n\n def _check_writable_quality(self, filled: dict) -> None:\n \"\"\"Each writable spec has min_length / min_count / etc. Verify.\"\"\"\n writable_specs = self.authority.get(\"writable\", [])\n for spec in writable_specs:\n if not isinstance(spec, dict):\n continue\n path = spec.get(\"path\")\n if not path:\n continue\n try:\n min_length = int(spec.get(\"min_length\", 0))\n except (ValueError, TypeError):\n min_length = 0\n try:\n min_count = int(spec.get(\"min_count\", 0))\n except (ValueError, TypeError):\n min_count = 0\n\n matches = _resolve_path(filled, path)\n\n if min_count and len(matches) \u003c min_count:\n self.errors.append(\n f\"V_WRITABLE_COUNT: path={path!r} requires min_count={min_count} \"\n f\"but only {len(matches)} present\"\n )\n\n for cp, val in matches:\n if val is None:\n self.errors.append(f\"V_WRITABLE_NULL: {cp} is null\")\n continue\n if isinstance(val, str):\n if \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" in val:\n self.errors.append(\n f\"V_WRITABLE_PLACEHOLDER: {cp} still contains \"\n \"\u003cLLM_NARRATIVE_PLACEHOLDER> — LLM did not fill\"\n )\n continue\n if \"\u003cPIPELINE_PHASE\" in val:\n self.errors.append(\n f\"V_WRITABLE_STUB: {cp} contains pipeline stub marker\"\n )\n continue\n if min_length and len(val) \u003c min_length:\n self.errors.append(\n f\"V_WRITABLE_TOO_SHORT: {cp} length={len(val)} \u003c \"\n f\"min_length={min_length}\"\n )\n\n def _check_provenance(self, filled: dict) -> None:\n \"\"\"For every anomaly.waves[].events[].nature, look up evt_ref and call\n EvidenceGraph.event_matches_narrative().\"\"\"\n if self._eg is None:\n return\n events = _resolve_path(filled, \"anomaly.waves[].events[]\")\n for cp, event in events:\n if not isinstance(event, dict):\n continue\n evt_ref = event.get(\"evt_ref\")\n nature = event.get(\"nature\", \"\")\n if not evt_ref:\n self.errors.append(f\"V_PROVENANCE_MISSING_REF: {cp} has no evt_ref\")\n continue\n if not nature or \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" in nature:\n # Already caught by writable check\n continue\n ok, why = self._eg.event_matches_narrative(evt_ref, nature)\n if not ok:\n self.errors.append(\n f\"V_PROVENANCE_MISMATCH: {cp}.nature inconsistent with \"\n f\"evidence_graph[{evt_ref!r}]: {why}\"\n )\n\n def _check_causal_rule_11(self, filled: dict) -> None:\n \"\"\"v0.6 condition 3: m6.rows must align with waves events.\n\n Specifically:\n - Every m6.row has m6_ref pointing into evidence_graph\n - Every wave event has evt_ref pointing into evidence_graph\n - If lineage.m6 has rows AND anomaly.waves is empty, REJECT (R4 strict)\n - Time ordering: waves[0] ts_range MUST start before or equal to waves[1] start\n \"\"\"\n m6_rows = _resolve_path(filled, \"lineage.m6.rows[]\")\n waves = _resolve_path(filled, \"anomaly.waves[]\")\n\n if m6_rows and not waves:\n self.errors.append(\n \"V_CAUSAL_RULE11: lineage.m6.rows has \"\n f\"{len(m6_rows)} entries but anomaly.waves is empty. Rule 11 \"\n \"found insiders; pipeline must emit waves_proposal.\"\n )\n\n # Verify each m6.row.m6_ref exists in evidence_graph\n if self._eg is None:\n return\n for cp, row in m6_rows:\n if not isinstance(row, dict):\n continue\n m6_ref = row.get(\"m6_ref\")\n if not m6_ref:\n self.errors.append(f\"V_CAUSAL_RULE11_NO_M6_REF: {cp} missing m6_ref\")\n continue\n if self._eg.lookup(m6_ref) is None:\n self.errors.append(\n f\"V_CAUSAL_RULE11_DANGLING: {cp}.m6_ref={m6_ref!r} not in evidence_graph\"\n )\n\n def _check_R1_detector_counts(self, filled: dict) -> None:\n \"\"\"Detector_summary[].count must be non-negative int.\"\"\"\n for cp, d in _resolve_path(filled, \"anomaly.detector_summary[]\"):\n if not isinstance(d, dict):\n continue\n cnt = d.get(\"count\")\n if cnt is None:\n self.errors.append(f\"V_R1_DETECTOR_NO_COUNT: {cp} has no count field\")\n continue\n try:\n if int(cnt) \u003c 0:\n self.errors.append(f\"V_R1_DETECTOR_NEG: {cp}.count={cnt} negative\")\n except (TypeError, ValueError):\n self.errors.append(f\"V_R1_DETECTOR_NOT_INT: {cp}.count={cnt!r} not int\")\n\n def _check_R2_m6_implies_detector(self, filled: dict) -> None:\n \"\"\"If m6.rows.length > 0, detector_summary MUST include a row about\n Rule 11 / 内幕地址 / pre-launch with matching count.\"\"\"\n m6_rows = _resolve_path(filled, \"lineage.m6.rows[]\")\n if not m6_rows:\n return\n n_m6 = len(m6_rows)\n # Check detector_summary has a label mentioning rule11 / 内幕 / pre-launch\n detectors = _resolve_path(filled, \"anomaly.detector_summary[]\")\n matching_keywords = (\"Rule 11\", \"rule 11\", \"Rule11\", \"内幕\", \"pre-launch\", \"Pre-launch\")\n for cp, d in detectors:\n if not isinstance(d, dict):\n continue\n label = d.get(\"label\", \"\")\n if any(kw in label for kw in matching_keywords):\n # Check count is plausible (>= number of full+partial dumpers, or >= n_quiet, etc.)\n # Loose check: count > 0\n try:\n if int(d.get(\"count\", 0)) >= 1:\n return # Found at least one detector covering Rule 11\n except (TypeError, ValueError):\n pass\n self.errors.append(\n f\"V_R2_M6_NO_DETECTOR: lineage.m6.rows has {n_m6} entries but no \"\n \"detector_summary entry mentions Rule 11 / 内幕 / pre-launch with count >= 1\"\n )\n\n def _check_R4_rule11_implies_waves(self, filled: dict) -> None:\n \"\"\"If any m6.rows.length > 0, anomaly.waves.length must be >= 1\n (Rule 11 backward trace produces wave 1 at minimum).\"\"\"\n m6_rows = _resolve_path(filled, \"lineage.m6.rows[]\")\n waves = _resolve_path(filled, \"anomaly.waves[]\")\n if m6_rows and not waves:\n # already caught by causal check; duplicate is OK\n return\n if not m6_rows and waves:\n # waves without m6 is fine — could be 72h anomaly only\n return\n\n def _check_R7_rhythm_matches_waves(self, filled: dict) -> None:\n \"\"\"anomaly.rhythm.waves count == anomaly.waves count.\"\"\"\n rhythm_waves = _resolve_path(filled, \"anomaly.rhythm.waves[]\")\n waves = _resolve_path(filled, \"anomaly.waves[]\")\n if len(rhythm_waves) != len(waves):\n self.errors.append(\n f\"V_R7_RHYTHM_MISMATCH: anomaly.waves has {len(waves)} but \"\n f\"anomaly.rhythm.waves has {len(rhythm_waves)}\"\n )\n\n def _check_R10_detector_categories(self, filled: dict) -> None:\n \"\"\"detector_summary should cover 7 mandatory categories. Soft check —\n at least 3 entries must exist (pipeline minimum).\"\"\"\n detectors = _resolve_path(filled, \"anomaly.detector_summary[]\")\n if len(detectors) \u003c 3:\n self.errors.append(\n f\"V_R10_DETECTOR_TOO_FEW: anomaly.detector_summary has only \"\n f\"{len(detectors)} entries; expect >= 3 mandatory categories\"\n )\n\n def _check_R11_decision_action_complete(self, filled: dict) -> None:\n \"\"\"decision_action_block.immediate_action.action_enum must be filled.\"\"\"\n action_paths = _resolve_path(filled, \"decision_action_block.immediate_action.action_enum\")\n for cp, v in action_paths:\n if not v or (isinstance(v, str) and \"\u003c\" in v):\n self.errors.append(\n f\"V_R11_DECISION_ACTION_NOT_DERIVED: {cp}={v!r} still a stub\"\n )\n\n def _check_R12_no_placeholders_remain(self, filled: dict) -> None:\n \"\"\"Recursively scan: no \u003cLLM_NARRATIVE_PLACEHOLDER> or \u003cPIPELINE_PHASE_*>\n markers can remain in the final filled JSON.\n\n v0.6.0-alpha.11 (cross-LLM audit MEDIUM): previously the docstring\n promised \u003cPIPELINE_PHASE_*> rejection but only \u003cLLM_NARRATIVE_PLACEHOLDER>\n was actually checked. A filled payload could ship with un-implemented\n pipeline stub markers and clear R12. Now BOTH classes are rejected.\n \"\"\"\n def walk(node, path):\n if isinstance(node, dict):\n for k, v in node.items():\n walk(v, f\"{path}.{k}\" if path else k)\n elif isinstance(node, list):\n for i, v in enumerate(node):\n walk(v, f\"{path}[{i}]\")\n elif isinstance(node, str):\n if \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" in node:\n self.errors.append(\n f\"V_R12_PLACEHOLDER: {path} still has \u003cLLM_NARRATIVE_PLACEHOLDER>\"\n )\n if \"\u003cPIPELINE_PHASE\" in node:\n self.errors.append(\n f\"V_R12_PLACEHOLDER: {path} still has pipeline stub marker \"\n f\"({node[:50]!r}) — section not yet wired\"\n )\n walk(filled, \"\")\n\n def _check_meta_language(self, filled: dict) -> None:\n \"\"\"For each writable spec with semantic_meta_check=true, check the\n filled value against the META blacklist corpus.\n\n Phase 4.1 implementation: substring + fuzzy ratio match. Phase 4.2\n upgrades to embedding cosine similarity.\n \"\"\"\n writable_specs = self.authority.get(\"writable\", [])\n for spec in writable_specs:\n if not isinstance(spec, dict):\n continue\n check_str = spec.get(\"semantic_meta_check\", \"\").lower()\n if check_str not in (\"true\", \"yes\", \"1\"):\n continue\n path = spec.get(\"path\")\n for cp, val in _resolve_path(filled, path):\n if not isinstance(val, str) or len(val) \u003c 5:\n continue\n hit = self._meta_corpus_check(val)\n if hit:\n self.errors.append(\n f\"V_META_LANGUAGE: {cp} matches blacklist phrase: \"\n f\"{hit[:80]!r}\"\n )\n\n def _meta_corpus_check(self, text: str) -> str | None:\n \"\"\"Return the first matching blacklist phrase, or None.\n\n Phase 4.1: substring + token-overlap match. Phase 4.2 will replace\n with embedding cosine sim > 0.75.\n \"\"\"\n text_norm = text.lower()\n for phrase in self.meta_corpus:\n p = phrase.lower()\n # Strong: substring of 15+ chars\n if len(p) >= 15 and p in text_norm:\n return phrase\n # Weaker: 75%+ token overlap (Chinese-aware: char-shingles)\n t_chars = set(text_norm)\n p_chars = set(p)\n if len(p_chars) >= 8:\n overlap = len(t_chars & p_chars) / len(p_chars)\n if overlap > 0.92 and len(p) > 20:\n # Plus: a 6-char shingle from phrase appears in text\n if any(p[i:i+6] in text_norm for i in range(0, len(p)-6, 3)):\n return phrase\n return None\n\n # ============================================================\n # v0.6.1: Anti-narrative-duplication checks\n # ============================================================\n # Triggered by cross-LLM regression test: lazy adversarial review filled 12 m6 rows\n # with identical \"项目方派发的内幕派发方\" string, 5 section interpretations\n # all = \"本节数据已落地, 详见证据图\", 3 key_takeaways all identical.\n # These pass V_WRITABLE_TOO_SHORT (length > 20) but produce useless report.\n #\n # 3 specific anti-dup rules — calibrated to allow some natural overlap\n # but reject obvious copy-paste filling. Threshold 70% for arrays > 5\n # items, all-identical for arrays = 3 items.\n\n def _check_no_narrative_duplication(self, filled: dict) -> None:\n \"\"\"Detect lazy LLM patterns where N narrative items are all same string.\"\"\"\n # Rule 1: lineage.m6.rows[].identity_narrative + status_narrative\n # 12 rows is typical; > 70% identical meanslaziness.\n m6_rows = filled.get(\"lineage\", {}).get(\"m6\", {}).get(\"rows\", [])\n if len(m6_rows) >= 5:\n self._check_array_field_uniqueness(\n items=[r.get(\"identity_narrative\") for r in m6_rows],\n field_path=\"lineage.m6.rows[].identity_narrative\",\n error_code=\"V_NARRATIVE_DUPLICATION\",\n threshold_pct=70,\n )\n self._check_array_field_uniqueness(\n items=[r.get(\"status_narrative\") for r in m6_rows],\n field_path=\"lineage.m6.rows[].status_narrative\",\n error_code=\"V_NARRATIVE_DUPLICATION\",\n threshold_pct=70,\n )\n\n # Rule 2: 5 section interpretations (multi_chain / tge / alloc /\n # cex_trace / liq). > 60% identical (3 of 5) meanslaziness.\n section_interps = []\n for sec in (\"multi_chain\", \"tge\", \"alloc\", \"cex_trace\", \"liq\"):\n v = filled.get(sec, {}).get(\"interpretation\")\n if isinstance(v, str):\n section_interps.append((sec, v))\n if len(section_interps) >= 4:\n self._check_array_field_uniqueness(\n items=[v for _, v in section_interps],\n field_path=\"*.interpretation (multi_chain/tge/alloc/cex_trace/liq)\",\n error_code=\"V_INTERPRETATION_DUPLICATION\",\n threshold_pct=60,\n )\n\n # Rule 3: holdings_distribution.key_takeaways[] — typically 3 items;\n # all 3 identical = obviouslaziness. Threshold = 100% (all same).\n takeaways = filled.get(\"holdings_distribution\", {}).get(\"key_takeaways\", [])\n if len(takeaways) >= 2:\n unique = {t for t in takeaways if isinstance(t, str) and len(t) > 5}\n if len(takeaways) >= 3 and len(unique) == 1:\n self.errors.append(\n f\"V_KEY_TAKEAWAYS_DUPLICATION: holdings_distribution.\"\n f\"key_takeaways has {len(takeaways)} items, all identical: \"\n f\"{takeaways[0][:60]!r}. LLM must write distinct insights.\"\n )\n\n # Rule 4: anomaly.detector_summary[].detail — typically 4 items;\n # all 4 identical = laziness.\n detectors = filled.get(\"anomaly\", {}).get(\"detector_summary\", [])\n if len(detectors) >= 3:\n self._check_array_field_uniqueness(\n items=[d.get(\"detail\") for d in detectors],\n field_path=\"anomaly.detector_summary[].detail\",\n error_code=\"V_DETECTOR_DUPLICATION\",\n threshold_pct=70,\n )\n\n # ============================================================\n # v0.6.4: Narrative-vs-locked semantic consistency check\n # ============================================================\n # For each narrative slot strongly coupled to a locked field, define\n # phrase patterns that MUST NOT appear when the locked field has\n # certain values. Catches the \"LLM dismisses a catalyst that the\n # pipeline clearly captured\" failure mode (STAR 2026-05-25:\n # cex_trace.tier locked to \"S2\" + Binance perp listing 10d ago, but\n # narrative said \"perp 未确认\" — both an LLM and adversarial review did this).\n #\n # Design notes:\n # - Phrase patterns are conservative substrings (long enough to avoid\n # incidental matches) anchored to specific contradictions.\n # - Only triggers when locked condition is met AND forbidden phrase\n # appears AND narrative is non-trivial length (> 30 chars).\n # - Each pair documents the specific locked → forbidden mapping.\n\n # locked condition → list of forbidden phrases (lowercase substring match)\n _NARRATIVE_VS_LOCKED_RULES = [\n {\n \"name\": \"cex_trace.tier S2/S3 must not deny CEX catalyst\",\n \"locked_path\": \"cex_trace.tier\",\n \"locked_predicate\": lambda v: isinstance(v, str) and v in (\"S2\", \"S3\"),\n \"narrative_paths\": [\n \"cex_trace.interpretation\",\n \"verdict.one_liner\",\n \"anomaly.verdict_impact\",\n ],\n \"forbidden_phrases\": [\n # Chinese\n \"perp 未上\",\n \"perp 未确认\",\n \"perp 未在 snapshot 中确认\",\n \"perp 等覆盖未在\",\n \"未确认是否上 perp\",\n \"未上 perp\",\n \"cex 催化未确认\",\n \"cex 催化不突出\",\n \"cex 催化线索不突出\",\n \"交易所催化线索目前不突出\",\n \"交易所催化线索不突出\",\n \"永续未上线\",\n \"永续未确认\",\n \"没有明确的近期 cex 催化\",\n \"没有明确的 cex 催化\",\n \"无 cex 催化\",\n # English\n \"no perp catalyst\",\n \"perp coverage unconfirmed\",\n \"no cex catalyst\",\n \"cex catalyst unclear\",\n ],\n },\n {\n \"name\": \"rule_11 empty must not claim active insider distribution\",\n \"locked_path\": \"lineage.m6.rows\",\n \"locked_predicate\": lambda v: isinstance(v, list) and len(v) == 0,\n \"narrative_paths\": [\n \"verdict.one_liner\",\n \"anomaly.verdict_impact\",\n \"lineage.m4_notes\", # list of strings\n ],\n \"forbidden_phrases\": [\n # v0.7.15: 派发 (sell-out) AND 分发 (transfer) versions both\n # forbidden — LLM must not claim either when the underlying\n # detector hasn't fired. Old \"派发\" entries kept so a regressed\n # LLM still gets caught even after the term split.\n \"active insider distribution\",\n \"内幕已确认派发\",\n \"内幕已确认分发\",\n \"内幕正在派发\",\n \"内幕正在分发\",\n \"项目方派发链路已确认\",\n \"项目方分发链路已确认\",\n \"派发链路清晰\",\n \"分发链路清晰\",\n \"active rule 11 distribution\",\n ],\n },\n {\n \"name\": \"deployer balance 0 must not claim deployer still holding\",\n \"locked_path\": \"holdings_distribution.role_rows\",\n \"locked_predicate\": lambda v: isinstance(v, list) and any(\n r.get(\"role\") == \"DEPLOYER\" and (r.get(\"total_balance\") or 0) \u003c 1\n for r in v\n ),\n \"narrative_paths\": [\n \"verdict.one_liner\",\n \"alloc.interpretation\",\n \"holdings_distribution.key_takeaways\", # list of strings\n ],\n \"forbidden_phrases\": [\n \"项目方仍持有大量筹码\",\n \"项目方仍持有大量\",\n \"项目方钱包持有大量\",\n \"deployer still holds significant\",\n \"deployer wallet still holding\",\n \"项目方未派发\", # legacy 派发 wording (kept for old-LLM regression)\n \"项目方未分发\", # v0.7.15 — correct 分发 wording\n ],\n },\n {\n \"name\": \"tier S1 must not claim CEX perp listing\",\n \"locked_path\": \"cex_trace.tier\",\n \"locked_predicate\": lambda v: v == \"S1\",\n \"narrative_paths\": [\n \"cex_trace.interpretation\",\n \"verdict.one_liner\",\n ],\n \"forbidden_phrases\": [\n \"已上 binance 永续\",\n \"binance 永续已上线\",\n \"perp 已上\",\n \"binance perp listed\",\n ],\n },\n ]\n\n # v0.6.4 P0-B: negation-context skip. If the forbidden phrase appears\n # AFTER a negation marker (within ~25 chars before it), it's likely\n # being refuted, not asserted. Skip the match.\n # Examples (should NOT trigger):\n # \"并非无 cex 催化, 实际上是有的\"\n # \"并不像 perp 未确认 那样, 已经上线了\"\n # \"没有 perp 未确认 的情况, 已经在 5/14 上\"\n # audit 2026-05-25 raised this FP risk.\n _NEGATION_MARKERS_RE = re.compile(\n r\"(并\\s*[非不未没]|并\\s*没有|并\\s*不是|\"\n r\"不是|没有|并\\s*未|实际上|事实上|与\\s*[此其]\\s*相反|\"\n r\"恰\\s*恰\\s*相反|相反\\s*的|相反\\s*地|\"\n r\"not\\s+(?:like|the\\s+case|the\\s+situation|true|that)|\"\n r\"contrary\\s+to|\"\n r\"opposite\\s+of|\"\n r\"refute[sd]?|\"\n r\"is\\s+not\\s+(?:the\\s+case|true))\"\n )\n\n def _phrase_is_negated(self, text_lower: str, phrase_lower: str) -> bool:\n \"\"\"Check if EVERY `phrase` occurrence in `text` is in a negation\n context. Returns True if all occurrences are refuted (skip-safe),\n False if at least one occurrence has no negation marker before it\n (real violation).\n\n Scans up to 10 chars BEFORE each phrase occurrence for a negation\n marker (immediate-context only — wider windows let earlier\n refutations leak into later phrases' lookback). audit\n 2026-05-25 raised the FP risk this addresses.\n \"\"\"\n start = 0\n saw_negated = False\n while True:\n idx = text_lower.find(phrase_lower, start)\n if idx == -1:\n # No more occurrences. If we skipped at least one as\n # negated AND never found a non-negated one, all\n # occurrences were refuted → safe to skip phrase.\n return saw_negated\n window_start = max(0, idx - 10)\n window = text_lower[window_start:idx]\n if self._NEGATION_MARKERS_RE.search(window):\n saw_negated = True\n start = idx + len(phrase_lower)\n continue\n return False # at least one occurrence without negation\n\n def _check_narrative_vs_locked_semantic(self, filled: dict) -> None:\n \"\"\"v0.6.4 — narrative MUST NOT contradict locked field semantics.\"\"\"\n for rule in self._NARRATIVE_VS_LOCKED_RULES:\n locked_val = self._resolve_dot_path(filled, rule[\"locked_path\"])\n if not rule[\"locked_predicate\"](locked_val):\n continue # locked condition not met → no constraint\n # Locked condition triggered: check each narrative slot\n for nar_path in rule[\"narrative_paths\"]:\n nar_val = self._resolve_dot_path(filled, nar_path)\n # Narrative can be a string OR a list of strings (e.g. m4_notes)\n texts = []\n if isinstance(nar_val, str):\n texts.append((nar_path, nar_val))\n elif isinstance(nar_val, list):\n for i, item in enumerate(nar_val):\n if isinstance(item, str):\n texts.append((f\"{nar_path}[{i}]\", item))\n for sub_path, text in texts:\n if len(text) \u003c 30:\n continue # too short to meaningfully contradict\n text_lower = text.lower()\n for phrase in rule[\"forbidden_phrases\"]:\n phrase_lower = phrase.lower()\n if phrase_lower not in text_lower:\n continue\n # P0-B: skip if this phrase occurrence is in a\n # negation context (refuted, not asserted).\n if self._phrase_is_negated(text_lower, phrase_lower):\n continue\n self.errors.append(\n f\"V_NARRATIVE_VS_LOCKED_SEMANTIC: {sub_path} \"\n f\"contains forbidden phrase {phrase!r} but \"\n f\"locked field {rule['locked_path']!r} = \"\n f\"{locked_val!r} ({rule['name']}). \"\n f\"Narrative: {text[:80]!r}\"\n )\n break # one error per (rule, path) is enough\n\n @staticmethod\n def _resolve_dot_path(obj: dict, path: str):\n \"\"\"Walk a dotted path through a nested dict. Returns None on miss.\"\"\"\n cur = obj\n for part in path.split(\".\"):\n if isinstance(cur, dict):\n cur = cur.get(part)\n else:\n return None\n if cur is None:\n return None\n return cur\n\n # ============================================================\n # v0.7: cross_sym whale validation\n # ============================================================\n # Pipeline locks every whale's identity_classification_enum (computed\n # by identity_classifier.classify deterministically from behavior\n # signature). LLM only writes 2 narrative slots:\n # - identity_narrative: WHY this classification (must cite ≥2 of the\n # locked evidence_required_fields by name)\n # - risk_assessment_narrative: trader-actionable read\n #\n # 4 validators (each one a hard gate):\n\n _IDENTITY_ENUM_WORDS = {\n # enum → list of phrases that imply the LLM is asserting this identity\n \"KOL_MANAGER\": [\n \"kol manager\", \"kol 经理\", \"kol 操盘\", \"kol 管钱\",\n \"市场制造商管理\", \"项目方代理人\",\n ],\n \"ACTIVE_MM\": [\n \"主动做市\", \"主动 mm\", \"主动mm\", \"做市商\", \"做市操作\",\n \"active market maker\", \"active mm\",\n ],\n \"ARB_DESK\": [\n \"套利\", \"套利桌\", \"arb desk\", \"arbitrage desk\", \"arb 桌\",\n \"套利商\", \"套利团队\",\n ],\n \"OTC_DESK\": [\n \"otc desk\", \"otc 桌\", \"otc 团队\", \"场外 desk\", \"场外交易桌\",\n ],\n \"UNKNOWN_WHALE_HIGH_CROSS_SYM\": [\n \"未知大户\", \"身份不明的大户\",\n ],\n \"INSUFFICIENT_SIGNAL\": [\n \"信号不足\", \"数据不足以分类\",\n ],\n }\n\n def _check_cross_sym_whales(self, skeleton: dict, filled: dict) -> None:\n \"\"\"v0.7 — cross_sym whales: lock + narrative + identity consistency.\"\"\"\n skel_whales = (skeleton.get(\"cross_sym\") or {}).get(\"whales\") or []\n fill_whales = (filled.get(\"cross_sym\") or {}).get(\"whales\") or []\n\n # V_CROSS_SYM_WHALES_INVARIANCE — count + address list must match\n skel_addrs = [(w.get(\"address\") or \"\").lower() for w in skel_whales]\n fill_addrs = [(w.get(\"address\") or \"\").lower() for w in fill_whales]\n if skel_addrs != fill_addrs:\n self.errors.append(\n f\"V_CROSS_SYM_WHALES_INVARIANCE: filled.cross_sym.whales addresses \"\n f\"({fill_addrs}) differ from skeleton's ({skel_addrs}). \"\n f\"LLM cannot add, remove, or reorder whale candidates.\"\n )\n return # Subsequent checks meaningless if list changed\n\n # Pair up by index for per-whale checks\n for i, (skel_w, fill_w) in enumerate(zip(skel_whales, fill_whales)):\n self._check_one_whale(i, skel_w, fill_w)\n\n # All pipeline-emitted fields per whale that must match between skeleton\n # and filled. Pipeline writes these; LLM can ONLY write the 2 narrative\n # slots (identity_narrative, risk_assessment_narrative). adversarial review final\n # audit 2026-05-25 flagged the original 2-field check as too narrow.\n _WHALE_LOCKED_KEYS = (\n # locked tier (pipeline-derived raw data)\n \"address\",\n \"this_token_pct\",\n \"this_token_balance\",\n \"cross_sym_count\",\n \"cross_sym_tokens\",\n \"top_cross_sym_token\",\n \"arkham_label\",\n \"pre_launch_insider_count\",\n \"pre_launch_insider_tokens\",\n \"behavior_signature\",\n # derived_locked tier (pipeline applies decision tree)\n \"identity_classification_enum\",\n \"confidence_score\",\n \"evidence_required_fields\",\n # v0.7.7 whale role classifier output (6-step on-chain analysis)\n \"role_classification\",\n )\n\n def _check_one_whale(self, idx: int, skel: dict, fill: dict) -> None:\n path_base = f\"cross_sym.whales[{idx}]\"\n\n # V_CROSS_SYM_CLASSIFICATION_LOCKED — every pipeline-emitted field is\n # locked. adversarial review final audit 2026-05-25 REJECT fix: previously only\n # checked identity_classification_enum + confidence_score, leaving\n # ~11 other fields LLM-tamperable.\n for locked_key in self._WHALE_LOCKED_KEYS:\n skel_val = skel.get(locked_key)\n fill_val = fill.get(locked_key)\n if skel_val != fill_val:\n # Truncate displayed values for readability\n self.errors.append(\n f\"V_CROSS_SYM_CLASSIFICATION_LOCKED: {path_base}.{locked_key} \"\n f\"modified by LLM: skeleton={_summary(skel_val)!r}, \"\n f\"filled={_summary(fill_val)!r}. \"\n f\"All pipeline-emitted whale fields are locked.\"\n )\n\n # V_CROSS_SYM_NARRATIVE_MUST_CITE_EVIDENCE\n evidence_required = skel.get(\"evidence_required_fields\") or []\n narrative = fill.get(\"identity_narrative\") or \"\"\n if not isinstance(narrative, str) or len(narrative.strip()) \u003c 30:\n # Too-short narrative also fails (defense in depth)\n # If empty / placeholder still present, R12 catches it separately.\n return\n if evidence_required:\n # Count how many of the evidence_required field names appear in the\n # narrative text (substring match). LLM should cite them by name\n # OR include a number that matches the locked value.\n cited = 0\n for field in evidence_required:\n if field.lower() in narrative.lower():\n cited += 1\n continue\n # Also accept if narrative contains the locked numeric value\n # for that field (e.g. \"9 个其他 Alpha 币\" cites cross_sym_count=9)\n sig = skel.get(\"behavior_signature\") or {}\n val_candidates = [\n skel.get(field),\n sig.get(field),\n ]\n for v in val_candidates:\n if v is None or v == \"\":\n continue\n # numeric: search for the integer or rounded-percent rendering\n try:\n v_num = float(v)\n # Try int + 1-2 decimal renderings\n if str(int(v_num)) in narrative:\n cited += 1\n break\n if v_num \u003c 1 and f\"{v_num*100:.0f}%\" in narrative:\n cited += 1\n break\n if f\"{v_num:.2f}\" in narrative or f\"{v_num:.1f}\" in narrative:\n cited += 1\n break\n except (ValueError, TypeError):\n if str(v) in narrative:\n cited += 1\n break\n if cited \u003c 2:\n self.errors.append(\n f\"V_CROSS_SYM_NARRATIVE_MUST_CITE_EVIDENCE: {path_base}.\"\n f\"identity_narrative cites only {cited}/{len(evidence_required)} \"\n f\"required evidence fields ({evidence_required!r}). Narrative \"\n f\"must cite ≥2 (by field name OR locked numeric value). \"\n f\"Got: {narrative[:120]!r}\"\n )\n\n # V_CROSS_SYM_NARRATIVE_NO_FREELANCE_IDENTITY — narrative cannot\n # assert an identity different from the locked enum.\n locked_enum = skel.get(\"identity_classification_enum\")\n if not locked_enum:\n return\n narrative_lower = narrative.lower()\n for other_enum, phrases in self._IDENTITY_ENUM_WORDS.items():\n if other_enum == locked_enum:\n continue # same enum, OK to mention\n for phrase in phrases:\n if phrase.lower() in narrative_lower:\n # Check for negation context to avoid FP — reuse Phase v0.6.4 helper\n if self._phrase_is_negated(narrative_lower, phrase.lower()):\n continue\n self.errors.append(\n f\"V_CROSS_SYM_NARRATIVE_NO_FREELANCE_IDENTITY: {path_base}.\"\n f\"identity_narrative asserts identity {other_enum!r} via \"\n f\"phrase {phrase!r}, but locked enum is {locked_enum!r}. \"\n f\"LLM cannot override pipeline classification.\"\n )\n return # one error per whale is enough\n\n # v0.7.7: wash_infrastructure section lock + narrative validation.\n _WASH_INFRA_LOCKED_KEYS = (\n \"executor_X\",\n \"maker_buy_P\",\n \"maker_sell_Q\",\n \"atomic_pair_ratio\",\n \"p_drift_pct\",\n \"q_drift_pct\",\n \"p_tok_in\",\n \"q_tok_in\",\n \"tx_from_diversity\",\n \"classification\",\n )\n\n _WASH_INFRA_CLASS_VALID = {\n \"wash_infrastructure_routed\",\n \"wash_infrastructure_operator_controlled\",\n \"wash_infrastructure_ambiguous\",\n }\n\n # v0.7.7: top-level wash section metadata is also pipeline-locked.\n # Cross-LLM security audit follow-up: previously these fields were not\n # locked by `_check_wash_infrastructure`, letting an LLM silently\n # alter `_credits_used` / `_n_candidates_scanned` / `_skip_reason`\n # / `_pipeline_source`.\n _WASH_INFRA_SECTION_LOCKED_KEYS = (\n \"_pipeline_source\",\n \"_credits_used\",\n \"_n_candidates_scanned\",\n \"_skip_reason\",\n )\n\n def _check_wash_infrastructure(self, skeleton: dict, filled: dict) -> None:\n \"\"\"v0.7.7 — wash_infrastructure section locked + writable validation.\n\n Pipeline emits the X / P / Q triplet and 5-step structural metrics\n as derived_locked. LLM may only write investigation_narrative per\n setup + the section-level summary_narrative.\n \"\"\"\n skel_wi = skeleton.get(\"wash_infrastructure\") or {}\n fill_wi = filled.get(\"wash_infrastructure\") or {}\n\n for k in self._WASH_INFRA_SECTION_LOCKED_KEYS:\n if skel_wi.get(k) != fill_wi.get(k):\n self.errors.append(\n f\"V_WASH_INFRA_SECTION_LOCKED: wash_infrastructure.{k} \"\n f\"modified by LLM: skeleton={_summary(skel_wi.get(k))!r}, \"\n f\"filled={_summary(fill_wi.get(k))!r}\"\n )\n\n skel_setups = skel_wi.get(\"setups\") or []\n fill_setups = fill_wi.get(\"setups\") or []\n\n if len(skel_setups) != len(fill_setups):\n self.errors.append(\n f\"V_WASH_INFRA_SETUPS_INVARIANCE: filled.wash_infrastructure.setups \"\n f\"length ({len(fill_setups)}) differs from skeleton \"\n f\"({len(skel_setups)}). LLM cannot add / remove setups.\"\n )\n return\n\n skel_executors = [(s.get(\"executor_X\") or \"\").lower() for s in skel_setups]\n fill_executors = [(s.get(\"executor_X\") or \"\").lower() for s in fill_setups]\n if skel_executors != fill_executors:\n self.errors.append(\n f\"V_WASH_INFRA_SETUPS_INVARIANCE: filled.wash_infrastructure.setups \"\n f\"executor_X list ({fill_executors}) differs from skeleton \"\n f\"({skel_executors}). LLM cannot reorder setups.\"\n )\n return\n\n for idx, (skel_s, fill_s) in enumerate(zip(skel_setups, fill_setups)):\n path_base = f\"wash_infrastructure.setups[{idx}]\"\n for locked_key in self._WASH_INFRA_LOCKED_KEYS:\n if skel_s.get(locked_key) != fill_s.get(locked_key):\n self.errors.append(\n f\"V_WASH_INFRA_LOCKED: {path_base}.{locked_key} modified \"\n f\"by LLM: skeleton={_summary(skel_s.get(locked_key))!r}, \"\n f\"filled={_summary(fill_s.get(locked_key))!r}\"\n )\n cls_val = skel_s.get(\"classification\")\n if cls_val and cls_val not in self._WASH_INFRA_CLASS_VALID:\n self.errors.append(\n f\"V_WASH_INFRA_CLASS_ENUM: {path_base}.classification \"\n f\"{cls_val!r} not in valid set {sorted(self._WASH_INFRA_CLASS_VALID)!r}\"\n )\n narr = fill_s.get(\"investigation_narrative\") or \"\"\n if narr == \"\u003cLLM_NARRATIVE_PLACEHOLDER>\" or not narr.strip():\n self.errors.append(\n f\"V_WASH_INFRA_NARRATIVE_MISSING: {path_base}.\"\n f\"investigation_narrative is empty / placeholder\"\n )\n\n def _check_array_field_uniqueness(\n self,\n items: list,\n field_path: str,\n error_code: str,\n threshold_pct: int,\n ) -> None:\n \"\"\"Helper: if > threshold_pct% of items share the same string, fail.\n\n v0.6.4: normalizes per-item content before counting — strips digits,\n normalizes whitespace, lowercases — so that\n boilerplate-with-fact-substitution evasion (e.g. \"上线前发现 2 个相关\n 接收地址,...\" vs \"上线前发现 0 个相关接收地址,...\", differing only\n in the count) collapses to the same normalized form and triggers\n the duplication check.\n\n Strips items that are None, empty, or stub strings.\n \"\"\"\n import re\n valid = [i for i in items if isinstance(i, str) and len(i) > 5]\n if len(valid) \u003c 3:\n return # too few items to judge\n\n def _norm(s: str) -> str:\n # Strip digits (1 / 12 / 2.5 / 99.9% etc.) and dollar amounts\n s = re.sub(r\"[\\d,.]+%?\", \"N\", s)\n s = re.sub(r\"\\$[\\d,.kKmM]+\", \"$X\", s)\n # Normalize whitespace + lowercase\n s = re.sub(r\"\\s+\", \" \", s.strip().lower())\n return s\n from collections import Counter\n # v0.6.4: dual check — exact match AND normalized match (the latter\n # catches boilerplate-with-fact-substitution evasion). P0-D fix\n # (adversarial review 2026-05-25 audit): normalized check uses a HIGHER threshold\n # (+15pp, capped at 95) AND requires the normalized form to be\n # substantive (≥30 chars) — short normalized strings often share\n # structure across genuinely-distinct narratives (FP risk adversarial review\n # raised for \"本类检测器命中\" vs \"另一类检测器命中\").\n norm_threshold = min(threshold_pct + 15, 95)\n for label, key_fn, eff_threshold, require_min_len in (\n (\"identical\", lambda s: s, threshold_pct, 0),\n (\"near-identical\", _norm, norm_threshold, 30),\n ):\n counts = Counter(key_fn(s) for s in valid)\n most_common_str, most_common_n = counts.most_common(1)[0]\n ratio = most_common_n / len(valid) * 100\n if ratio \u003c= eff_threshold:\n continue\n # P0-D: skip if normalized form is too short to meaningfully\n # signal duplication (short boilerplate naturally collides).\n if require_min_len and len(most_common_str) \u003c require_min_len:\n continue\n # Find a representative original (not normalized) string\n sample = next(s for s in valid if key_fn(s) == most_common_str)\n self.errors.append(\n f\"{error_code}: {field_path} has {most_common_n}/{len(valid)} \"\n f\"items ({ratio:.0f}%) sharing {label} narrative: \"\n f\"{sample[:80]!r}. LLM must write distinct content \"\n f\"for each item (threshold: {eff_threshold}%).\"\n )\n return # one error per field is enough\n\n # ============================================================\n # v0.7.3 narrative quality validators\n # ============================================================\n # Three soft-fail checks that catch dud narrative not caught by earlier\n # validators. v0.7.2 acceptance testing found two distinct failure modes:\n # 1. adversarial review/an LLM default to invoking tests/smoke_fill.py → caught by\n # render_report.py smoke gate (v0.7.2).\n # 2. another LLM did real LLM fill but with boilerplate evasion like\n # \"该字段已沿邻近锁定数据补充叙述\" repeated across slots, or\n # hallucinated numbers that don't match locked fields → caught\n # by the validators below (v0.7.3).\n # All three categorize as NARRATIVE_QUALITY so render still produces a\n # report (with warning header), but the agent gets clear feedback.\n\n # Phrase blacklist — observed boilerplate from cross-LLM testing that\n # carries no analytical content. Each phrase is a substring match on\n # casefolded narrative (case-insensitive). Conservative list: only\n # phrases that have NO legitimate analytical use case. Expanded only\n # after observing the phrase in real lazy-LLM output.\n _GENERIC_BOILERPLATE_PHRASES = (\n # another LLM v0.7.1 boilerplate (observed 2026-05-25):\n \"该字段已沿邻近锁定数据补充叙述\",\n \"本字段已沿邻近锁定数据补充叙述\",\n \"沿邻近锁定数据补充叙述\",\n # adversarial review smoke_fill template (observed 2026-05-25):\n \"本节数据已落地, 关键读数与结论推理一致\",\n \"本节数据已落地,关键读数与结论推理一致\",\n \"关键读数与结论推理一致\",\n # Other lazy patterns we've seen in cross-LLM testing:\n \"详见证据图中对应\",\n \"请参考相邻锁定字段\",\n \"请参考邻近锁定字段\",\n \"请参考相关字段\",\n \"数据见上方\",\n \"见上方表格\",\n \"见上文\",\n \"见相邻字段\",\n # English equivalents (an LLM sometimes mixes EN in ZH reports):\n \"see adjacent locked fields\",\n \"see above\",\n \"refer to the locked data\",\n \"data has been populated\",\n )\n\n def _check_narrative_generic_phrases(self, filled: dict) -> None:\n \"\"\"V_NARRATIVE_GENERIC_PHRASES — reject narrative slots that contain\n observed-in-the-wild boilerplate phrases adding no analytical content.\n\n A phrase appearing once in a long narrative is OK (might be quoting).\n We flag when:\n - The phrase is present AND\n - The narrative is short (\u003c 60 chars) — i.e. the phrase IS most of\n the content, not an aside.\n This avoids false-positives on legitimate narratives that happen to\n reference these phrases (e.g. discussing what NOT to write).\n \"\"\"\n for path, text in self._iter_narrative_strings(filled):\n if not isinstance(text, str) or len(text) \u003c 5:\n continue\n text_cf = text.casefold()\n for phrase in self._GENERIC_BOILERPLATE_PHRASES:\n phrase_cf = phrase.casefold()\n if phrase_cf not in text_cf:\n continue\n # Length-gate: if narrative is mostly boilerplate, fire.\n if len(text) \u003c 60:\n self.errors.append(\n f\"V_NARRATIVE_GENERIC_PHRASES: {path} contains \"\n f\"boilerplate phrase {phrase!r} and is too short \"\n f\"({len(text)} chars) to carry independent content. \"\n f\"Write a real interpretation tied to the locked \"\n f\"data adjacent to this slot. Got: {text[:120]!r}\"\n )\n break\n\n def _check_narrative_template_reuse(self, filled: dict) -> None:\n \"\"\"V_NARRATIVE_TEMPLATE_REUSE — global cross-slot template detection.\n\n _check_no_narrative_duplication already catches per-array duplication\n (e.g. all 12 m6.rows.narrative identical). This check catches the\n CROSS-section case: writer uses one template across many independent\n slots (e.g. anomaly.detector_summary[].detail + section.interpretation\n + key_takeaways[] all share the same \"本节数据已落地\" template).\n\n Strategy: collect every narrative string across the whole filled\n document, normalize via the same _norm() as v0.6.4, count occurrences,\n flag the largest cluster if it covers > 25% of the corpus (and corpus\n size ≥ 8). Threshold 25% chosen empirically — another llm v0.7.1 report had\n the \"该字段已沿邻近...\" template covering ~40% of slots.\n \"\"\"\n import re\n from collections import Counter\n # Collect narrative strings from the filled document.\n strings = []\n for path, text in self._iter_narrative_strings(filled):\n if not isinstance(text, str) or len(text) \u003c 20:\n continue\n strings.append((path, text))\n if len(strings) \u003c 8:\n return # corpus too small to meaningfully detect templates\n # Same normalization as _check_array_field_uniqueness (strips digits,\n # dollar amounts, whitespace, case) so factual substitution doesn't\n # disguise template reuse.\n def _norm(s: str) -> str:\n s = re.sub(r\"[\\d,.]+%?\", \"N\", s)\n s = re.sub(r\"\\$[\\d,.kKmM]+\", \"$X\", s)\n s = re.sub(r\"\\s+\", \" \", s.strip().lower())\n return s\n norm_to_paths: dict[str, list[str]] = {}\n for path, text in strings:\n key = _norm(text)\n if len(key) \u003c 25:\n continue # short normalized form, naturally collides\n norm_to_paths.setdefault(key, []).append(path)\n if not norm_to_paths:\n return\n counts = Counter({k: len(v) for k, v in norm_to_paths.items()})\n top_key, top_n = counts.most_common(1)[0]\n ratio = top_n / len(strings) * 100\n if ratio \u003c 25 or top_n \u003c 3:\n return\n # Representative original string for the offender cluster\n sample_paths = norm_to_paths[top_key]\n sample_text = next(\n text for path, text in strings if _norm(text) == top_key\n )\n self.errors.append(\n f\"V_NARRATIVE_TEMPLATE_REUSE: {top_n}/{len(strings)} narrative \"\n f\"slots ({ratio:.0f}%) share the same normalized template. \"\n f\"Write distinct content per slot. Offender (sample): \"\n f\"{sample_text[:120]!r}. Paths: {sample_paths[:5]}\"\n f\"{' ...' if len(sample_paths) > 5 else ''}\"\n )\n\n def _check_narrative_numeric_hallucination(self, filled: dict) -> None:\n \"\"\"V_NARRATIVE_NUMERIC_HALLUCINATION — every concrete number cited in\n a narrative slot must appear in some locked numeric field nearby,\n within ±5% relative tolerance (or ±0.5 absolute for small numbers).\n\n Catches LLMs that invent plausible-looking numbers in narrative:\n - \"操盘者持有 0.5%\" when locked.pct_of_total = 0.19%\n - \"派发了约 $1.2M\" when locked.usd_value = $244,000\n - \"12 小时前\" when actual timestamp is 36 hours ago\n\n Excluded number patterns (high false-positive risk):\n - Tx hash short forms (0x followed by hex)\n - Ratio-like phrasing (\"3:1\", \"2/3\")\n - Common ordinals (\"第 1 个\", \"第二个\")\n - Single digits in regulation-text contexts (Rule 11, S1, S2, etc.)\n\n Tolerance: a narrative number N matches locked L if either:\n |N - L| / max(|L|, 1) \u003c= 0.05 (5% relative)\n |N - L| \u003c= 0.5 (absolute, for small N/L)\n \"\"\"\n import re\n # Collect every numeric value from the locked side. We pool from the\n # whole filled document (not just adjacent fields) — narrative may\n # reference numbers computed elsewhere. Tolerance is the gate.\n locked_numbers = self._collect_all_numbers(filled)\n if not locked_numbers:\n return\n # Number regex: matches integers and floats with optional %, K, M, B\n # suffix or $ prefix. Captures the bare numeric value.\n # v0.7.3 P1 (E2E found): allow comma-thousands separator so\n # \"$282,740\" matches as one number, not \"$282\" + \"740\".\n num_re = re.compile(\n r\"(?\u003c![\\w0-9])\" # not preceded by letter/digit\n r\"\\$?([0-9]+(?:,[0-9]{3})*(?:\\.[0-9]+)?)\" # 1 / 1,234 / 1,234.56\n r\"\\s?([%KkMmBb])?\" # optional unit suffix\n r\"(?![\\w])\" # not followed by letter\n )\n # Skip patterns — narrative contexts where embedded numbers are\n # rules/identifiers/time-ago, not factual claims.\n skip_re = re.compile(\n r\"(?:0x[0-9a-fA-F]{4,}\" # tx hash / address\n r\"|rule\\s*1?1\" # Rule 11\n r\"|s[123]\\b\" # S1/S2/S3 tier markers\n r\"|m[3-9]\\b\" # M3-M9 detector codes\n r\"|d[1-9]\\b\" # D1-D9 detector codes\n r\"|第\\s*[一二三四五六七八九十百]\" # Chinese ordinals\n # Time-ago / duration phrases — these numbers are derived from\n # now(), not stored in any locked field. Skip the whole window.\n r\"|\\d+\\s*(?:小时|分钟|秒|天|周|月|年|hour|minute|second|day|week|month|year)s?\\s*(?:前|ago)?\"\n r\"|约\\s*\\d+\\s*(?:小时|分钟|天|周|月|h\\b)\" # 约 12 小时\n r\"|\\d+\\s*[-–]\\s*\\d+\\s*(?:小时|h|天|day)\" # 12-24h ranges\n # Section / chapter markers\n r\"|chapter\\s*\\d+\"\n # Times: HH:MM, HH:MM:SS — narrative often quotes evt timestamps\n r\"|\\d{1,2}:\\d{2}(?::\\d{2})?\"\n # Dates: YYYY-MM-DD (the year is already skipped, but day/month digits\n # remain — easier to skip the whole token).\n r\"|\\d{4}-\\d{1,2}-\\d{1,2}\"\n # List size / rank: \"Top-100 holder\", \"前 100 持币\", \"top 5\"\n r\"|top\\s*-?\\s*\\d+\"\n r\"|前\\s*\\d+\\s*(?:大|名|个|位)?\"\n r\")\",\n re.IGNORECASE,\n )\n hits = [] # collect to emit one consolidated error\n for path, text in self._iter_narrative_strings(filled):\n if not isinstance(text, str) or len(text) \u003c 30:\n continue\n # v0.7.14 (adversarial review HIGH #1): no path-based exemption here. An earlier\n # patch skipped `m4_notes[0]` because the pipeline-authored Rule 11\n # summary cited numbers (mint_amount etc.) that were not mirrored as\n # standalone locked numeric fields, so they tripped the hallucination\n # check. But `lineage.m4_notes[]` is WRITABLE — exempting index 0\n # would let an LLM smuggle a fabricated number through. The proper\n # fix is upstream: forensic_pipeline now exposes those numbers as\n # `lineage.summary_locked_numbers`, so they enter the locked-number\n # pool naturally and m4_notes[0] passes without an exemption.\n # Mask out skip patterns so their embedded digits don't surface.\n masked = skip_re.sub(\" SKIP \", text)\n for m in num_re.finditer(masked):\n raw = m.group(1)\n suffix = (m.group(2) or \"\").lower()\n try:\n # Strip comma-thousands separators before float parse.\n val = float(raw.replace(\",\", \"\"))\n except ValueError:\n continue\n # Apply unit suffix\n if suffix == \"%\":\n # Percentages can be stored as 0.x ratio OR raw % — check both\n candidates = (val, val / 100.0)\n elif suffix == \"k\":\n candidates = (val * 1_000,)\n elif suffix == \"m\":\n candidates = (val * 1_000_000,)\n elif suffix == \"b\":\n candidates = (val * 1_000_000_000,)\n else:\n candidates = (val,)\n # FP guard: ignore very small bare numbers (1-10 with no\n # unit) — often \"3 weeks ago\", \"Top 5\", \"2 days\", indices.\n if not suffix and val \u003c 10:\n continue\n # Ignore 4-digit years (2024-2027).\n if not suffix and 2020 \u003c= val \u003c= 2030:\n continue\n if not any(self._number_matches(c, locked_numbers) for c in candidates):\n hits.append((path, m.group(0), candidates))\n # Emit a max of 5 hits per validator run to keep error log readable.\n for path, snippet, candidates in hits[:5]:\n cand_str = \", \".join(f\"{c:.4g}\" for c in candidates)\n self.errors.append(\n f\"V_NARRATIVE_NUMERIC_HALLUCINATION: {path} mentions number \"\n f\"{snippet!r} (parsed as {cand_str}) which does not match \"\n f\"any locked numeric field within ±5%. Either cite the \"\n f\"locked value verbatim or rephrase to avoid the number.\"\n )\n if len(hits) > 5:\n self.errors.append(\n f\"V_NARRATIVE_NUMERIC_HALLUCINATION: ...and {len(hits)-5} \"\n f\"more unmatched numbers across narrative slots (showing \"\n f\"first 5).\"\n )\n\n # --- helpers for the 3 v0.7.3 validators ---\n\n _NARRATIVE_LEAF_SUFFIXES = (\n \".one_liner\", \".verdict_impact\", \".interpretation\", \".detail\",\n \".nature\", \".identity_narrative\", \".risk_assessment_narrative\",\n \".status_narrative\", \".alert\", \".rationale\", \".narrative\",\n \".summary_narrative\", \".monitoring_footer\", \".hours_ago_text\",\n \".status_text\",\n )\n\n def _iter_narrative_strings(self, filled):\n \"\"\"Yield (path, string) pairs for every writable narrative slot.\n Path uses dotted notation with [i] for list indices. Skips\n non-narrative metadata (anything starting with `_`).\"\"\"\n stack = [(\"\", filled)]\n while stack:\n path, node = stack.pop()\n if isinstance(node, str):\n # Only yield strings whose path looks like a narrative slot.\n if any(path.endswith(suf) for suf in self._NARRATIVE_LEAF_SUFFIXES):\n yield path, node\n # Also yield list items of narrative arrays (key_takeaways[])\n elif \"key_takeaways\" in path or \"m4_notes\" in path:\n yield path, node\n elif isinstance(node, dict):\n for k, v in node.items():\n if k.startswith(\"_\"):\n continue\n stack.append((f\"{path}.{k}\" if path else k, v))\n elif isinstance(node, list):\n for i, v in enumerate(node):\n stack.append((f\"{path}[{i}]\", v))\n\n def _collect_all_numbers(self, filled):\n \"\"\"Walk the whole filled doc and collect every numeric value\n from non-narrative fields. Used by numeric hallucination check.\"\"\"\n numbers = set()\n stack = [(\"\", filled)]\n while stack:\n path, node = stack.pop()\n if isinstance(node, (int, float)) and not isinstance(node, bool):\n numbers.add(float(node))\n elif isinstance(node, str):\n # Don't trust strings — narrative slots are filled with\n # strings too. But locked numeric fields are stored as\n # actual int/float in JSON, so the float-walk above is\n # sufficient.\n pass\n elif isinstance(node, dict):\n for k, v in node.items():\n if k.startswith(\"_\"):\n continue\n # Skip narrative leaves — they're what we're validating.\n if any(k == suf.lstrip(\".\") for suf in self._NARRATIVE_LEAF_SUFFIXES):\n continue\n stack.append((f\"{path}.{k}\" if path else k, v))\n elif isinstance(node, list):\n for i, v in enumerate(node):\n stack.append((f\"{path}[{i}]\", v))\n return numbers\n\n @staticmethod\n def _number_matches(val: float, locked_numbers: set) -> bool:\n \"\"\"True if val is within ±5% relative or ±0.5 absolute of any\n number in locked_numbers.\n\n v0.9.6 Fix #4 (adversarial review Windows EVAA 2026-06-15 feedback): also\n match on MAGNITUDE (|val| vs |L|). The narrative regex extracts\n unsigned digits — e.g. \"余额 -591\" parses as 591. The locked\n field is signed (`balance_tokens: -590.5980...`). Pre-v0.9.6,\n 591 vs -590.598 failed both relative (591 + 590.598 = 1181.6,\n ratio 2.0 ≫ 5%) and absolute (>0.5) checks, hard-failing the\n render with V_NARRATIVE_NUMERIC_HALLUCINATION even though the\n narrative was correctly referring to the locked quantity.\n Magnitude fallback closes this false positive.\n \"\"\"\n val_abs = abs(val)\n for L in locked_numbers:\n denom = max(abs(L), 1.0)\n # Signed exact (or near) match: original behavior.\n if abs(val - L) / denom \u003c= 0.05:\n return True\n if abs(val - L) \u003c= 0.5:\n return True\n # Magnitude fallback: narrative may strip the sign.\n L_abs = abs(L)\n if abs(val_abs - L_abs) / denom \u003c= 0.05:\n return True\n if abs(val_abs - L_abs) \u003c= 0.5:\n return True\n return False\n\n\n## v0.7.1: Error categorization for soft-fail vs hard-abort.\n\n# Error code prefixes that indicate LLM tampered locked data, pipeline\n# regression, or security boundary breach. These NEVER let the report\n# render — render_report.py must abort with exit 1.\nSTRUCTURAL_ERROR_PREFIXES = (\n \"V_LOCKED_FIELD_\", # locked field modified or absent\n \"V_SCHEMA_VERSION\", # schema mismatch\n \"V_CROSS_SYM_WHALES_INVARIANCE\", # LLM added/removed whales\n \"V_CROSS_SYM_CLASSIFICATION_LOCKED\", # LLM tampered classification\n \"V_R12_PLACEHOLDERS_REMAIN\", # placeholder leaked to output\n \"V_PROVENANCE_\", # provenance chain broken\n \"V_CAUSAL_RULE11\", # rule 11 pipeline regression\n \"V_R2_M6_NO_DETECTOR\", # pipeline didn't emit detector\n \"V_R10_DETECTOR_TOO_FEW\", # pipeline regression\n \"V_NO_DATA_SOURCE_LEAK\", # security\n \"V_WRITABLE_COUNT\", # array length mismatch\n \"V_INVALID_AUTHORITY\", # YAML config corrupt\n)\n\n# Error code prefixes that are RECOVERABLE — narrative quality issues.\n# Render the report anyway with warning. Caller (AI agent) may re-fill\n# the specific narrative slot and re-render to clear.\nNARRATIVE_QUALITY_ERROR_PREFIXES = (\n \"V_WRITABLE_TOO_SHORT\",\n \"V_NARRATIVE_DUPLICATION\",\n \"V_INTERPRETATION_DUPLICATION\",\n \"V_KEY_TAKEAWAYS_DUPLICATION\",\n \"V_DETECTOR_DUPLICATION\",\n \"V_CROSS_SYM_NARRATIVE_MUST_CITE_EVIDENCE\",\n \"V_CROSS_SYM_NARRATIVE_NO_FREELANCE_IDENTITY\",\n \"V_NARRATIVE_VS_LOCKED_SEMANTIC\",\n \"V_META_LANGUAGE\",\n \"V_R7_RHYTHM_MATCHES_WAVES\",\n # v0.7.3 narrative quality validators\n \"V_NARRATIVE_GENERIC_PHRASES\",\n \"V_NARRATIVE_TEMPLATE_REUSE\",\n \"V_NARRATIVE_NUMERIC_HALLUCINATION\",\n)\n\n\ndef categorize_errors(errors: list[str]) -> tuple[list[str], list[str]]:\n \"\"\"Split errors into (structural, narrative_quality) lists.\n\n An error is structural if its code starts with any prefix in\n STRUCTURAL_ERROR_PREFIXES. Otherwise it's classified as narrative\n quality.\n\n Unknown prefixes default to STRUCTURAL (fail-closed): if a new error\n code is added without updating this categorizer, it goes into the\n abort path until explicitly classified.\n\n v0.7.19.5: V_NARRATIVE_NUMERIC_HALLUCINATION is now classified as\n STRUCTURAL by default — a hardcoded number in narrative that does\n not match any locked field is a DATA CORRECTNESS bug (the report\n self-contradicts its own locked tables), not a style preference.\n The COLLECT v0.7.19.4 rerun (price had moved from $0.0578 to $0.0522,\n LP from $635K to None / $1.82M, entry_cap from $8K to $9,239) had\n 4 such warnings — they were soft-passed by render, the report\n shipped with \"$8K\" while the locked decision-summary table said\n \"$9,239\" right next to it. Set\n BINANCE_ALPHA_ALLOW_NUMERIC_WARNINGS=1 (truthy) to keep the old\n soft-warning behavior for dev / CI flows that intentionally test\n against stale fixtures.\n \"\"\"\n import os as _os\n allow_numeric_warn = _os.environ.get(\n \"BINANCE_ALPHA_ALLOW_NUMERIC_WARNINGS\", \"\"\n ).lower() in (\"1\", \"true\", \"yes\", \"on\")\n\n structural = []\n narrative = []\n for err in errors:\n # Error format: \"V_CODE: details\"\n code = err.split(\":\", 1)[0].strip()\n is_narrative = any(code.startswith(p) for p in NARRATIVE_QUALITY_ERROR_PREFIXES)\n is_structural = any(code.startswith(p) for p in STRUCTURAL_ERROR_PREFIXES)\n # v0.7.19.5: NUMERIC_HALLUCINATION is structural-by-default unless\n # the dev opted in via env. This keeps the COLLECT-class\n # narrative-vs-locked mismatch from ever shipping again.\n if (code == \"V_NARRATIVE_NUMERIC_HALLUCINATION\"\n and not allow_numeric_warn):\n structural.append(err)\n continue\n if is_narrative and not is_structural:\n narrative.append(err)\n else:\n structural.append(err)\n return structural, narrative\n\n\ndef _summary(v) -> str:\n \"\"\"Truncate a value for error messages.\"\"\"\n s = json.dumps(v, ensure_ascii=False, default=str)\n return s if len(s) \u003c= 80 else s[:77] + \"...\"\n\n\ndef main() -> int:\n # Beta.15: force UTF-8 stdout/stderr (Windows cp1252 console fix).\n for stream in (sys.stdout, sys.stderr):\n if hasattr(stream, \"reconfigure\"):\n try:\n stream.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[0])\n ap.add_argument(\"--skeleton\", required=True, help=\"Pipeline output (pre-LLM-fill)\")\n ap.add_argument(\"--filled\", required=True, help=\"LLM-filled JSON\")\n ap.add_argument(\"--quiet\", action=\"store_true\")\n # v0.6.0-alpha.3 (adversarial review feedback): removed --yaml and --corpus CLI flags.\n # Allowing override created a validator bypass surface: adversarial review demonstrated\n # passing v0.6.0-alpha.2 by moving the locked line for action_enum out of\n # a custom yaml. Hardcoded to schema/ directory now; no opt-out.\n args = ap.parse_args()\n\n skel = json.loads(Path(args.skeleton).read_text(encoding=\"utf-8\"))\n filled = json.loads(Path(args.filled).read_text(encoding=\"utf-8\"))\n\n v = Validator() # locked to schema/ defaults\n errors = v.validate(skel, filled)\n\n if errors:\n structural, narrative = categorize_errors(errors)\n if structural:\n print(f\"V_SEMANTIC_VALIDATION STRUCTURAL FAIL: {len(structural)} blocking issue(s):\", file=sys.stderr)\n for i, e in enumerate(structural, 1):\n print(f\" {i}. {e}\", file=sys.stderr)\n if narrative:\n print(f\"V_SEMANTIC_VALIDATION NARRATIVE_QUALITY: {len(narrative)} recoverable issue(s):\", file=sys.stderr)\n for i, e in enumerate(narrative, 1):\n print(f\" {i}. {e}\", file=sys.stderr)\n return 1 if structural else 2\n if not args.quiet:\n print(f\"OK: validator pass ({len(filled.get('evidence_graph', {}))} evidence entries)\")\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":89069,"content_sha256":"d99d1aa940257a108ce85153428a0a1a56fd55246cb4c44c0935d5c173c80955"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"hertzflow — Crypto trade-decision intelligence (single skill, multi-domain)","type":"text"}]},{"type":"paragraph","content":[{"text":"hertzflow","type":"text","marks":[{"type":"code_inline"}]},{"text":" is HertzFlow's umbrella skill for on-chain + CEX trading research. Like Surf, it's one skill with internal routing — read the table below to see which sub-domain handles the user's request, then follow that sub-domain's ","type":"text"},{"text":"INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"🌐 Language — MANDATORY, read first","type":"text"}]},{"type":"paragraph","content":[{"text":"Detect the language of the user's most recent message and use that language for everything: your conversation replies AND the generated report.","type":"text","marks":[{"type":"strong"}]},{"text":" This is not optional and not a preference the user has to ask for.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Message contains CJK characters (","type":"text"},{"text":"一-鿿","type":"text","marks":[{"type":"code_inline"}]},{"text":", kana, hangul) → operate in ","type":"text"},{"text":"Chinese","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"zh","type":"text","marks":[{"type":"code_inline"}]},{"text":"): reply in Chinese, run the pipeline with ","type":"text"},{"text":"--lang zh","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Otherwise (English / Latin-script prompt) → operate in ","type":"text"},{"text":"English","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":"): reply in English, run the pipeline with ","type":"text"},{"text":"--lang en","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Supported report languages are ","type":"text"},{"text":"zh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":" (static, human-curated packs). For a prompt in any other language, reply in that language but generate the report in ","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":" (closest supported), and say so in one line.","type":"text"}]},{"type":"paragraph","content":[{"text":"The report-body language is locked at ","type":"text"},{"text":"pipeline","type":"text","marks":[{"type":"strong"}]},{"text":" time via ","type":"text"},{"text":"forensic_pipeline.py --lang {zh|en}","type":"text","marks":[{"type":"code_inline"}]},{"text":" — it cannot be changed at render time, and narrative fills must be authored in the same language. See the \"Report language\" section of ","type":"text"},{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the exact rule. Getting this wrong forces a full pipeline re-run (re-spends Surf credits), so set it from the user's language up front.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Routing table","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"If the user input matches…","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sub-domain","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Instructions file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Raw 0x-prefixed 40-hex EVM CA (","type":"text"},{"text":"0xea37a8de…","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Binance Alpha forensic)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solana base58 CA (e.g. ","type":"text"},{"text":"Grass7B4R…","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha","type":"text","marks":[{"type":"code_inline"}]},{"text":" (HOLDER_SNAPSHOT mode)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ticker of a token currently on Binance Alpha (","type":"text"},{"text":"$JCT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"$VELVET","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha","type":"text","marks":[{"type":"code_inline"}]},{"text":" — first resolve ticker → CA via ","type":"text"},{"text":"surf search-token --q \u003cticker>","type":"text","marks":[{"type":"code_inline"}]},{"text":", then dispatch","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Natural-language phrasing about Alpha distribution / 内幕出货 / 链上派发 / quiet insider / chip structure","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Perp symbol (e.g. ","type":"text"},{"text":"BTCUSDT","type":"text","marks":[{"type":"code_inline"}]},{"text":") + analysis intent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"perp","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not yet shipped)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"— coming soon","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bridge protocol name + audit intent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bridge","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not yet shipped)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"— coming soon","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HertzFlow's own contract address + diagnostic intent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"core","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not yet shipped)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"— coming soon","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"First action when invoked","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identify the sub-domain from the routing table above (default: ","type":"text"},{"text":"alpha","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"cat \u003csub-domain>/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (paths are relative to this skill's install root — at runtime that's ","type":"text"},{"text":"~/.claude/skills/hertzflow/","type":"text","marks":[{"type":"code_inline"}]},{"text":" or the equivalent agent skill dir, NOT the repo root. So for the alpha sub-domain, read ","type":"text"},{"text":"alpha/INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".) Or read it via the agent's file-read tool) — that file is the full per-domain SKILL.md, including onboarding (Surf CLI install + API key prompt), prerequisites, 3-step workflow (","type":"text"},{"text":"forensic_pipeline.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → LLM fill → ","type":"text"},{"text":"render_report.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"), writable-slot guide, validator rules, output schema.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow the instructions in that file verbatim. Do ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" re-derive the workflow from this router file — the per-domain instructions are authoritative and version-locked.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Why this skill is structured this way","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Single slash ","type":"text","marks":[{"type":"strong"}]},{"text":"/hertzflow","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":": HertzFlow is one product brand covering multiple research domains (Alpha forensic, perp metrics, bridge audit, HertzFlow core). Users invoke one slash, the router handles dispatch.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Description bait is wide","type":"text","marks":[{"type":"strong"}]},{"text":": the description (above) tells the agent to auto-trigger on natural-language mentions of insider distribution, chip structure, etc. — like Surf, users don't have to type the slash explicitly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sub-domain isolation","type":"text","marks":[{"type":"strong"}]},{"text":": each sub-domain lives in its own folder with its own ","type":"text"},{"text":"INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", helpers, and version tracking. Adding ","type":"text"},{"text":"perp/","type":"text","marks":[{"type":"code_inline"}]},{"text":" later does not require touching ","type":"text"},{"text":"alpha/","type":"text","marks":[{"type":"code_inline"}]},{"text":" code.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Trust boundaries (apply to every sub-domain)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pipeline (Python).","type":"text","marks":[{"type":"strong"}]},{"text":" Deterministic. Owns all SQL, on-chain reads, evidence-graph IDs. The only layer that touches data.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LLM (any model).","type":"text","marks":[{"type":"strong"}]},{"text":" Fills narrative slots inside structured writable fields. Cannot write SQL. Cannot pick the verdict. Cannot decide thresholds.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validator.","type":"text","marks":[{"type":"strong"}]},{"text":" Rejects LLM writes outside the writable slots, references to data not in the evidence graph, duplicated boilerplate, and any attempt to overwrite locked pipeline output.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Every sub-domain's ","type":"text"},{"text":"INSTRUCTIONS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" must respect these three boundaries.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Upgrade","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"npx skills update hertzflow","type":"text"}]},{"type":"paragraph","content":[{"text":"That pulls the latest revision from ","type":"text"},{"text":"main","type":"text","marks":[{"type":"code_inline"}]},{"text":" of ","type":"text"},{"text":"HertzFlow/hertzflow-skills","type":"text","marks":[{"type":"code_inline"}]},{"text":". Run after major releases (","type":"text"},{"text":"v0.9.x","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"v1.0.0","type":"text","marks":[{"type":"code_inline"}]},{"text":" etc.). Current version is in the frontmatter (","type":"text"},{"text":"metadata.version","type":"text","marks":[{"type":"code_inline"}]},{"text":") and ","type":"text"},{"text":"alpha/v06/helpers/_version.py","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"hertzflow","tools":["bash"],"author":"@skillopedia","source":{"stars":94,"repo_name":"hertzflow-skills","origin_url":"https://github.com/HertzFlow/hertzflow-skills/blob/HEAD/skills/hertzflow/SKILL.md","repo_owner":"HertzFlow","body_sha256":"342c0269b9e0f31a8fbce34264b4ad4e8aa9d160730b8b1626d92bb9b802744c","cluster_key":"9ffbe71a161dc3828fb5f79eb062ad9afa0f5bd5dc806d6f67674ec9142efcc9","clean_bundle":{"format":"clean-skill-bundle-v1","source":"HertzFlow/hertzflow-skills/skills/hertzflow/SKILL.md","text_bytes":1048137,"source_commit":"ffcf579b6415a898cb9307b57618a61466508318","skills_sh_rank":16813,"attachment_count":34,"text_attachments":34,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/hertzflow/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"finance-legal-compliance","category_label":"Finance"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"finance-legal-compliance","metadata":{"version":"0.9.0"},"import_tag":"skills-sh-top20k-2026-11","description":"HertzFlow on-chain trade-decision intelligence. Currently covers Binance Alpha forensic across all surf-SQL EVM chains (BSC / Ethereum / Arbitrum / Base / Polygon / Optimism) — insider distribution, 真实派发 confirmed sell-out, 筹码三分法 (operator / CEX pool / verifiable retail), anomaly waves, monitoring exports. Solana runs in HOLDER_SNAPSHOT mode.\nAuto-trigger whenever the user pastes a raw 0x-prefixed 40-hex EVM CA, a Solana base58 CA, mentions a Binance Alpha token by ticker, or asks about 链上 forensic / 内幕出货 / 派发 / chip structure / quiet insider / Alpha distribution / on-chain dump — even if they don't say \"hertzflow\" explicitly. Pipeline runs deterministically (~2-10 min per CA depending on activity + surf cache state); LLM only fills narrative slots, never picks the verdict or writes SQL.\nPerp metrics, bridge audits, and HertzFlow core contract analysis sub-domains are coming — when those ship, this skill will dispatch to them based on input pattern (perp symbol, bridge protocol name, etc.) using the router table below.\nREQUIRES a Surf account + SURF_API_KEY. New users get 2000 free credits (~6-8 reports) via the HertzFlow private invite. Full forensic costs ~$1.5-3 USD per CA in Surf credits after the free tier runs out."}},"renderedAt":1793666369454}

hertzflow — Crypto trade-decision intelligence (single skill, multi-domain) is HertzFlow's umbrella skill for on-chain + CEX trading research. Like Surf, it's one skill with internal routing — read the table below to see which sub-domain handles the user's request, then follow that sub-domain's . 🌐 Language — MANDATORY, read first Detect the language of the user's most recent message and use that language for everything: your conversation replies AND the generated report. This is not optional and not a preference the user has to ask for. - Message contains CJK characters ( , kana, hangul) →…