Compare commits

...

18 Commits

Author SHA1 Message Date
davidsx dfea6e8fae Merge pull request #187 from gina888666/fix/robinhood-chain-detection 2026-07-27 20:39:12 +08:00
gina888666 56648c5286 fix(holder-analysis): add robinhood to known chains to skip auto-detection
When CHAIN is explicitly set to 'robinhood', the auto-detection logic
previously treated it as unknown and fell back to probing bsc/eth/base,
returning empty data and forcing slow inline workarounds.

Adding a KNOWN_CHAINS tuple that includes 'robinhood' (and 'sol') causes
the script to skip the probe entirely and query the specified chain directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-27 20:30:10 +08:00
GMGN.AI 7205bf20d5 1.5.4 2026-07-23 12:34:41 +08:00
GMGN.AI 90d938ec09 Merge pull request #185 from GMGNAI/docs-signal-type-1-21
docs(market): extend signal_type range to 21 and name new types
2026-07-23 12:34:02 +08:00
David Lau fa556acb93 docs(market): extend signal_type range to 21 and name new types
Backend now accepts signal_type 1-21. Add types 19 (PlatformCallV2),
20 (KOLBuy), 21 (BankerClaims) to the signal-types tables and update the
1-18 -> 1-21 range references.

- src/commands/market.ts: --signal-type help text 1-18 -> 1-21
- docs/cli-usage.md: param note + signal-types table rows 19/20/21
- skills/gmgn-market/SKILL.md: param note, response-field note, signal-types table

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:23:36 +08:00
GMGN.AI abcb7bec93 1.5.3 2026-07-22 20:23:38 +08:00
GMGN.AI 11ef4fea61 Merge pull request #183 from GMGNAI/feat-robinhood-kol-smartmoney-signal-cooking
feat(chains): support robinhood on kol, smartmoney, signal and cooking
2026-07-22 20:22:36 +08:00
David Lau 748ec55e6e docs(gmgn-market): add robinhood to market signal Parameters chain line
Missed one stale "sol / bsc only" reference in the Parameters section;
signal supports sol / bsc / robinhood.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:53:25 +08:00
David Lau e68e889c3c feat(chains): support robinhood on kol, smartmoney, signal and cooking
- Remove the robinhood-rejection guards in kol, smartmoney, signal and
  cooking create-token so the CLI matches the backend, which now accepts
  robinhood on these endpoints
- Update --chain / --dex / --raised-token help text and error messages
- cooking: robinhood launchpad uses trench / pons DEXes, native raised
  token only
- Sync SKILL.md files, cli-usage.md and both Readmes with the new chain
  support

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:53:25 +08:00
GMGN.AI 97df79b9f6 Merge pull request #182 from camila330/add-gmgn-wallet-score-skill
Add gmgn-wallet-score skill for wallet copy-trade scoring
2026-07-22 15:19:48 +08:00
Mila e9c8b0a9b5 Add gmgn-wallet-score skill for wallet copy-trade scoring
Adds a new skill that scores any wallet address across three angles —
profitability (track-record score), copy-tradeability (score + a
latency/slippage/gas backtest), and Dev reputation for token-creator
wallets — plus trading-style tags, computed deterministically from
GMGN portfolio data. Wires it into CLAUDE.md's skill routing and the
Readme skill tables/install instructions, and refreshes the stale
plugin metadata descriptions/keywords to reflect all 8 current skills.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 12:07:22 +08:00
GMGN.AI c84e322bc7 Merge pull request #181 from gina888666/fix/gmgn-holder-analysis-output-and-style
fix(gmgn-holder-analysis): fix duplicate output and simplify entry cost section
2026-07-20 20:51:37 +08:00
gina888666 1c618a4883 fix(gmgn-holder-analysis): fix duplicate output and simplify entry cost section
- Fix output appearing twice by clarifying Output Rule in SKILL.md
- Simplify entry cost batch detail: replace per-wallet lists with
  selling/accumulating counts on the batch summary line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-20 20:27:21 +08:00
davidsx f77da2a95e Merge pull request #180 from GMGNAI/fix-sanitize-token-metadata
fix(security): defend against prompt injection via token metadata
2026-07-20 19:27:07 +08:00
GMGN.AI b5fdaad3b3 Merge pull request #179 from gina888666/feat/gmgn-holder-analysis-optimize
feat(gmgn-holder-analysis): extract script to file + enforce verbatim output
2026-07-20 16:41:25 +08:00
gina888666 7696a45a36 feat(gmgn-holder-analysis): extract script to file and enforce verbatim output
Two improvements:
1. Extract 700-line inline Python script to analyze.py — Claude no longer
   streams the entire script as text, reducing response time from ~2min to
   ~5s (API calls take ~3s, script execution <1s).
   Also adds full 3-way concurrency: holders + dev fetched in parallel, then
   created-tokens fires immediately when dev result arrives — saving ~1.2s vs
   prior serial approach. EVM addresses auto-detect chain (bsc→eth→base).

2. Add Output Rule section after the run command — Claude's reply MUST contain
   only the raw stdout verbatim, no summary or omission. Previously the CRITICAL
   note appeared before the command, so it was forgotten by reply time; moving
   it after (as the last thing Claude reads before replying) enforces compliance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-20 16:22:24 +08:00
GMGN.AI 020190f088 Merge pull request #178 from gina888666/feat/gmgn-holder-analysis
feat: add gmgn-holder-analysis skill
2026-07-16 20:43:53 +08:00
wuyanling 91489bda9d feat: add gmgn-holder-analysis skill
New skill for analyzing token holder chip structure.
Includes dump risk, dev wallet status, related funds,
quality signals, entry cost batches, buying power,
and AI recommendation. Supports zh/en bilingual output
via LANG env var. Uses pure Python subprocess + ThreadPoolExecutor,
no /tmp files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 20:39:41 +08:00
17 changed files with 2096 additions and 58 deletions
+8 -4
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "gmgn-cli",
"description": "GMGN OpenAPI skills for AI coding assistants — token info, market data, wallet portfolio, and swap execution across sol / bsc / base / eth / robinhood",
"description": "GMGN OpenAPI skills for AI coding assistants — token info, market data, wallet portfolio & copy-trade scoring, and swap execution across sol / bsc / base / eth / robinhood",
"owner": {
"name": "GMGN"
},
@@ -9,7 +9,7 @@
{
"name": "gmgn-cli",
"source": "./",
"description": "4 GMGN skills — token info & security, K-line market data, wallet portfolio analysis, and DEX swap execution",
"description": "8 GMGN skills — token info & security, holder chip analysis, K-line market data & trending tokens, wallet portfolio analysis, wallet copy-trade scoring, follow/KOL/Smart Money tracking, DEX swap execution, and launchpad token creation",
"version": "1.0.0",
"author": {
"name": "GMGN"
@@ -27,7 +27,9 @@
"portfolio",
"market",
"blockchain",
"skills"
"skills",
"copytrade",
"launchpad"
],
"category": "web3",
"tags": [
@@ -37,7 +39,9 @@
"portfolio",
"swap",
"defi",
"blockchain"
"blockchain",
"copytrade",
"launchpad"
],
"strict": false
}
+2 -2
View File
@@ -1,13 +1,13 @@
{
"name": "gmgn-cli",
"version": "1.0.0",
"description": "GMGN OpenAPI skills — token info, market data, wallet portfolio, and swap",
"description": "GMGN OpenAPI skills — token info, market data, wallet portfolio & copy-trade scoring, and swap",
"author": {
"name": "GMGN",
"url": "https://gmgn.ai"
},
"repository": "https://github.com/GMGNAI/gmgn-skills",
"license": "MIT",
"keywords": ["gmgn", "crypto", "solana", "token", "defi", "swap"],
"keywords": ["gmgn", "crypto", "solana", "token", "defi", "swap", "copytrade"],
"skills": "./skills/"
}
+4
View File
@@ -29,6 +29,7 @@ This is a **Claude Code plugin** — a collection of GMGN OpenAPI skills for on-
| `gmgn-token` | Token info, security, pool, holders, traders | User asks about a token's price, market cap, security risk, liquidity pool, top holders, or top traders; user wants to research a token before buying; user asks "is this token safe", "who holds this token", "what's the liquidity" |
| `gmgn-market` | K-line / candlestick market data + trending tokens + newly launched launchpad tokens | User asks for price history, chart data, OHLCV candles, trading volume over time; user wants to analyze price trends; user asks "show me the 1h chart", "what was the price last week", "give me kline data for this token"; user wants to discover hot or trending tokens; user asks "what tokens are trending", "show me top tokens by volume", "find hot tokens on SOL"; **user asks about newly launched tokens, fresh tokens, latest tokens on launchpads** — e.g. "show me new tokens on pump.fun", "what tokens just launched on SOL", "find newly created tokens", "latest tokens on letsbonk" → use `market trenches --type new_creation` |
| `gmgn-portfolio` | Wallet holdings, activity, trading stats, token balance | User asks about a wallet's holdings, P&L, transaction history, trading statistics, or token balance; user wants to analyze a wallet; user asks "what tokens does this wallet hold", "show me recent trades", "what's the win rate of this wallet" |
| `gmgn-wallet-score` | Wallet scoring across three angles — profitability (track-record score), copy-tradeability (score + latency/slippage/gas backtest), and Dev reputation for token-creator wallets — plus trading-style tags | User asks about a wallet's profitability ("钱包盈利能力怎么样", "is this wallet profitable"), copy-trade worthiness ("is this wallet worth copying", "跟单评分", "钱包评分", "值不值得跟单", "if I copy this wallet what's my real return"), or launch/Dev reputation ("钱包发盘情况怎么样", "是不是发币方钱包", "dev 信誉怎么样"); user gives a wallet address and wants any of these judgments |
| `gmgn-track` | Track trade activity of wallets I follow, KOL trades, Smart Money trades across chains | User asks about trades from wallets they follow; user wants to see what KOLs or Smart Money are buying/selling; user asks "show me what wallets I follow have traded recently", "what are KOLs buying", "show me smart money moves on BSC" |
| `gmgn-swap` | Token swap execution + order status query | User wants to swap tokens, execute a trade, or check an order status; user asks "swap SOL for USDC", "buy this token", "check my order"; **requires private key configured in `.env`** |
@@ -48,6 +49,9 @@ Match the user's request to the right skill and workflow:
| "wallets I follow", "my followed wallets traded" | `gmgn-track follow-wallet` |
| "analyze this wallet", "is this wallet worth following", wallet address provided | `gmgn-portfolio` → full workflow: `docs/workflow-wallet-analysis.md` |
| "wallet style", "smart money profile", "聪明钱画像", "这个钱包是长线还是短线", "跟着他买收益如何", "聪明钱排行榜" | `gmgn-portfolio` + `gmgn-track``docs/workflow-smart-money-profile.md` |
| "钱包盈利能力怎么样", "钱包战绩怎么样", "is this wallet profitable" | `gmgn-wallet-score` (profitability angle — track-record score) |
| "跟单评分", "钱包评分", "值不值得跟单", "is this wallet worth copying", "copy trade score", wallet address provided + copy-trade decision | `gmgn-wallet-score` (copy-tradeability angle — score + backtest) |
| "钱包发盘情况怎么样", "是不是发币方钱包", "dev 信誉怎么样", "is this a token-creator wallet" | `gmgn-wallet-score` (Dev-reputation angle) |
| "risk warning", "风险预警", "有没有巨鲸出货", "流动性正常吗", "这个项目还安全吗" | `gmgn-token` + `gmgn-track``docs/workflow-risk-warning.md` |
| "swap", "buy TOKEN", "sell TOKEN" | `gmgn-swap` — MUST run `gmgn-token security` on output token first |
| "chart", "price history", "kline", "OHLCV" | `gmgn-market kline` |
+4 -3
View File
@@ -96,6 +96,7 @@ No more scraping web pages or getting blocked by Cloudflare. Query all the profe
| [`/gmgn-token`](skills/gmgn-token/SKILL.md) | Token info, security, pool, holders, traders | [SKILL.md](skills/gmgn-token/SKILL.md) |
| [`/gmgn-market`](skills/gmgn-market/SKILL.md) | K-line market data, trending tokens | [SKILL.md](skills/gmgn-market/SKILL.md) |
| [`/gmgn-portfolio`](skills/gmgn-portfolio/SKILL.md) | Wallet holdings, activity, stats | [SKILL.md](skills/gmgn-portfolio/SKILL.md) |
| [`/gmgn-wallet-score`](skills/gmgn-wallet-score/SKILL.md) | Wallet copy-trade scoring — track-record, copy-tradeability, backtest, Dev reputation | [SKILL.md](skills/gmgn-wallet-score/SKILL.md) |
| [`/gmgn-track`](skills/gmgn-track/SKILL.md) | Follow-wallet trades, KOL trades, Smart Money trades | [SKILL.md](skills/gmgn-track/SKILL.md) |
| [`/gmgn-swap`](skills/gmgn-swap/SKILL.md) | Swap submission + limit orders + strategy orders + order query | [SKILL.md](skills/gmgn-swap/SKILL.md) |
| [`/gmgn-cooking`](skills/gmgn-cooking/SKILL.md) | One-command cooking orders (buy + take-profit/stop-loss in a single flow) | [SKILL.md](skills/gmgn-cooking/SKILL.md) |
@@ -350,7 +351,7 @@ Skills are automatically discovered via the `.cursor-plugin/` configuration.
```bash
echo "$(npm root -g)/gmgn-skills/skills"
```
3. Restart Cline — `/gmgn-token`, `/gmgn-market`, `/gmgn-portfolio`, `/gmgn-track`, `/gmgn-swap`, `/gmgn-cooking` will be available
3. Restart Cline — `/gmgn-token`, `/gmgn-market`, `/gmgn-portfolio`, `/gmgn-wallet-score`, `/gmgn-track`, `/gmgn-swap`, `/gmgn-cooking` will be available
#### Codex CLI
@@ -731,8 +732,8 @@ gmgn-cli cooking \
| token / market / portfolio / track | `sol` / `bsc` / `base` / `eth` / `robinhood` | — |
| swap / order | `sol` / `bsc` / `base` / `eth` / `robinhood` | sol: SOL, USDC · bsc: BNB, USDC · base: ETH, USDC · eth: ETH |
| gas-price | `sol` / `bsc` / `base` / `eth` / `robinhood` | — |
| track kol / track smartmoney · market signal | `sol` / `bsc` / `base` / `eth` (kol/smartmoney) · `sol` / `bsc` (signal) — **no `robinhood`** | — |
| cooking create | `sol` / `bsc` / `base` — **no `robinhood`** | — |
| track kol / track smartmoney · market signal | `sol` / `bsc` / `base` / `eth` / `robinhood` (kol/smartmoney) · `sol` / `bsc` / `robinhood` (signal) | — |
| cooking create | `sol` / `bsc` / `base` / `robinhood` | — |
---
+4 -3
View File
@@ -99,6 +99,7 @@ SOL / BSC / Base / ETH 多链数据每次查询均为实时,支持多参数个
| [`/gmgn-token`](skills/gmgn-token/SKILL.md) | Token 信息、安全、池子、持有者、交易者 | [SKILL.md](skills/gmgn-token/SKILL.md) |
| [`/gmgn-market`](skills/gmgn-market/SKILL.md) | K 线行情数据、热门代币 | [SKILL.md](skills/gmgn-market/SKILL.md) |
| [`/gmgn-portfolio`](skills/gmgn-portfolio/SKILL.md) | 钱包持仓、活动、统计 | [SKILL.md](skills/gmgn-portfolio/SKILL.md) |
| [`/gmgn-wallet-score`](skills/gmgn-wallet-score/SKILL.md) | 钱包跟单评分——真实战绩分、可跟单分、跟单回测、Dev 信誉分 | [SKILL.md](skills/gmgn-wallet-score/SKILL.md) |
| [`/gmgn-track`](skills/gmgn-track/SKILL.md) | 追踪关注钱包交易动态、KOL 交易动态、聪明钱交易动态 | [SKILL.md](skills/gmgn-track/SKILL.md) |
| [`/gmgn-swap`](skills/gmgn-swap/SKILL.md) | 兑换提交 + 限价单 + 策略单 + 订单查询 | [SKILL.md](skills/gmgn-swap/SKILL.md) |
| [`/gmgn-cooking`](skills/gmgn-cooking/SKILL.md) | 一键 Cooking 策略单(买入 + 止盈止损条件单一体化) | [SKILL.md](skills/gmgn-cooking/SKILL.md) |
@@ -354,7 +355,7 @@ cp .env.example .env
```bash
echo "$(npm root -g)/gmgn-skills/skills"
```
3. 重启 Cline — `/gmgn-token`、`/gmgn-market`、`/gmgn-portfolio`、`/gmgn-track`、`/gmgn-swap`、`/gmgn-cooking` 即可使用
3. 重启 Cline — `/gmgn-token`、`/gmgn-market`、`/gmgn-portfolio`、`/gmgn-wallet-score`、`/gmgn-track`、`/gmgn-swap`、`/gmgn-cooking` 即可使用
#### Codex CLI
@@ -755,8 +756,8 @@ gmgn-cli cooking \
| token / market / portfolio / track | `sol` / `bsc` / `base` / `eth` / `robinhood` | — |
| swap / order | `sol` / `bsc` / `base` / `eth` / `robinhood` | sol: SOL、USDC · bsc: BNB、USDC · base: ETH、USDC · eth: ETH |
| gas-price | `sol` / `bsc` / `base` / `eth` / `robinhood` | — |
| track kol / track smartmoney · market signal | `sol` / `bsc` / `base` / `eth`kol/smartmoney)· `sol` / `bsc`signal)— **不支持 `robinhood`** | — |
| cooking create | `sol` / `bsc` / `base` — **不支持 `robinhood`** | — |
| track kol / track smartmoney · market signal | `sol` / `bsc` / `base` / `eth` / `robinhood`kol/smartmoney)· `sol` / `bsc` / `robinhood`signal | — |
| cooking create | `sol` / `bsc` / `base` / `robinhood` | — |
---
+10 -7
View File
@@ -325,8 +325,8 @@ gmgn-cli market signal --chain sol --groups '<json_array>' [--raw]
| Option | Required | Description |
|--------|----------|-------------|
| `--chain` | Yes | `sol` / `bsc` |
| `--signal-type` | No | Signal type(s), repeatable (118, default: all). See Signal Types below. |
| `--chain` | Yes | `sol` / `bsc` / `robinhood` |
| `--signal-type` | No | Signal type(s), repeatable (121, default: all). See Signal Types below. |
| `--mc-min` | No | Min market cap at trigger time (USD) |
| `--mc-max` | No | Max market cap at trigger time (USD) |
| `--trigger-mc-min` | No | Min market cap at signal trigger moment (USD) |
@@ -359,6 +359,9 @@ gmgn-cli market signal --chain sol --groups '<json_array>' [--raw]
| 16 | SignalTypeMultiLargeBuy | Multiple large buys |
| 17 | SignalTypeBagsClaims | Bags Claim |
| 18 | SignalTypePumpClaims | Pump Claim |
| 19 | SignalTypePlatformCallV2 | Platform call (V2) |
| 20 | SignalTypeKOLBuy | KOL buy |
| 21 | SignalTypeBankerClaims | Banker Claim (Base chain Banker platform claim fee) |
---
@@ -476,7 +479,7 @@ gmgn-cli track kol [--chain <chain>] [--limit <n>] [--side <side>] [--raw]
| Option | Required | Description |
|--------|----------|-------------|
| `--chain` | No | `sol` / `bsc` / `base` (default `sol`) |
| `--chain` | No | `sol` / `bsc` / `base` / `eth` / `robinhood` (default `sol`) |
| `--limit` | No | Page size (1200, default 100) |
| `--side` | No | Filter by trade direction: `buy` / `sell` (client-side filter) |
@@ -492,7 +495,7 @@ gmgn-cli track smartmoney [--chain <chain>] [--limit <n>] [--side <side>] [--raw
| Option | Required | Description |
|--------|----------|-------------|
| `--chain` | No | `sol` / `bsc` / `base` (default `sol`) |
| `--chain` | No | `sol` / `bsc` / `base` / `eth` / `robinhood` (default `sol`) |
| `--limit` | No | Page size (1200, default 100) |
| `--side` | No | Filter by trade direction: `buy` / `sell` (client-side filter) |
@@ -845,8 +848,8 @@ gmgn-cli cooking create \
| Option | Required | Description |
|--------|----------|-------------|
| `--chain` | Yes | `sol` / `bsc` / `base` |
| `--dex` | Yes | Launchpad per chain: `pump` / `bonk` / `bags` (sol), `fourmeme` / `flap` (bsc), `klik` / `clanker` (base) |
| `--chain` | Yes | `sol` / `bsc` / `base` / `robinhood` |
| `--dex` | Yes | Launchpad per chain: `pump` / `bonk` / `bags` (sol), `fourmeme` / `flap` (bsc), `klik` / `clanker` (base), `trench` / `pons` (robinhood) |
| `--from` | Yes | Wallet address (must match API Key binding) |
| `--name` | Yes | Token name |
| `--symbol` | Yes | Token symbol |
@@ -866,7 +869,7 @@ gmgn-cli cooking create \
| `--max-priority-fee-per-gas` | No | Max priority fee per gas in wei (**EVM only**) |
| `--anti-mev` | No | Enable anti-MEV protection (**SOL only**) |
| `--anti-mev-mode` | No | Anti-MEV mode: `off` / `normal` / `secure` (**SOL only**) |
| `--raised-token` | No | Raise token symbol: `pump``USDC`; `bonk``USD1`; `fourmeme``USDT`/`USD1`; omit for native |
| `--raised-token` | No | Raise token symbol: `pump``USDC`; `bonk``USD1`; `fourmeme``USDT`/`USD1`; `base`/`robinhood`→native only; omit for native |
| `--dev-wallet-bps` | No | Dev wallet fee share in basis points (100 = 1%) |
| `--dev-gas` | No | Dev gas amount |
| `--dev-priority` | No | Dev priority fee |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "gmgn-cli",
"version": "1.5.2",
"version": "1.5.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gmgn-cli",
"version": "1.5.2",
"version": "1.5.4",
"license": "MIT",
"dependencies": {
"commander": "^12.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gmgn-cli",
"version": "1.5.2",
"version": "1.5.4",
"description": "GMGN OpenAPI CLI — call GMGN market, token, portfolio and swap APIs from the command line",
"type": "module",
"bin": {
+7 -6
View File
@@ -60,15 +60,16 @@ This is a hard, code-level barrier — do not attempt to work around it. If a to
## Supported Chains
`sol` / `bsc` / `base`
`sol` / `bsc` / `base` / `robinhood`
## Supported Launchpads by Chain
| Chain | `--dex` values | Raise token (`--raised-token`) |
| ------ | ---------------------- | ------------------------------ |
| `sol` | `pump`, `bonk`, `bags` | `pump`: `""` (SOL) or `USDC`; `bonk`: `""` (SOL) or `USD1`; `bags`: `""` (SOL only) |
| `bsc` | `fourmeme`, `flap` | `fourmeme`: `""` (BNB), `USD1`, `USDT`; `flap`: `""` (BNB only) |
| `base` | `klik`, `clanker` | `""` only (quote token fixed to WETH) |
| Chain | `--dex` values | Raise token (`--raised-token`) |
| ----------- | ---------------------- | ------------------------------ |
| `sol` | `pump`, `bonk`, `bags` | `pump`: `""` (SOL) or `USDC`; `bonk`: `""` (SOL) or `USD1`; `bags`: `""` (SOL only) |
| `bsc` | `fourmeme`, `flap` | `fourmeme`: `""` (BNB), `USD1`, `USDT`; `flap`: `""` (BNB only) |
| `base` | `klik`, `clanker` | `""` only (quote token fixed to WETH) |
| `robinhood` | `trench`, `pons` | `""` only (native token) |
When the user names a platform colloquially (e.g. "pump.fun", "four.meme"), map it to the correct `--dex` identifier from this table before running the command.
+808
View File
@@ -0,0 +1,808 @@
---
name: gmgn-holder-analysis
description: Token holder chip analysis — deep analysis of holder structure including chip distribution, entry cost, whale/dev/KOL behavior, risk wallets (rat traders, bundlers, snipers), related wallets, smart money signals, and an AI rating based purely on token structure. Use when user asks about holder analysis, 筹码分析, 持仓分析, chip structure, who is holding, or whether a token is safe to buy based on its holder composition.
argument-hint: "--chain <sol|bsc|base|eth|robinhood> --address <token_address>"
metadata:
cliHelp: "gmgn-cli token holders --help && gmgn-cli portfolio created-tokens --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, run `gmgn-cli config` and show output, then apply the key with `gmgn-cli config --apply <KEY>`. If unknown option, tell user to run `npm install -g gmgn-cli`.**
**IMPORTANT: Always use `gmgn-cli` commands. Do NOT use curl, WebFetch, or visit gmgn.ai.**
When the user asks to analyze holders for a token, extract `--chain` and `--address` from their message, then run the analysis script below. Also detect the user's language: set `LANG` to `'zh'` if the user wrote in Chinese, `'en'` if in English (default `'zh'`).
## Analysis Script
Run the following command, replacing the placeholders with the actual values:
```bash
python3 ~/.claude/skills/gmgn-holder-analysis/analyze.py <FILL_IN_TOKEN_ADDRESS> <FILL_IN_CHAIN> <FILL_IN_LANG>
```
- FILL_IN_CHAIN: `sol` for Solana addresses; for EVM `0x...` addresses use `auto` unless the user explicitly specifies a chain (`bsc`/`eth`/`base`)
- FILL_IN_LANG: `zh` if user wrote Chinese, `en` if English, default `zh`
## Output Rule
After the script finishes, paste the complete stdout verbatim into your reply — every line, every section, nothing omitted or summarized. Do NOT add any introduction, commentary, or summary before or after the output block.
<!-- legacy inline script kept below for reference — DO NOT run this block -->
<!--
```python
python3 << 'PYEOF'
import json, subprocess, time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
TOKEN_ADDR = "<FILL_IN_TOKEN_ADDRESS>"
CHAIN = "<FILL_IN_CHAIN>"
LANG = "<FILL_IN_LANG>" # 'zh' or 'en'
WINDOW = 1800
now_ts = int(time.time())
ZH = (LANG == 'zh')
def _(zh, en): return zh if ZH else en
def run_cli(args, timeout=30):
r = subprocess.run(['gmgn-cli'] + args + ['--raw'],
capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise RuntimeError(r.stderr)
return json.loads(r.stdout)
# Fetch holders and devs in parallel, then created-tokens after extracting creator
with ThreadPoolExecutor(max_workers=2) as ex:
f_holders = ex.submit(run_cli, ['token', 'holders', '--chain', CHAIN, '--address', TOKEN_ADDR, '--limit', '100'])
f_devs = ex.submit(run_cli, ['token', 'holders', '--chain', CHAIN, '--address', TOKEN_ADDR, '--tag', 'dev', '--limit', '20'])
holders = f_holders.result()['list']
devs = f_devs.result()['list']
_creator_tmp = next((d for d in devs if 'creator' in (d.get('maker_token_tags') or [])), None)
created_data = None
if _creator_tmp:
try:
created_data = run_cli(['portfolio', 'created-tokens', '--chain', CHAIN,
'--wallet', _creator_tmp['address'],
'--order-by', 'market_cap', '--direction', 'desc'])
except: pass
# ── Wallet classification ────────────────────────────────
# addr_type: 0=normal, 1=burn, 2=DEX/pool
normal = [h for h in holders if h.get('addr_type', 0) == 0]
burn = [h for h in holders if h.get('addr_type', 0) == 1]
dex = [h for h in holders if h.get('addr_type', 0) == 2]
# ── Helpers ──────────────────────────────────────────────
def pct(v): return v * 100
def usd(v):
if v is None: return "$0"
if abs(v) >= 1_000_000: return f"${v/1_000_000:.2f}M"
if abs(v) >= 1_000: return f"${v/1_000:.1f}K"
return f"${v:.0f}"
def fmt_amt(v):
if v >= 1_000_000: return f"{v/1_000_000:.1f}M"
if v >= 1_000: return f"{v/1_000:.0f}K"
return f"{v:.0f}"
def age_label(entry_ts):
secs = now_ts - entry_ts
days = secs // 86400
hours = secs // 3600
if ZH: return f"{hours}小时前入场" if days == 0 else f"{days}天前入场"
else: return f"{hours}h ago" if days == 0 else f"{days}d ago"
def addr_short(addr):
return f"{addr[:4]}...{addr[-4:]}"
# ── Price / MC ───────────────────────────────────────────
supply_list = [h['balance']/h['amount_percentage'] for h in normal
if h.get('amount_percentage',0)>0 and h.get('balance',0)>0]
total_supply = sorted(supply_list)[len(supply_list)//2] if supply_list else 1_000_000_000
price_list = [h['usd_value']/h['balance'] for h in normal
if h.get('balance',0)>0 and h.get('usd_value',0)>0]
cur_price = sorted(price_list)[len(price_list)//2] if price_list else 0
cur_mc = total_supply * cur_price
burn_pct = sum(h['amount_percentage'] for h in burn)
dex_pct = sum(h['amount_percentage'] for h in dex)
top10 = sum(h['amount_percentage'] for h in holders[:10])
top20 = sum(h['amount_percentage'] for h in holders[:20])
# ── Risk wallets ─────────────────────────────────────────
# maker_token_tags: bundler, rat_trader, sniper, whale, top_holder, transfer_in, dev_team, creator
# tags: smart_degen, pump_smart, renowned, fresh_wallet, wash_trader, fomo, kol
airdrop = [h for h in normal if h.get('buy_tx_count_cur', 0)==0 and h.get('balance', 0)>0]
bundlers = [h for h in normal if 'bundler' in (h.get('maker_token_tags') or [])]
rats = [h for h in normal if 'rat_trader' in (h.get('maker_token_tags') or [])]
snipers = [h for h in normal if 'sniper' in (h.get('maker_token_tags') or [])]
fresh = [h for h in normal if 'fresh_wallet' in (h.get('tags') or [])]
wash = [h for h in normal if 'wash_trader' in (h.get('tags') or [])]
risk_all = set(h['address'] for g in [bundlers, rats, snipers, fresh, wash] for h in g)
risk_pct = sum(h['amount_percentage'] for h in normal if h['address'] in risk_all)
airdrop_pct = sum(h['amount_percentage'] for h in airdrop)
rats_pct = sum(h['amount_percentage'] for h in rats)
# ── Healthy chip ratio ───────────────────────────────────
normal_pct = sum(h['amount_percentage'] for h in normal)
all_bad = set(h['address'] for h in airdrop) | risk_all
bad_pct = sum(h['amount_percentage'] for h in normal if h['address'] in all_bad)
healthy_pct = max(normal_pct - bad_pct, 0)
healthy_ratio = (healthy_pct / normal_pct) if normal_pct > 0 else 0
# ── Related wallets ──────────────────────────────────────
from_map = defaultdict(list)
for h in normal:
fa = (h.get('native_transfer') or {}).get('from_address', '')
if fa: from_map[fa].append(h)
same_src_groups = sorted([(fa, ws) for fa, ws in from_map.items() if len(ws)>=2], key=lambda x: -len(x[1]))
same_src_wallets = sum(len(ws) for __,ws in same_src_groups)
same_src_pct = sum(h['amount_percentage'] for __,ws in same_src_groups for h in ws)
funded = [((h.get('native_transfer') or {}).get('timestamp',0), h)
for h in normal if (h.get('native_transfer') or {}).get('timestamp',0)]
bucket = defaultdict(list)
for ts, h in funded:
bucket[(ts//WINDOW)*WINDOW].append(h)
win_groups = sorted([(k,v) for k,v in bucket.items() if len(v)>=2], key=lambda x: -len(x[1]))
win_pct = sum(h['amount_percentage'] for __,v in win_groups for h in v)
related = set()
for __,ws in same_src_groups:
for h in ws: related.add(h['address'])
for __,v in win_groups:
for h in v: related.add(h['address'])
related_pct = sum(h['amount_percentage'] for h in normal if h['address'] in related)
related_usd = sum(h.get('usd_value',0) for h in normal if h['address'] in related)
# ── Quality signals ──────────────────────────────────────
smart = [h for h in normal if any(t in (h.get('tags') or []) for t in ['smart_degen','pump_smart'])]
kol = [h for h in normal if 'kol' in (h.get('tags') or []) or 'renowned' in (h.get('tags') or [])]
whales = [h for h in normal if 'whale' in (h.get('maker_token_tags') or [])]
diamond = [h for h in normal if h.get('sell_tx_count_cur',0)==0 and h.get('balance',0)>0]
partial = [h for h in normal if 0<(h.get('sell_amount_percentage') or 0)<0.5]
heavy_sell = [h for h in normal if (h.get('sell_amount_percentage') or 0)>=0.5]
smart_pct = sum(h['amount_percentage'] for h in smart)
kol_pct = sum(h['amount_percentage'] for h in kol)
whale_pct = sum(h['amount_percentage'] for h in whales)
diamond_pct = sum(h['amount_percentage'] for h in diamond)
# ── Dev data ─────────────────────────────────────────────
top100_map = {h['address']: h for h in holders}
creator = next((d for d in devs if 'creator' in (d.get('maker_token_tags') or [])), None)
sub_devs = [d for d in devs if 'creator' not in (d.get('maker_token_tags') or [])]
dev_realized = sum(d.get('realized_profit') or 0 for d in devs)
dev_holding = [d for d in devs if (d.get('balance') or 0)>=1]
# ── Holding metrics ──────────────────────────────────────
valid_starts = [h['start_holding_at'] for h in holders if (h.get('start_holding_at') or 0)>0]
token_launch = min(valid_starts) if valid_starts else now_ts
durations = [now_ts-h['start_holding_at'] for h in normal
if (h.get('start_holding_at') or 0)>0 and now_ts>h['start_holding_at']]
avg_hold_days = (sum(durations)/len(durations)/86400) if durations else 0
profit_w = [h for h in normal if (h.get('profit') or 0)>0]
loss_w = [h for h in normal if (h.get('profit') or 0)<0]
trapped = [h for h in normal if (h.get('unrealized_pnl') or 0)<-0.2 and h.get('balance',0)>0]
# ── Buying power ─────────────────────────────────────────
# SOL: native_balance in lamports (1e9); EVM: in wei (1e18)
NATIVE_PRICE = 160 if CHAIN == 'sol' else (700 if CHAIN == 'bsc' else 2500)
NATIVE_DENOM = 1e9 if CHAIN == 'sol' else 1e18
def native_usd(h): return int(h.get('native_balance') or 0)/NATIVE_DENOM*NATIVE_PRICE
zero_wallets = [h for h in normal if native_usd(h)==0]
low_wallets = [h for h in normal if 0 < native_usd(h) <= 200]
mid_wallets = [h for h in normal if 200 < native_usd(h) <= 1200]
high_wallets = [h for h in normal if native_usd(h) > 1200]
zero_pct_val = sum(h['amount_percentage'] for h in zero_wallets)
low_pct_val = sum(h['amount_percentage'] for h in low_wallets)
mid_pct_val = sum(h['amount_percentage'] for h in mid_wallets)
high_pct_val = sum(h['amount_percentage'] for h in high_wallets)
high_total = sum(native_usd(h) for h in high_wallets)
total_buying_power = sum(native_usd(h) for h in normal)
# ── Wallet roles / behaviors ─────────────────────────────
ROLE_MAP = {
'rat_trader': _('老鼠仓', 'Rat Trader'),
'sniper': _('狙击', 'Sniper'),
'bundler': _('捆绑', 'Bundler'),
'whale': _('鲸鱼', 'Whale'),
'smart_degen': _('聪明钱', 'Smart'),
'pump_smart': _('聪明钱', 'Smart'),
'renowned': 'KOL',
'kol': 'KOL',
'fresh_wallet': _('新钱包', 'Fresh'),
'wash_trader': _('刷量', 'Wash'),
'creator': 'Dev',
'dev_team': 'Dev',
}
def wallet_roles(h):
roles = []
for t in (h.get('maker_token_tags') or [])+(h.get('tags') or []):
if t in ROLE_MAP: roles.append(ROLE_MAP[t])
return list(dict.fromkeys(roles))
def holding_status(h):
sp = h.get('sell_amount_percentage', 0) or 0
bal = h.get('balance', 0) or 0
if bal <= 0: return _("已清仓", "Cleared")
if sp >= 0.8: return _("🔴 大量出货", "🔴 Heavy Selling")
if sp >= 0.3: return _("🟡 出货中", "🟡 Selling")
if sp > 0: return _("少量出货", "Light Selling")
return _("持仓未动", "Holding")
def wallet_behavior(h):
buy_tx = h.get('buy_tx_count_cur', 0) or 0
sell_tx = h.get('sell_tx_count_cur', 0) or 0
if buy_tx==0 and sell_tx==0: return _("几乎无链上活动", "Almost no on-chain activity")
if buy_tx>0 and sell_tx==0: return _("持续买入,尚未卖出", "Buying only, not sold yet")
if sell_tx>0 and buy_tx==0: return _("只卖不买", "Selling only")
return ""
def trend_str(wlist):
buying = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)==0]
selling = [h for h in wlist if (h.get('sell_tx_count_cur') or 0)>0 and (h.get('buy_tx_count_cur') or 0)==0]
both = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)>0]
holding = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)==0 and (h.get('sell_tx_count_cur') or 0)==0]
parts = []
if buying: parts.append(f"📈 {_('买入中', 'Buying')} {len(buying)}")
if selling: parts.append(f"📉 {_('卖出中', 'Selling')} {len(selling)}")
if both: parts.append(f"🔄 {_('买卖都有', 'Both')} {len(both)}")
if holding: parts.append(f"🤝 {_('未动', 'Holding')} {len(holding)}")
return " ".join(parts) if parts else "—"
def is_active(h): return (h.get('buy_tx_count_cur') or 0)+(h.get('sell_tx_count_cur') or 0)>0
def is_selling(h): return (h.get('sell_tx_count_cur') or 0)>0 and (h.get('balance') or 0)>=1
def is_buying_only(h): return (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)==0
# ── Rating ───────────────────────────────────────────────
biggest = max(normal, key=lambda h: h['amount_percentage']) if normal else None
dangers = []
if rats and rats_pct > 0.1:
dangers.append(_( f"老鼠仓持仓 {pct(rats_pct):.1f}%,出货即砸盘",
f"Rat traders hold {pct(rats_pct):.1f}% — instant dump risk"))
if biggest and biggest['amount_percentage'] > 0.15:
dangers.append(_( f"最大单钱包持仓 {pct(biggest['amount_percentage']):.1f}%,筹码极度集中",
f"Largest wallet holds {pct(biggest['amount_percentage']):.1f}% — extreme concentration"))
if creator:
to_out = creator.get('token_transfer_out') or {}
if (to_out.get('address') or '') in top100_map:
dangers.append(_("Dev 筹码转给内部马甲,换手控盘",
"Dev transferred chips to internal wallet — covert control"))
warns = []
if dev_holding:
hold_pct_val = sum(d.get('amount_percentage',0) for d in dev_holding)
if hold_pct_val > 0.01:
warns.append(_( f"Dev 仍持仓 {pct(hold_pct_val):.2f}%",
f"Dev still holds {pct(hold_pct_val):.2f}%"))
if airdrop_pct > 0.15:
warns.append(_( f"空降筹码 {pct(airdrop_pct):.1f}%,来源不透明",
f"Airdrop supply {pct(airdrop_pct):.1f}% — opaque origin"))
if risk_pct > 0.3:
warns.append(_( f"风险钱包持仓 {pct(risk_pct):.1f}%,筹码质量差",
f"Risk wallets hold {pct(risk_pct):.1f}% — low chip quality"))
if related_pct > 0.1:
warns.append(_( f"关联钱包 {len(related)} 个持仓 {pct(related_pct):.1f}%",
f"Linked wallets ({len(related)}) hold {pct(related_pct):.1f}%"))
if dangers:
rating_em, rating_text = "🔴", _("不建议买", "Not Recommended")
elif len(warns) >= 2:
rating_em, rating_text = "⚠️", _("谨慎参与", "Caution")
elif len(warns) == 1:
rating_em, rating_text = "🟡", _("可轻仓", "Light Position")
else:
rating_em, rating_text = "✅", _("正常参与", "Normal")
goods = []
if burn_pct > 0.05:
goods.append(_( f"销毁 {pct(burn_pct):.1f}% 永久锁仓,流通减少",
f"Burned {pct(burn_pct):.1f}% permanently — reduced supply"))
if whales:
buying_w = [h for h in whales if is_buying_only(h)]
if buying_w:
goods.append(_( f"鲸鱼 {len(buying_w)} 个持续买入,尚未出货",
f"{len(buying_w)} whale(s) still accumulating, not sold"))
if kol:
goods.append(_( f"KOL {len(kol)} 个在场({pct(kol_pct):.2f}%",
f"{len(kol)} KOL(s) holding ({pct(kol_pct):.2f}%)"))
if diamond_pct > 0.4:
goods.append(_( f"钻石手持仓 {pct(diamond_pct):.1f}%,筹码稳定",
f"Diamond hands hold {pct(diamond_pct):.1f}% — stable chips"))
exit_signals = []
if rats:
exit_signals.append(_("老鼠仓钱包出现卖出操作", "Rat trader wallets start selling"))
if dev_holding:
exit_signals.append(_("Dev 钱包开始出货", "Dev wallets start dumping"))
if airdrop_pct>0.15:
exit_signals.append(_("空降大户出现集中卖出", "Airdrop whales start concentrated selling"))
if not exit_signals:
exit_signals = [_("Top5 大户出现集中出货", "Top 5 holders start concentrated selling"),
_("价格跌破建仓均价支撑", "Price breaks below average entry cost")]
exit_signals = exit_signals[:3]
# ── Top5 pressure analysis ───────────────────────────────
top5_holders = sorted(normal, key=lambda h: -h['amount_percentage'])[:5]
def top5_pressure(h):
avg_cost = h.get('avg_cost') or 0
up_pnl = h.get('unrealized_pnl') or 0
up_usd = h.get('unrealized_profit') or 0
buy0 = h.get('buy_tx_count_cur',0)==0
roles = wallet_roles(h)
if buy0 and not roles: roles.append(_('空降', 'Airdrop'))
role_str = "["+"·".join(roles)+"] " if roles else ""
display_id = (h.get('twitter_name') or '') or addr_short(h['address'])
if buy0 or avg_cost==0:
cost_str = _("零成本(转账获得)", "Zero cost (received via transfer)")
pnl_str = "—"
lv = "⚠️ " + _("高", "High")
note = _("零成本,随时可出货", "Zero cost — can dump anytime")
elif up_pnl>1.0:
mult = up_pnl+1
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"+{up_pnl*100:.0f}% ({mult:.1f}x) {usd(up_usd)}"
lv = "⚠️ " + _("高", "High")
note = _(f"建仓MC {usd(total_supply*avg_cost)} → 现 {usd(cur_mc)},浮盈 {mult:.1f}x 压力强",
f"Entry MC {usd(total_supply*avg_cost)} → now {usd(cur_mc)}, {mult:.1f}x gain — strong pressure")
elif up_pnl>0.1:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"+{up_pnl*100:.0f}% {usd(up_usd)}"
lv = "🟡 " + _("中", "Med")
note = _("小幅浮盈,出货意愿一般", "Moderate gain — mild sell pressure")
elif up_pnl>=-0.1:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"{up_pnl*100:+.0f}% {usd(up_usd)}" if abs(up_usd)>=1 else _("接近成本", "Near cost")
lv = "🟢 " + _("低", "Low")
note = _("接近成本,短期抛压有限", "Near break-even — limited short-term pressure")
else:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"{up_pnl*100:.0f}% {usd(up_usd)}"
lv = "🟢 " + _("低", "Low")
note = _("套牢中,短期不易割肉", "Underwater — unlikely to sell soon")
beh = wallet_behavior(h)
beh_str = (" " + _("行为", "behavior") + ": " + beh) if beh else ""
return role_str, display_id, cost_str, pnl_str, lv, note, beh_str, holding_status(h)
# ══════════════════════════════════════════════════════════
# OUTPUT
# ══════════════════════════════════════════════════════════
title = _("Holder 筹码分析", "Holder Chip Analysis")
print(f"┌{'─'*56}┐")
print(f"│{(' '+title):^56}│")
print(f"│{(' '+TOKEN_ADDR[:10]+'...'+TOKEN_ADDR[-4:]+' · Top100 · '+CHAIN.upper()):^56}│")
print(f"│{(' MC '+usd(cur_mc)):^56}│")
print(f"└{'─'*56}┘")
print()
# ── Dump Risk ──
sec1 = _("🚨 砸盘风险", "🚨 Dump Risk")
print(f"━━ {sec1} {'━'*(54-len(sec1))}")
print()
c10f = "🔴" if top10>0.5 else ("🟡" if top10>0.3 else "🟢")
c20f = "🔴" if top20>0.6 else ("🟡" if top20>0.4 else "🟢")
print(f" {_('集中度', 'Concentration')} Top10 {pct(top10):.1f}% {c10f} Top20 {pct(top20):.1f}% {c20f}")
print()
if burn:
print(f" 🔥 {_('销毁地址', 'Burn addr')} {pct(burn_pct):.2f}% ✅ {_('永久锁仓,无法流通', 'Permanently locked, non-circulating')}")
print()
airf = "🔴" if airdrop_pct>0.2 else ("🟡" if airdrop_pct>0.1 else "🟢")
print(f" {_('空降筹码(从未买入、靠转账获得)', 'Airdrop (never bought, received via transfer)')} {len(airdrop)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(airdrop_pct):.2f}% {airf}")
print()
riskf = "🔴" if risk_pct>0.3 else ("🟡" if risk_pct>0.1 else "🟢")
print(f" {_('风险钱包', 'Risk wallets')} {_('合计', 'total')} {len(risk_all)} {_('个', '')} {_('持仓', 'hold')} {pct(risk_pct):.2f}% {riskf}")
risk_labels = [
(_("老鼠仓", "Rat Trader"), rats, "🚨"),
(_("捆绑交易", "Bundler"), bundlers, "⚠️"),
(_("狙击者", "Sniper"), snipers, "⚠️"),
(_("新钱包", "Fresh"), fresh, ""),
(_("刷量", "Wash"), wash, ""),
]
for label, group, flag in risk_labels:
if group:
gp = pct(sum(h['amount_percentage'] for h in group))
print(f" · {label:10s} {len(group):2d} {_('个', '')} {_('持仓', 'hold')} {gp:.2f}% {flag}")
if not any([rats,bundlers,snipers,fresh,wash]):
print(f" · {_('未发现风险标签钱包', 'No risk-tagged wallets found')} 🟢")
print()
bundler_pct_val = sum(h['amount_percentage'] for h in bundlers)
sniper_pct_val = sum(h['amount_percentage'] for h in snipers)
if dangers:
sdp_summary = f"🔴 {dangers[0]}"
elif rats_pct > 0.05:
sdp_summary = _( f"🔴 老鼠仓持仓 {pct(rats_pct):.1f}%,零成本拿的,随时可以无损砸盘",
f"🔴 Rat traders hold {pct(rats_pct):.1f}% at zero cost — can dump with no loss anytime")
elif top10 > 0.4:
sdp_summary = _( f"🟡 Top10 持仓 {pct(top10):.1f}%,筹码过于集中,大户一旦出货冲击很大",
f"🟡 Top10 hold {pct(top10):.1f}% — highly concentrated, big impact if they sell")
elif bundler_pct_val > 0.2:
sdp_summary = _( f"🟡 捆绑钱包持仓 {pct(bundler_pct_val):.1f}%,这批是开盘机器人扫货,出货时可能集中砸盘",
f"🟡 Bundlers hold {pct(bundler_pct_val):.1f}% — bot-swept at open, may dump together")
elif airdrop_pct > 0.15:
sdp_summary = _( f"🟡 空降筹码 {pct(airdrop_pct):.1f}%,这些人零成本拿到币,随时可能出货",
f"🟡 Airdrop supply {pct(airdrop_pct):.1f}% at zero cost — may sell anytime")
elif sniper_pct_val > 0.1:
sdp_summary = _( f"🟡 狙击者持仓 {pct(sniper_pct_val):.1f}%,开盘低价进的,浮盈高、随时可套现",
f"🟡 Snipers hold {pct(sniper_pct_val):.1f}% at launch price — high unrealized gain, may cash out")
elif risk_pct > 0.1:
sdp_summary = _( f"🟡 风险钱包合计持仓 {pct(risk_pct):.1f}%,需要留意动向",
f"🟡 Risk wallets total {pct(risk_pct):.1f}% — watch their moves")
elif burn_pct > 0.1:
sdp_summary = _( f"🟢 销毁了 {pct(burn_pct):.1f}%,流通筹码少,LP 也锁住了,相对干净",
f"🟢 {pct(burn_pct):.1f}% burned — reduced supply, LP locked, relatively clean")
else:
sdp_summary = _("🟢 集中度正常,没发现明显的砸盘风险",
"🟢 Normal concentration, no obvious dump risk detected")
print(f" → {_('小结', 'Summary')}{_('', ': ')}{sdp_summary}")
print()
print(f" {_('Top5 持仓钱包抛压分析', 'Top 5 Holder Sell Pressure')}")
print(f" {_('浮盈越高 / 建仓MC越低 → 获利了结压力越强', 'Higher unrealized gain / lower entry MC → stronger sell pressure')}")
print()
for i, h in enumerate(top5_holders, 1):
role_str, display_id, cost_str, pnl_str, lv, note, beh_str, st = top5_pressure(h)
hp = pct(h['amount_percentage'])
print(f" {i}. {role_str}{display_id}")
print(f" {_('持仓', 'hold')} {hp:.2f}% {cost_str} {_('盈亏', 'pnl')} {pnl_str}")
print(f" {_('抛压', 'pressure')} {lv} — {note}")
print(f" {_('状态', 'status')} {st}{beh_str}")
print()
# ── Dev Wallets ──
sec2 = _("👨‍💻 Dev 钱包", "👨‍💻 Dev Wallets")
print(f"━━ {sec2} {'━'*(54-len(sec2))}")
print()
dev_status = (_("✅ 全部余额归零", "✅ All cleared") if not dev_holding
else _(f"⚠️ 仍有 {len(dev_holding)} 个持仓中", f"⚠️ {len(dev_holding)} still holding"))
if sub_devs:
print(f" {_('共', 'Total')} {len(devs)} {_('个钱包(1 主号 + ', 'wallets (1 main + ')}{len(sub_devs)}{_(' 小号)', ' sub)')} {dev_status}")
else:
print(f" {_('共', 'Total')} {len(devs)} {_('个钱包', 'wallets')} {dev_status}")
print(f" {_('Dev 合计已实现利润', 'Dev total realized profit')} {usd(dev_realized)}")
print()
if creator:
c_sell_v = creator.get('sell_volume_cur') or 0
tf_out = creator.get('history_transfer_out_amount') or 0
tf_val = creator.get('history_transfer_out_income') or 0
sell_amt = creator.get('sell_amount_cur') or 0
hold_pct_c = creator.get('amount_percentage') or 0
c_status = (_("余额归零", "Balance zero") if (creator.get('balance') or 0)<1
else _(f"⚠️ 持仓 {pct(hold_pct_c):.2f}%", f"⚠️ Holding {pct(hold_pct_c):.2f}%"))
print(f" {_('主号 (Creator)', 'Main (Creator)')} {addr_short(creator['address'])}")
parts = [c_status]
if sell_amt>0:
parts.append(_(f"卖出 {fmt_amt(sell_amt)} 个({usd(c_sell_v)}",
f"Sold {fmt_amt(sell_amt)} ({usd(c_sell_v)})"))
if tf_out>0:
to_out = creator.get('token_transfer_out') or {}
to_addr = to_out.get('address') or ''
if to_addr and to_addr in top100_map:
parts.append(_(f"转出 {fmt_amt(tf_out)} 个至 Top100 内部钱包(估值 {usd(tf_val)}",
f"Transferred {fmt_amt(tf_out)} to Top100 internal wallet (est. {usd(tf_val)})"))
else:
parts.append(_(f"转出 {fmt_amt(tf_out)} 个至外部地址(估值 {usd(tf_val)}",
f"Transferred {fmt_amt(tf_out)} to external addr (est. {usd(tf_val)})"))
print(f" {' '.join(parts)}")
to_out = creator.get('token_transfer_out') or {}
to_addr = to_out.get('address') or ''
if to_addr and to_addr in top100_map:
target = top100_map[to_addr]
t_mtags = [t for t in (target.get('maker_token_tags') or []) if t not in ('top_holder','transfer_in')]
print(f"\n ⚠️ {_('转出筹码仍在 Top100(换马甲继续持有):', 'Transferred chips still in Top100 (sock puppet):')}")
print(f" {addr_short(to_addr)} {_('持仓', 'hold')} {pct(target.get('amount_percentage',0)):.2f}% {_('标签', 'tags')}: {' '.join(t_mtags) or _('无','none')}")
elif (creator.get('balance') or 0)<1 and tf_out==0:
print(f" ✅ {_('已完全卖出,无异常转账记录', 'Fully sold, no abnormal transfers')}")
print()
if to_addr and to_addr in top100_map:
dev_summary = _( "🔴 Dev 换马甲持仓,这个很危险,随时可以砸盘",
"🔴 Dev using sock puppet — very dangerous, can dump anytime")
elif dev_holding:
dev_summary = _( "🟡 Dev 还没出完,有出货风险,关注钱包动向",
"🟡 Dev hasn't fully exited — dump risk, watch wallet activity")
elif dev_realized > 50000:
dev_summary = _( f"🟡 Dev 已套现 {usd(dev_realized)},虽然出完了但赚了不少",
f"🟡 Dev cashed out {usd(dev_realized)} — exited but made significant profit")
else:
dev_summary = _( "🟢 Dev 已清仓,没有持仓压力",
"🟢 Dev fully exited — no holding pressure")
print(f" → {_('小结', 'Summary')}{_('', ': ')}{dev_summary}")
print()
if created_data:
all_tokens = created_data.get('tokens') or []
total_cnt = (created_data.get('inner_count') or 0)+(created_data.get('open_count') or 0)
mig_cnt = created_data.get('open_count') or 0
nonmig_cnt = created_data.get('inner_count') or 0
print(f" {_('历史发币', 'Token history')} {_('共', 'total')} {total_cnt} {_('已迁移', 'migrated')} {mig_cnt} {_('未迁移', 'unmigrated')} {nonmig_cnt}")
top3_mc = sorted(all_tokens, key=lambda t: float(t.get('market_cap') or 0), reverse=True)[:3]
if top3_mc:
print(f" {_('当前市值 Top3', 'Current MC Top3')}:")
for i, t in enumerate(top3_mc, 1):
mig_label = _('已迁移', 'migrated') if t.get('is_open') else _('未迁移', 'unmigrated')
print(f" {i}. {t.get('symbol','?')} {usd(float(t.get('market_cap') or 0))} [{mig_label}]")
ath_info = created_data.get('creator_ath_info') or {}
if ath_info and ath_info.get('ath_mc'):
is_curr = ath_info.get('ath_token','').lower()==TOKEN_ADDR.lower()
curr_label = _('(本币)', ' (this token)') if is_curr else ''
print(f" {_('历史最高市值', 'All-time high MC')}: {ath_info.get('token_name','')}({ath_info.get('token_symbol','?')}){curr_label} ATH {usd(float(ath_info.get('ath_mc') or 0))}")
print()
# ── Related Funds ──
sec3 = _("🔗 关联资金", "🔗 Related Funds")
print(f"━━ {sec3} {'━'*(54-len(sec3))}")
print()
print(f" {_('多个钱包来自同一资金来源地址,或在极短时间内同步注资', 'Multiple wallets from same funding source or funded in tight time windows')}")
print()
if related:
relf = "🔴" if related_pct>0.15 else ("🟡" if related_pct>0.05 else "🟢")
print(f" {_('涉及', 'Involves')} {len(related)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(related_pct):.2f}% {usd(related_usd)} {relf}")
print()
print(f" ├─ {_('同一资金来源地址', 'Same funding source')} {len(same_src_groups)} {_('组', 'groups')} / {same_src_wallets} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(same_src_pct):.2f}%")
if same_src_groups:
fa, ws = same_src_groups[0]
native_in = sum(float((w.get('native_transfer') or {}).get('amount',0) or 0) for w in ws)
native_sym = 'SOL' if CHAIN=='sol' else ('BNB' if CHAIN=='bsc' else 'ETH')
print(f" │ {_('最大组', 'Largest group')}: {len(ws)} {_('个钱包', 'wallets')} {_('同一地址先后转入启动资金', 'funded sequentially from same addr')} {native_in:.4f} {native_sym}")
print(f" │")
if win_groups:
win_total = sum(len(v) for __,v in win_groups)
if win_total>=3:
k,v = win_groups[0]
print(f" └─ {_('同期注资(30min内集中入场)', 'Coordinated funding (within 30min)')} {win_total} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(win_pct):.2f}% ⚠️")
print(f" {_('最大批次', 'Largest batch')}: {len(v)} {_('个钱包', 'wallets')} {_('合计持仓', 'total hold')} {pct(sum(h['amount_percentage'] for h in v)):.3f}%")
else:
print(f" └─ {_('同期注资(30min内集中入场)', 'Coordinated funding (within 30min)')} {win_total} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(win_pct):.2f}%")
else:
print(f" └─ {_('未发现同期集中注资', 'No coordinated funding detected')}")
else:
print(f" {_('未发现明显关联资金', 'No significant linked funds detected')} 🟢")
print()
# ── Quality Signals ──
sec4 = _("🧠 优质信号", "🧠 Quality Signals")
print(f"━━ {sec4} {'━'*(54-len(sec4))}")
print()
print(f" {_('聪明钱', 'Smart Money')} {len(smart):2d} {_('持仓', 'hold')} {pct(smart_pct):.2f}% {'✅' if smart else '—'}")
if smart: print(f" {_('近期动向', 'Recent')}: {trend_str(smart)}")
print(f" KOL {len(kol):2d} {_('持仓', 'hold')} {pct(kol_pct):.2f}% {'✅' if kol else '—'}")
if kol:
for h in kol:
name = h.get('twitter_name') or h.get('name') or addr_short(h['address'])
print(f" · {name} {_('持仓', 'hold')} {pct(h['amount_percentage']):.2f}% {holding_status(h)} {_('买/卖', 'buy/sell')}: {h.get('buy_tx_count_cur',0)}/{h.get('sell_tx_count_cur',0)}")
print(f" {_('鲸鱼', 'Whale')} {len(whales):2d} {_('持仓', 'hold')} {pct(whale_pct):.2f}% {'✅' if whales else '—'}")
if whales: print(f" {_('近期动向', 'Recent')}: {trend_str(whales)}")
print()
df = "✅" if diamond_pct>0.5 else ("🟡" if diamond_pct>0.3 else "⚠️")
print(f" {_('钻石手(从未卖出)', 'Diamond hands (never sold)')} {len(diamond):2d} {_('持仓', 'hold')} {pct(diamond_pct):.1f}% {df}")
print(f" {_('部分卖出(<50%)', 'Partial sell (<50%)')} {len(partial):2d} {_('大量卖出(≥50%)', 'Heavy sell (≥50%)')} {len(heavy_sell):2d}")
sig_count = len(smart) + len(kol) + len(whales)
kol_selling = [h for h in kol if (h.get('sell_tx_count_cur') or 0) > 0]
kol_holding = [h for h in kol if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
smart_selling = [h for h in smart if (h.get('sell_tx_count_cur') or 0) > 0]
smart_holding = [h for h in smart if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
if sig_count == 0:
sig_summary = _("🟡 没有聪明钱和KOL,这个币没什么外部背书",
"🟡 No smart money or KOL — no external endorsement")
elif kol_selling and len(kol_selling) >= max(1, len(kol)//2+1):
sig_summary = _(f"🟡 {len(kol)}个KOL里有{len(kol_selling)}个已开始卖——跟进要小心",
f"🟡 {len(kol_selling)}/{len(kol)} KOL(s) already selling — be careful following")
elif smart_selling and len(smart_selling) >= max(1, len(smart)//2+1):
sig_summary = _(f"🟡 聪明钱里有{len(smart_selling)}个已经在出货,信号在减弱",
f"🟡 {len(smart_selling)} smart money wallet(s) selling — signal weakening")
elif smart_holding:
sig_summary = _(f"🟢 {len(smart_holding)}个聪明钱一直持仓没卖,这类人通常提前判断,信号较强",
f"🟢 {len(smart_holding)} smart money wallet(s) holding firm — usually early movers, strong signal")
elif kol_holding:
sig_summary = _(f"🟢 {len(kol_holding)}个KOL全程持仓未卖,参考价值保留",
f"🟢 {len(kol_holding)} KOL(s) fully holding — signal still valid")
else:
sig_summary = _("🟢 鲸鱼在场,有大资金背书",
"🟢 Whales present — backed by large capital")
print(f" → {_('小结', 'Summary')}{_('', ': ')}{sig_summary}")
print()
# ── Entry Cost Analysis ──
sec5 = _("📈 入场成本分析", "📈 Entry Cost Analysis")
print(f"━━ {sec5} {'━'*(54-len(sec5))}")
print()
print(f" {_('当前 MC', 'Current MC')} {usd(cur_mc)}")
print()
time_clusters = defaultdict(list)
for h in normal:
sh = h.get('start_holding_at') or 0
if sh<=0: continue
time_clusters[(sh-token_launch)//86400].append(h)
sig_clusters = sorted([(age,ws) for age,ws in time_clusters.items() if len(ws)>=2],
key=lambda x: -sum(h['amount_percentage'] for h in x[1]))[:4]
for rank, (age, ws) in enumerate(sig_clusters, 1):
entry_ts = token_launch + age*86400
label = age_label(entry_ts)
total_hp = sum(h['amount_percentage'] for h in ws)
costed = [h for h in ws if (h.get('avg_cost') or 0)>0]
selling_out = [h for h in ws if holding_status(h) in (
_('🔴 大量出货','🔴 Heavy Selling'), _('🟡 出货中','🟡 Selling'))]
still_hold = [h for h in ws if (h.get('balance') or 0)>=1]
if costed:
avg_entry = sum(total_supply*h['avg_cost'] for h in costed)/len(costed)
roi = (cur_mc-avg_entry)/avg_entry*100 if avg_entry>0 else 0
mc_str = _(f"建仓MC {usd(avg_entry)} → 现 {usd(cur_mc)} ({roi:+.0f}%)",
f"Entry MC {usd(avg_entry)} → now {usd(cur_mc)} ({roi:+.0f}%)")
else:
avg_entry=0; roi=0
mc_str = _("建仓MC 未知(转账获得)", "Entry MC unknown (received via transfer)")
sell_ratio = len(selling_out)/len(still_hold) if still_hold else 0
if roi>500 and sell_ratio>0.15:
risk_flag = "🔴"
conclusion = _(f"这批人建仓时才 {usd(avg_entry)},涨了 {roi:.0f}%,现在有 {len(selling_out)} 个在卖出套现——追入容易接到他们的盘",
f"Entry at {usd(avg_entry)}, up {roi:.0f}%, {len(selling_out)} already selling — buying now means catching their exits")
elif roi>200 and sell_ratio>0.1:
risk_flag = "🟡"
conclusion = _(f"涨了 {roi:.0f}%,有 {len(selling_out)} 个开始出货,但多数还没动——注意大户下一步动向",
f"Up {roi:.0f}%, {len(selling_out)} starting to sell but most still holding — watch whale next moves")
elif roi>0:
risk_flag = "🟢"
conclusion = _(f"涨了 {roi:.0f}%,出货的人不多,短期卖压不大",
f"Up {roi:.0f}%, few selling — limited short-term pressure")
else:
risk_flag = "🟢"
conclusion = _(f"这批人目前亏着呢({roi:.0f}%),不到割肉的程度,短期不太会卖",
f"Currently down {roi:.0f}% — unlikely to sell at a loss short-term")
batch_label = _('批次', 'Batch')
print(f" {batch_label}{rank}{_('', '(')}{label}{_('', ')')} {len(ws)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(total_hp):.2f}% {risk_flag}")
print(f" {mc_str}")
print(f" ➤ {conclusion}")
print()
notable = sorted([h for h in ws if h.get('balance',0)>=1 and h['amount_percentage']>=0.002],
key=lambda h: -h['amount_percentage'])
if not notable:
notable = sorted([h for h in ws if h.get('balance',0)>=1], key=lambda h: -h['amount_percentage'])[:5]
dumping = [h for h in notable if is_selling(h)]
holding_firm = [h for h in notable if is_buying_only(h)]
silent_list = [h for h in notable if not is_active(h)]
if dumping:
print(f" 🚨 {_('出货中的大户', 'Selling whales')}")
for h in dumping[:3]:
did = (h.get('twitter_name') or '') or addr_short(h['address'])
roles = wallet_roles(h)
role_s = "["+"·".join(roles)+"] " if roles else ""
print(f" · {role_s}{did} {pct(h['amount_percentage']):.2f}% {_('浮盈','pnl')}{(h.get('unrealized_pnl') or 0)*100:+.0f}% {_('已卖','sold')} {h.get('sell_tx_count_cur') or 0}x {holding_status(h)}")
print()
if holding_firm:
print(f" 📈 {_('持续加仓/未出货', 'Still accumulating / not sold')}")
for h in holding_firm[:3]:
did = (h.get('twitter_name') or '') or addr_short(h['address'])
roles = wallet_roles(h)
role_s = "["+"·".join(roles)+"] " if roles else ""
print(f" · {role_s}{did} {pct(h['amount_percentage']):.2f}% {_('浮盈','pnl')}{(h.get('unrealized_pnl') or 0)*100:+.0f}% {_('已买','bought')} {h.get('buy_tx_count_cur') or 0}x")
print()
if silent_list:
silent_pct_val = sum(h['amount_percentage'] for h in silent_list)
buy0_cnt = sum(1 for h in silent_list if h.get('buy_tx_count_cur',0)==0)
note_str = (_("零成本空降为主", "mostly zero-cost airdrop") if buy0_cnt>len(silent_list)//2
else _("持仓未动", "holding without activity"))
print(f" 💤 {_('静默持仓', 'Silent holders')} {len(silent_list)} {pct(silent_pct_val):.2f}% → {_('抛压低,', 'low pressure, ')}{note_str}")
print()
print()
# ── Buying Power ──
sec6 = _("💰 持仓者购买力", "💰 Holder Buying Power")
print(f"━━ {sec6} {'━'*(54-len(sec6))}")
print()
print(f" {_('衡量现有持仓者还有多少子弹可以加仓', 'How much ammo holders have left to add')} {_('合计可用余额', 'Total balance')} {usd(total_buying_power)}")
print()
if zero_wallets:
print(f" ⚫ {_('零余额', 'Zero balance')} {len(zero_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(zero_pct_val):.2f}% $0 → {_('无加仓能力,可能是分仓小号', 'No buying power, likely sub-wallets')}")
if low_wallets:
low_total = sum(native_usd(h) for h in low_wallets)
print(f" 🟡 {_('低(<$200', 'Low (<$200)')} {len(low_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(low_pct_val):.2f}% {usd(low_total)}")
if mid_wallets:
mid_total = sum(native_usd(h) for h in mid_wallets)
print(f" 🟠 {_('中($200~$1200', 'Mid ($200~$1200)')} {len(mid_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(mid_pct_val):.2f}% {usd(mid_total)}")
if high_wallets:
print(f" 🔴 {_('高($1200+', 'High ($1200+)')} {len(high_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(high_pct_val):.2f}% {usd(high_total)} → {_('可随时加仓', 'can add anytime')}")
print()
if high_wallets:
print(f" ➤ {_('高余额钱包', 'High-balance wallets')} {len(high_wallets)} {_('个持仓', 'holding')} {pct(high_pct_val):.1f}%{_('', ', ')}{_('合计', 'total')} {usd(high_total)} {_('可随时加仓', 'ready to add')}")
if zero_wallets and zero_pct_val > 0.05:
print(f" ➤ {len(zero_wallets)} {_('个钱包零余额(持仓 ', 'wallets with zero balance (hold ')}{pct(zero_pct_val):.1f}%{_('', ')')}, {_('无加仓能力,可能是分仓小号', 'no buying power, likely sub-wallets')}")
print()
# ── Chip Structure ──
sec7 = _("📊 筹码结构", "📊 Chip Structure")
print(f"━━ {sec7} {'━'*(54-len(sec7))}")
print()
total_n = len(normal)
if total_n > 0:
print(f" {_('盈利', 'Profit')} {len(profit_w)} ({len(profit_w)/total_n*100:.0f}%) {_('亏损', 'Loss')} {len(loss_w)} ({len(loss_w)/total_n*100:.0f}%) {_('持平', 'Break-even')} {total_n-len(profit_w)-len(loss_w)}")
tf_flag = "⚠️" if len(trapped)>30 else ""
print(f" {_('套牢盘(浮亏>20%', 'Underwater (>20% loss)')} {len(trapped)} {_('持仓', 'hold')} {pct(sum(h['amount_percentage'] for h in trapped)):.2f}% {tf_flag}")
print(f" {_('平均持仓时长', 'Avg hold duration')} {avg_hold_days:.1f} {_('天', 'days')}")
print()
# ── AI Advice ──
sec8 = _("🤖 AI 建议", "🤖 AI Advice")
print(f"━━ {sec8} {'━'*(54-len(sec8))}")
print()
print(f" {rating_em} {rating_text}")
print()
if dangers:
print(f" {_('核心风险', 'Core Risks')}:")
for d in dangers: print(f" · {d}")
if warns:
print(f" {_('注意信号', 'Warnings')}:")
for w in warns: print(f" · {w}")
if goods:
print(f" {_('积极因素', 'Positives')}:")
for g in goods: print(f" · {g}")
hf = "🔴" if healthy_ratio<0.3 else ("🟡" if healthy_ratio<0.5 else "🟢")
print(f"\n {_('健康筹码', 'Healthy chips')} {pct(healthy_ratio):.1f}% {hf} DEX {pct(dex_pct):.1f}% {_('不纳入评估', 'excluded from eval')}")
print()
print(f" {_('关注以下信号,出现则考虑离场:', 'Watch for these exit signals:')}")
for sig in exit_signals:
print(f" · {sig}")
PYEOF
```
-->
## Field Reference
### Holder object key fields
| Field | Type | Meaning |
|-------|------|---------|
| `address` | string | Wallet address |
| `balance` | float | Current token balance |
| `amount_percentage` | float | Fraction of total supply (01). Multiply by 100 for %. |
| `usd_value` | float | Current USD value of holdings |
| `avg_cost` | float | Average buy price per token |
| `unrealized_pnl` | float | Unrealized PnL ratio (0.5 = +50%) |
| `unrealized_profit` | float | Unrealized PnL in USD |
| `realized_profit` | float | Realized PnL in USD |
| `buy_tx_count_cur` | int | Buy transactions since token creation |
| `sell_tx_count_cur` | int | Sell transactions since token creation |
| `sell_amount_percentage` | float | Fraction of total buys that have been sold |
| `start_holding_at` | int | Unix timestamp of first buy |
| `addr_type` | int | 0=normal wallet, 1=burn/dead, 2=DEX/pool |
| `maker_token_tags` | list | `bundler`, `rat_trader`, `sniper`, `whale`, `top_holder`, `transfer_in`, `dev_team`, `creator` |
| `tags` | list | `smart_degen`, `pump_smart`, `renowned`, `fresh_wallet`, `wash_trader`, `fomo`, `kol` |
| `native_balance` | string | Raw native token balance (SOL: lamports /1e9; EVM: wei /1e18) |
| `native_transfer` | object | `{from_address, amount, timestamp}` — how wallet was funded |
| `twitter_name` | string | Twitter handle if known |
| `exchange` | string | DEX name for pool wallets |
### Created-tokens response fields
| Field | Meaning |
|-------|---------|
| `inner_count` | Unmigrated token count |
| `open_count` | Migrated token count |
| `tokens[].market_cap` | Current market cap in USD |
| `tokens[].is_open` | true = migrated |
| `creator_ath_info.ath_mc` | All-time high MC across all created tokens |
| `creator_ath_info.ath_token` | Token address of the ATH token |
| `creator_ath_info.token_symbol` | Symbol of the ATH token |
## Rating Standard
Entry timing pressure (批次浮盈/出货) does **NOT** affect the overall rating — it only affects section display.
| Rating (ZH) | Rating (EN) | Emoji | Condition |
|-------------|-------------|-------|-----------|
| 不建议买 | Not Recommended | 🔴 | Any: rat traders >10% / largest wallet >15% / dev sock puppet |
| 谨慎参与 | Caution | ⚠️ | ≥2 of: Dev still holding / airdrop >15% / risk wallets >30% / linked >10% |
| 可轻仓 | Light Position | 🟡 | Exactly 1 of above warns |
| 正常参与 | Normal | ✅ | None of the above |
## Supported Chains
`sol`, `bsc`, `base`, `eth`, `robinhood`
## Notes
- `balance >= 1` threshold avoids dust false positives when identifying dev holdings
- SOL `native_balance` is in lamports (÷1e9); EVM `native_balance` is in wei (÷1e18)
- `total_supply` is estimated as the median of `balance / amount_percentage` across normal wallets
- `cur_price` is estimated as the median of `usd_value / balance` across normal wallets
- Top5 displays Twitter name when available; else `first4...last4` format
+675
View File
@@ -0,0 +1,675 @@
#!/usr/bin/env python3
import json, subprocess, sys, time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
TOKEN_ADDR = sys.argv[1]
CHAIN = sys.argv[2]
LANG = sys.argv[3] if len(sys.argv) > 3 else 'zh'
# EVM 地址自动探测链(0x... 且 chain 传入 'auto' 或未明确指定时)
KNOWN_CHAINS = ('bsc', 'eth', 'base', 'sol', 'robinhood')
if CHAIN == 'auto' or (TOKEN_ADDR.startswith('0x') and CHAIN not in KNOWN_CHAINS):
for _c in ('bsc', 'eth', 'base'):
_r = subprocess.run(['gmgn-cli', 'token', 'holders', '--chain', _c,
'--address', TOKEN_ADDR, '--limit', '5', '--raw'],
capture_output=True, text=True, timeout=15)
if _r.returncode == 0:
_data = json.loads(_r.stdout)
if _data.get('list'):
CHAIN = _c
break
else:
CHAIN = 'eth' # fallback
WINDOW = 1800
now_ts = int(time.time())
ZH = (LANG == 'zh')
def _(zh, en): return zh if ZH else en
def run_cli(args, timeout=30):
r = subprocess.run(['gmgn-cli'] + args + ['--raw'],
capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise RuntimeError(r.stderr)
return json.loads(r.stdout)
with ThreadPoolExecutor(max_workers=3) as ex:
f_holders = ex.submit(run_cli, ['token', 'holders', '--chain', CHAIN, '--address', TOKEN_ADDR, '--limit', '100'])
f_devs = ex.submit(run_cli, ['token', 'holders', '--chain', CHAIN, '--address', TOKEN_ADDR, '--tag', 'dev', '--limit', '20'])
# dev 结果一到,立即发起 created-tokens,不等 holders
devs = f_devs.result()['list']
_creator_tmp = next((d for d in devs if 'creator' in (d.get('maker_token_tags') or [])), None)
f_created = None
if _creator_tmp:
f_created = ex.submit(run_cli, ['portfolio', 'created-tokens', '--chain', CHAIN,
'--wallet', _creator_tmp['address'],
'--order-by', 'token_ath_mc', '--direction', 'desc'])
holders = f_holders.result()['list']
created_data = f_created.result() if f_created else None
normal = [h for h in holders if h.get('addr_type', 0) == 0]
burn = [h for h in holders if h.get('addr_type', 0) == 1]
dex = [h for h in holders if h.get('addr_type', 0) == 2]
def pct(v): return v * 100
def usd(v):
if v is None: return "$0"
if abs(v) >= 1_000_000: return f"${v/1_000_000:.2f}M"
if abs(v) >= 1_000: return f"${v/1_000:.1f}K"
return f"${v:.0f}"
def fmt_amt(v):
if v >= 1_000_000: return f"{v/1_000_000:.1f}M"
if v >= 1_000: return f"{v/1_000:.0f}K"
return f"{v:.0f}"
def age_label(entry_ts):
secs = now_ts - entry_ts
days = secs // 86400
hours = secs // 3600
if ZH: return f"{hours}小时前入场" if days == 0 else f"{days}天前入场"
else: return f"{hours}h ago" if days == 0 else f"{days}d ago"
def addr_short(addr):
return f"{addr[:4]}...{addr[-4:]}"
supply_list = [h['balance']/h['amount_percentage'] for h in normal
if h.get('amount_percentage',0)>0 and h.get('balance',0)>0]
total_supply = sorted(supply_list)[len(supply_list)//2] if supply_list else 1_000_000_000
price_list = [h['usd_value']/h['balance'] for h in normal
if h.get('balance',0)>0 and h.get('usd_value',0)>0]
cur_price = sorted(price_list)[len(price_list)//2] if price_list else 0
cur_mc = total_supply * cur_price
burn_pct = sum(h['amount_percentage'] for h in burn)
dex_pct = sum(h['amount_percentage'] for h in dex)
top10 = sum(h['amount_percentage'] for h in holders[:10])
top20 = sum(h['amount_percentage'] for h in holders[:20])
airdrop = [h for h in normal if h.get('buy_tx_count_cur', 0)==0 and h.get('balance', 0)>0]
bundlers = [h for h in normal if 'bundler' in (h.get('maker_token_tags') or [])]
rats = [h for h in normal if 'rat_trader' in (h.get('maker_token_tags') or [])]
snipers = [h for h in normal if 'sniper' in (h.get('maker_token_tags') or [])]
fresh = [h for h in normal if 'fresh_wallet' in (h.get('tags') or [])]
wash = [h for h in normal if 'wash_trader' in (h.get('tags') or [])]
risk_all = set(h['address'] for g in [bundlers, rats, snipers, fresh, wash] for h in g)
risk_pct = sum(h['amount_percentage'] for h in normal if h['address'] in risk_all)
airdrop_pct = sum(h['amount_percentage'] for h in airdrop)
rats_pct = sum(h['amount_percentage'] for h in rats)
normal_pct = sum(h['amount_percentage'] for h in normal)
all_bad = set(h['address'] for h in airdrop) | risk_all
bad_pct = sum(h['amount_percentage'] for h in normal if h['address'] in all_bad)
healthy_pct = max(normal_pct - bad_pct, 0)
healthy_ratio = (healthy_pct / normal_pct) if normal_pct > 0 else 0
from_map = defaultdict(list)
for h in normal:
fa = (h.get('native_transfer') or {}).get('from_address', '')
if fa: from_map[fa].append(h)
same_src_groups = sorted([(fa, ws) for fa, ws in from_map.items() if len(ws)>=2], key=lambda x: -len(x[1]))
same_src_wallets = sum(len(ws) for __,ws in same_src_groups)
same_src_pct = sum(h['amount_percentage'] for __,ws in same_src_groups for h in ws)
funded = [((h.get('native_transfer') or {}).get('timestamp',0), h)
for h in normal if (h.get('native_transfer') or {}).get('timestamp',0)]
bucket = defaultdict(list)
for ts, h in funded:
bucket[(ts//WINDOW)*WINDOW].append(h)
win_groups = sorted([(k,v) for k,v in bucket.items() if len(v)>=2], key=lambda x: -len(x[1]))
win_pct = sum(h['amount_percentage'] for __,v in win_groups for h in v)
related = set()
for __,ws in same_src_groups:
for h in ws: related.add(h['address'])
for __,v in win_groups:
for h in v: related.add(h['address'])
related_pct = sum(h['amount_percentage'] for h in normal if h['address'] in related)
related_usd = sum(h.get('usd_value',0) for h in normal if h['address'] in related)
smart = [h for h in normal if any(t in (h.get('tags') or []) for t in ['smart_degen','pump_smart'])]
kol = [h for h in normal if 'kol' in (h.get('tags') or []) or 'renowned' in (h.get('tags') or [])]
whales = [h for h in normal if 'whale' in (h.get('maker_token_tags') or [])]
diamond = [h for h in normal if h.get('sell_tx_count_cur',0)==0 and h.get('balance',0)>0]
partial = [h for h in normal if 0<(h.get('sell_amount_percentage') or 0)<0.5]
heavy_sell = [h for h in normal if (h.get('sell_amount_percentage') or 0)>=0.5]
smart_pct = sum(h['amount_percentage'] for h in smart)
kol_pct = sum(h['amount_percentage'] for h in kol)
whale_pct = sum(h['amount_percentage'] for h in whales)
diamond_pct = sum(h['amount_percentage'] for h in diamond)
top100_map = {h['address']: h for h in holders}
creator = next((d for d in devs if 'creator' in (d.get('maker_token_tags') or [])), None)
sub_devs = [d for d in devs if 'creator' not in (d.get('maker_token_tags') or [])]
dev_realized = sum(d.get('realized_profit') or 0 for d in devs)
dev_holding = [d for d in devs if (d.get('balance') or 0)>=1]
valid_starts = [h['start_holding_at'] for h in holders if (h.get('start_holding_at') or 0)>0]
token_launch = min(valid_starts) if valid_starts else now_ts
durations = [now_ts-h['start_holding_at'] for h in normal
if (h.get('start_holding_at') or 0)>0 and now_ts>h['start_holding_at']]
avg_hold_days = (sum(durations)/len(durations)/86400) if durations else 0
profit_w = [h for h in normal if (h.get('profit') or 0)>0]
loss_w = [h for h in normal if (h.get('profit') or 0)<0]
trapped = [h for h in normal if (h.get('unrealized_pnl') or 0)<-0.2 and h.get('balance',0)>0]
NATIVE_PRICE = 160 if CHAIN == 'sol' else (700 if CHAIN == 'bsc' else 2500)
NATIVE_DENOM = 1e9 if CHAIN == 'sol' else 1e18
def native_usd(h): return int(h.get('native_balance') or 0)/NATIVE_DENOM*NATIVE_PRICE
zero_wallets = [h for h in normal if native_usd(h)==0]
low_wallets = [h for h in normal if 0 < native_usd(h) <= 200]
mid_wallets = [h for h in normal if 200 < native_usd(h) <= 1200]
high_wallets = [h for h in normal if native_usd(h) > 1200]
zero_pct_val = sum(h['amount_percentage'] for h in zero_wallets)
low_pct_val = sum(h['amount_percentage'] for h in low_wallets)
mid_pct_val = sum(h['amount_percentage'] for h in mid_wallets)
high_pct_val = sum(h['amount_percentage'] for h in high_wallets)
high_total = sum(native_usd(h) for h in high_wallets)
total_buying_power = sum(native_usd(h) for h in normal)
ROLE_MAP = {
'rat_trader': _('老鼠仓', 'Rat Trader'),
'sniper': _('狙击', 'Sniper'),
'bundler': _('捆绑', 'Bundler'),
'whale': _('鲸鱼', 'Whale'),
'smart_degen': _('聪明钱', 'Smart'),
'pump_smart': _('聪明钱', 'Smart'),
'renowned': 'KOL',
'kol': 'KOL',
'fresh_wallet': _('新钱包', 'Fresh'),
'wash_trader': _('刷量', 'Wash'),
'creator': 'Dev',
'dev_team': 'Dev',
}
def wallet_roles(h):
roles = []
for t in (h.get('maker_token_tags') or [])+(h.get('tags') or []):
if t in ROLE_MAP: roles.append(ROLE_MAP[t])
return list(dict.fromkeys(roles))
def holding_status(h):
sp = h.get('sell_amount_percentage', 0) or 0
bal = h.get('balance', 0) or 0
if bal <= 0: return _("已清仓", "Cleared")
if sp >= 0.8: return _("🔴 大量出货", "🔴 Heavy Selling")
if sp >= 0.3: return _("🟡 出货中", "🟡 Selling")
if sp > 0: return _("少量出货", "Light Selling")
return _("持仓未动", "Holding")
def wallet_behavior(h):
buy_tx = h.get('buy_tx_count_cur', 0) or 0
sell_tx = h.get('sell_tx_count_cur', 0) or 0
if buy_tx==0 and sell_tx==0: return _("几乎无链上活动", "Almost no on-chain activity")
if buy_tx>0 and sell_tx==0: return _("持续买入,尚未卖出", "Buying only, not sold yet")
if sell_tx>0 and buy_tx==0: return _("只卖不买", "Selling only")
return ""
def trend_str(wlist):
buying = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)==0]
selling = [h for h in wlist if (h.get('sell_tx_count_cur') or 0)>0 and (h.get('buy_tx_count_cur') or 0)==0]
both = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)>0]
holding = [h for h in wlist if (h.get('buy_tx_count_cur') or 0)==0 and (h.get('sell_tx_count_cur') or 0)==0]
parts = []
if buying: parts.append(f"📈 {_('买入中', 'Buying')} {len(buying)}")
if selling: parts.append(f"📉 {_('卖出中', 'Selling')} {len(selling)}")
if both: parts.append(f"🔄 {_('买卖都有', 'Both')} {len(both)}")
if holding: parts.append(f"🤝 {_('未动', 'Holding')} {len(holding)}")
return " ".join(parts) if parts else ""
def is_active(h): return (h.get('buy_tx_count_cur') or 0)+(h.get('sell_tx_count_cur') or 0)>0
def is_selling(h): return (h.get('sell_tx_count_cur') or 0)>0 and (h.get('balance') or 0)>=1
def is_buying_only(h): return (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)==0
biggest = max(normal, key=lambda h: h['amount_percentage']) if normal else None
dangers = []
if rats and rats_pct > 0.1:
dangers.append(_( f"老鼠仓持仓 {pct(rats_pct):.1f}%,出货即砸盘",
f"Rat traders hold {pct(rats_pct):.1f}% — instant dump risk"))
if biggest and biggest['amount_percentage'] > 0.15:
dangers.append(_( f"最大单钱包持仓 {pct(biggest['amount_percentage']):.1f}%,筹码极度集中",
f"Largest wallet holds {pct(biggest['amount_percentage']):.1f}% — extreme concentration"))
if creator:
to_out = creator.get('token_transfer_out') or {}
if (to_out.get('address') or '') in top100_map:
dangers.append(_("Dev 筹码转给内部马甲,换手控盘",
"Dev transferred chips to internal wallet — covert control"))
warns = []
if dev_holding:
hold_pct_val = sum(d.get('amount_percentage',0) for d in dev_holding)
if hold_pct_val > 0.01:
warns.append(_( f"Dev 仍持仓 {pct(hold_pct_val):.2f}%",
f"Dev still holds {pct(hold_pct_val):.2f}%"))
if airdrop_pct > 0.15:
warns.append(_( f"空降筹码 {pct(airdrop_pct):.1f}%,来源不透明",
f"Airdrop supply {pct(airdrop_pct):.1f}% — opaque origin"))
if risk_pct > 0.3:
warns.append(_( f"风险钱包持仓 {pct(risk_pct):.1f}%,筹码质量差",
f"Risk wallets hold {pct(risk_pct):.1f}% — low chip quality"))
if related_pct > 0.1:
warns.append(_( f"关联钱包 {len(related)} 个持仓 {pct(related_pct):.1f}%",
f"Linked wallets ({len(related)}) hold {pct(related_pct):.1f}%"))
if dangers:
rating_em, rating_text = "🔴", _("不建议买", "Not Recommended")
elif len(warns) >= 2:
rating_em, rating_text = "⚠️", _("谨慎参与", "Caution")
elif len(warns) == 1:
rating_em, rating_text = "🟡", _("可轻仓", "Light Position")
else:
rating_em, rating_text = "", _("正常参与", "Normal")
goods = []
if burn_pct > 0.05:
goods.append(_( f"销毁 {pct(burn_pct):.1f}% 永久锁仓,流通减少",
f"Burned {pct(burn_pct):.1f}% permanently — reduced supply"))
if whales:
buying_w = [h for h in whales if is_buying_only(h)]
if buying_w:
goods.append(_( f"鲸鱼 {len(buying_w)} 个持续买入,尚未出货",
f"{len(buying_w)} whale(s) still accumulating, not sold"))
if kol:
goods.append(_( f"KOL {len(kol)} 个在场({pct(kol_pct):.2f}%",
f"{len(kol)} KOL(s) holding ({pct(kol_pct):.2f}%)"))
if diamond_pct > 0.4:
goods.append(_( f"钻石手持仓 {pct(diamond_pct):.1f}%,筹码稳定",
f"Diamond hands hold {pct(diamond_pct):.1f}% — stable chips"))
exit_signals = []
if rats:
exit_signals.append(_("老鼠仓钱包出现卖出操作", "Rat trader wallets start selling"))
if dev_holding:
exit_signals.append(_("Dev 钱包开始出货", "Dev wallets start dumping"))
if airdrop_pct>0.15:
exit_signals.append(_("空降大户出现集中卖出", "Airdrop whales start concentrated selling"))
if not exit_signals:
exit_signals = [_("Top5 大户出现集中出货", "Top 5 holders start concentrated selling"),
_("价格跌破建仓均价支撑", "Price breaks below average entry cost")]
exit_signals = exit_signals[:3]
top5_holders = sorted(normal, key=lambda h: -h['amount_percentage'])[:5]
def top5_pressure(h):
avg_cost = h.get('avg_cost') or 0
up_pnl = h.get('unrealized_pnl') or 0
up_usd = h.get('unrealized_profit') or 0
buy0 = h.get('buy_tx_count_cur',0)==0
roles = wallet_roles(h)
if buy0 and not roles: roles.append(_('空降', 'Airdrop'))
role_str = "["+"·".join(roles)+"] " if roles else ""
display_id = (h.get('twitter_name') or '') or addr_short(h['address'])
if buy0 or avg_cost==0:
cost_str = _("零成本(转账获得)", "Zero cost (received via transfer)")
pnl_str = ""
lv = "⚠️ " + _("", "High")
note = _("零成本,随时可出货", "Zero cost — can dump anytime")
elif up_pnl>1.0:
mult = up_pnl+1
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"+{up_pnl*100:.0f}% ({mult:.1f}x) {usd(up_usd)}"
lv = "⚠️ " + _("", "High")
note = _(f"建仓MC {usd(total_supply*avg_cost)} → 现 {usd(cur_mc)},浮盈 {mult:.1f}x 压力强",
f"Entry MC {usd(total_supply*avg_cost)} → now {usd(cur_mc)}, {mult:.1f}x gain — strong pressure")
elif up_pnl>0.1:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"+{up_pnl*100:.0f}% {usd(up_usd)}"
lv = "🟡 " + _("", "Med")
note = _("小幅浮盈,出货意愿一般", "Moderate gain — mild sell pressure")
elif up_pnl>=-0.1:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"{up_pnl*100:+.0f}% {usd(up_usd)}" if abs(up_usd)>=1 else _("接近成本", "Near cost")
lv = "🟢 " + _("", "Low")
note = _("接近成本,短期抛压有限", "Near break-even — limited short-term pressure")
else:
cost_str = f"{_('均价', 'avg')} ${avg_cost:.4f}"
pnl_str = f"{up_pnl*100:.0f}% {usd(up_usd)}"
lv = "🟢 " + _("", "Low")
note = _("套牢中,短期不易割肉", "Underwater — unlikely to sell soon")
beh = wallet_behavior(h)
beh_str = (" " + _("行为", "behavior") + ": " + beh) if beh else ""
return role_str, display_id, cost_str, pnl_str, lv, note, beh_str, holding_status(h)
title = _("Holder 筹码分析", "Holder Chip Analysis")
print(f"{''*56}")
print(f"{(' '+title):^56}")
print(f"{(' '+TOKEN_ADDR[:10]+'...'+TOKEN_ADDR[-4:]+' · Top100 · '+CHAIN.upper()):^56}")
print(f"{(' MC '+usd(cur_mc)):^56}")
print(f"{''*56}")
print()
sec1 = _("🚨 砸盘风险", "🚨 Dump Risk")
print(f"━━ {sec1} {''*(54-len(sec1))}")
print()
c10f = "🔴" if top10>0.5 else ("🟡" if top10>0.3 else "🟢")
c20f = "🔴" if top20>0.6 else ("🟡" if top20>0.4 else "🟢")
print(f" {_('集中度', 'Concentration')} Top10 {pct(top10):.1f}% {c10f} Top20 {pct(top20):.1f}% {c20f}")
print()
if burn:
print(f" 🔥 {_('销毁地址', 'Burn addr')} {pct(burn_pct):.2f}% ✅ {_('永久锁仓,无法流通', 'Permanently locked, non-circulating')}")
print()
airf = "🔴" if airdrop_pct>0.2 else ("🟡" if airdrop_pct>0.1 else "🟢")
print(f" {_('空降筹码(从未买入、靠转账获得)', 'Airdrop (never bought, received via transfer)')} {len(airdrop)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(airdrop_pct):.2f}% {airf}")
print()
riskf = "🔴" if risk_pct>0.3 else ("🟡" if risk_pct>0.1 else "🟢")
print(f" {_('风险钱包', 'Risk wallets')} {_('合计', 'total')} {len(risk_all)} {_('', '')} {_('持仓', 'hold')} {pct(risk_pct):.2f}% {riskf}")
risk_labels = [
(_("老鼠仓", "Rat Trader"), rats, "🚨"),
(_("捆绑交易", "Bundler"), bundlers, "⚠️"),
(_("狙击者", "Sniper"), snipers, "⚠️"),
(_("新钱包", "Fresh"), fresh, ""),
(_("刷量", "Wash"), wash, ""),
]
for label, group, flag in risk_labels:
if group:
gp = pct(sum(h['amount_percentage'] for h in group))
print(f" · {label:10s} {len(group):2d} {_('', '')} {_('持仓', 'hold')} {gp:.2f}% {flag}")
if not any([rats,bundlers,snipers,fresh,wash]):
print(f" · {_('未发现风险标签钱包', 'No risk-tagged wallets found')} 🟢")
print()
bundler_pct_val = sum(h['amount_percentage'] for h in bundlers)
sniper_pct_val = sum(h['amount_percentage'] for h in snipers)
if dangers:
sdp_summary = f"🔴 {dangers[0]}"
elif rats_pct > 0.05:
sdp_summary = _( f"🔴 老鼠仓持仓 {pct(rats_pct):.1f}%,零成本拿的,随时可以无损砸盘",
f"🔴 Rat traders hold {pct(rats_pct):.1f}% at zero cost — can dump with no loss anytime")
elif top10 > 0.4:
sdp_summary = _( f"🟡 Top10 持仓 {pct(top10):.1f}%,筹码过于集中,大户一旦出货冲击很大",
f"🟡 Top10 hold {pct(top10):.1f}% — highly concentrated, big impact if they sell")
elif bundler_pct_val > 0.2:
sdp_summary = _( f"🟡 捆绑钱包持仓 {pct(bundler_pct_val):.1f}%,这批是开盘机器人扫货,出货时可能集中砸盘",
f"🟡 Bundlers hold {pct(bundler_pct_val):.1f}% — bot-swept at open, may dump together")
elif airdrop_pct > 0.15:
sdp_summary = _( f"🟡 空降筹码 {pct(airdrop_pct):.1f}%,这些人零成本拿到币,随时可能出货",
f"🟡 Airdrop supply {pct(airdrop_pct):.1f}% at zero cost — may sell anytime")
elif sniper_pct_val > 0.1:
sdp_summary = _( f"🟡 狙击者持仓 {pct(sniper_pct_val):.1f}%,开盘低价进的,浮盈高、随时可套现",
f"🟡 Snipers hold {pct(sniper_pct_val):.1f}% at launch price — high unrealized gain, may cash out")
elif risk_pct > 0.1:
sdp_summary = _( f"🟡 风险钱包合计持仓 {pct(risk_pct):.1f}%,需要留意动向",
f"🟡 Risk wallets total {pct(risk_pct):.1f}% — watch their moves")
elif burn_pct > 0.1:
sdp_summary = _( f"🟢 销毁了 {pct(burn_pct):.1f}%,流通筹码少,LP 也锁住了,相对干净",
f"🟢 {pct(burn_pct):.1f}% burned — reduced supply, LP locked, relatively clean")
else:
sdp_summary = _("🟢 集中度正常,没发现明显的砸盘风险",
"🟢 Normal concentration, no obvious dump risk detected")
print(f"{_('小结', 'Summary')}{_('', ': ')}{sdp_summary}")
print()
print(f" {_('Top5 持仓钱包抛压分析', 'Top 5 Holder Sell Pressure')}")
print(f" {_('浮盈越高 / 建仓MC越低 → 获利了结压力越强', 'Higher unrealized gain / lower entry MC → stronger sell pressure')}")
print()
for i, h in enumerate(top5_holders, 1):
role_str, display_id, cost_str, pnl_str, lv, note, beh_str, st = top5_pressure(h)
hp = pct(h['amount_percentage'])
print(f" {i}. {role_str}{display_id}")
print(f" {_('持仓', 'hold')} {hp:.2f}% {cost_str} {_('盈亏', 'pnl')} {pnl_str}")
print(f" {_('抛压', 'pressure')} {lv}{note}")
print(f" {_('状态', 'status')} {st}{beh_str}")
print()
sec2 = _("👨‍💻 Dev 钱包", "👨‍💻 Dev Wallets")
print(f"━━ {sec2} {''*(54-len(sec2))}")
print()
dev_status = (_("✅ 全部余额归零", "✅ All cleared") if not dev_holding
else _(f"⚠️ 仍有 {len(dev_holding)} 个持仓中", f"⚠️ {len(dev_holding)} still holding"))
if sub_devs:
print(f" {_('', 'Total')} {len(devs)} {_('个钱包(1 主号 + ', 'wallets (1 main + ')}{len(sub_devs)}{_(' 小号)', ' sub)')} {dev_status}")
else:
print(f" {_('', 'Total')} {len(devs)} {_('个钱包', 'wallets')} {dev_status}")
print(f" {_('Dev 合计已实现利润', 'Dev total realized profit')} {usd(dev_realized)}")
print()
if creator:
c_sell_v = creator.get('sell_volume_cur') or 0
tf_out = creator.get('history_transfer_out_amount') or 0
tf_val = creator.get('history_transfer_out_income') or 0
sell_amt = creator.get('sell_amount_cur') or 0
hold_pct_c = creator.get('amount_percentage') or 0
c_status = (_("余额归零", "Balance zero") if (creator.get('balance') or 0)<1
else _(f"⚠️ 持仓 {pct(hold_pct_c):.2f}%", f"⚠️ Holding {pct(hold_pct_c):.2f}%"))
print(f" {_('主号 (Creator)', 'Main (Creator)')} {addr_short(creator['address'])}")
parts = [c_status]
if sell_amt>0:
parts.append(_(f"卖出 {fmt_amt(sell_amt)} 个({usd(c_sell_v)}",
f"Sold {fmt_amt(sell_amt)} ({usd(c_sell_v)})"))
if tf_out>0:
to_out = creator.get('token_transfer_out') or {}
to_addr = to_out.get('address') or ''
if to_addr and to_addr in top100_map:
parts.append(_(f"转出 {fmt_amt(tf_out)} 个至 Top100 内部钱包(估值 {usd(tf_val)}",
f"Transferred {fmt_amt(tf_out)} to Top100 internal wallet (est. {usd(tf_val)})"))
else:
parts.append(_(f"转出 {fmt_amt(tf_out)} 个至外部地址(估值 {usd(tf_val)}",
f"Transferred {fmt_amt(tf_out)} to external addr (est. {usd(tf_val)})"))
print(f" {' '.join(parts)}")
to_out = creator.get('token_transfer_out') or {}
to_addr = to_out.get('address') or ''
if to_addr and to_addr in top100_map:
target = top100_map[to_addr]
t_mtags = [t for t in (target.get('maker_token_tags') or []) if t not in ('top_holder','transfer_in')]
print(f"\n ⚠️ {_('转出筹码仍在 Top100(换马甲继续持有):', 'Transferred chips still in Top100 (sock puppet):')}")
print(f" {addr_short(to_addr)} {_('持仓', 'hold')} {pct(target.get('amount_percentage',0)):.2f}% {_('标签', 'tags')}: {' '.join(t_mtags) or _('','none')}")
elif (creator.get('balance') or 0)<1 and tf_out==0:
print(f"{_('已完全卖出,无异常转账记录', 'Fully sold, no abnormal transfers')}")
print()
if to_addr and to_addr in top100_map:
dev_summary = _("🔴 Dev 换马甲持仓,这个很危险,随时可以砸盘",
"🔴 Dev using sock puppet — very dangerous, can dump anytime")
elif dev_holding:
dev_summary = _("🟡 Dev 还没出完,有出货风险,关注钱包动向",
"🟡 Dev hasn't fully exited — dump risk, watch wallet activity")
elif dev_realized > 50000:
dev_summary = _(f"🟡 Dev 已套现 {usd(dev_realized)},虽然出完了但赚了不少",
f"🟡 Dev cashed out {usd(dev_realized)} — exited but made significant profit")
else:
dev_summary = _("🟢 Dev 已清仓,没有持仓压力",
"🟢 Dev fully exited — no holding pressure")
print(f"{_('小结', 'Summary')}{_('', ': ')}{dev_summary}")
print()
if created_data:
all_tokens = created_data.get('tokens') or []
total_cnt = (created_data.get('inner_count') or 0)+(created_data.get('open_count') or 0)
mig_cnt = created_data.get('open_count') or 0
nonmig_cnt = created_data.get('inner_count') or 0
print(f" {_('历史发币', 'Token history')} {_('', 'total')} {total_cnt} {_('已迁移', 'migrated')} {mig_cnt} {_('未迁移', 'unmigrated')} {nonmig_cnt}")
top3_mc = sorted(all_tokens, key=lambda t: float(t.get('market_cap') or 0), reverse=True)[:3]
if top3_mc:
print(f" {_('当前市值 Top3', 'Current MC Top3')}:")
for i, t in enumerate(top3_mc, 1):
mig_label = _('已迁移', 'migrated') if t.get('is_open') else _('未迁移', 'unmigrated')
print(f" {i}. {t.get('symbol','?')} {usd(float(t.get('market_cap') or 0))} [{mig_label}]")
ath_info = created_data.get('creator_ath_info') or {}
if ath_info and ath_info.get('ath_mc'):
is_curr = ath_info.get('ath_token','').lower()==TOKEN_ADDR.lower()
curr_label = _('(本币)', ' (this token)') if is_curr else ''
print(f" {_('历史最高市值', 'All-time high MC')}: {ath_info.get('token_name','')}({ath_info.get('token_symbol','?')}){curr_label} ATH {usd(float(ath_info.get('ath_mc') or 0))}")
print()
sec3 = _("🔗 关联资金", "🔗 Related Funds")
print(f"━━ {sec3} {''*(54-len(sec3))}")
print()
print(f" {_('多个钱包来自同一资金来源地址,或在极短时间内同步注资', 'Multiple wallets from same funding source or funded in tight time windows')}")
print()
if related:
relf = "🔴" if related_pct>0.15 else ("🟡" if related_pct>0.05 else "🟢")
print(f" {_('涉及', 'Involves')} {len(related)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(related_pct):.2f}% {usd(related_usd)} {relf}")
print()
print(f" ├─ {_('同一资金来源地址', 'Same funding source')} {len(same_src_groups)} {_('', 'groups')} / {same_src_wallets} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(same_src_pct):.2f}%")
if same_src_groups:
fa, ws = same_src_groups[0]
native_in = sum(float((w.get('native_transfer') or {}).get('amount',0) or 0) for w in ws)
native_sym = 'SOL' if CHAIN=='sol' else ('BNB' if CHAIN=='bsc' else 'ETH')
print(f"{_('最大组', 'Largest group')}: {len(ws)} {_('个钱包', 'wallets')} {_('同一地址先后转入启动资金', 'funded sequentially from same addr')} {native_in:.4f} {native_sym}")
print(f"")
if win_groups:
win_total = sum(len(v) for __,v in win_groups)
if win_total>=3:
k,v = win_groups[0]
print(f" └─ {_('同期注资(30min内集中入场)', 'Coordinated funding (within 30min)')} {win_total} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(win_pct):.2f}% ⚠️")
print(f" {_('最大批次', 'Largest batch')}: {len(v)} {_('个钱包', 'wallets')} {_('合计持仓', 'total hold')} {pct(sum(h['amount_percentage'] for h in v)):.3f}%")
else:
print(f" └─ {_('同期注资(30min内集中入场)', 'Coordinated funding (within 30min)')} {win_total} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(win_pct):.2f}%")
else:
print(f" └─ {_('未发现同期集中注资', 'No coordinated funding detected')}")
else:
print(f" {_('未发现明显关联资金', 'No significant linked funds detected')} 🟢")
print()
sec4 = _("🧠 优质信号", "🧠 Quality Signals")
print(f"━━ {sec4} {''*(54-len(sec4))}")
print()
print(f" {_('聪明钱', 'Smart Money')} {len(smart):2d} {_('持仓', 'hold')} {pct(smart_pct):.2f}% {'' if smart else ''}")
if smart: print(f" {_('近期动向', 'Recent')}: {trend_str(smart)}")
print(f" KOL {len(kol):2d} {_('持仓', 'hold')} {pct(kol_pct):.2f}% {'' if kol else ''}")
if kol:
for h in kol:
name = h.get('twitter_name') or h.get('name') or addr_short(h['address'])
print(f" · {name} {_('持仓', 'hold')} {pct(h['amount_percentage']):.2f}% {holding_status(h)} {_('买/卖', 'buy/sell')}: {h.get('buy_tx_count_cur',0)}/{h.get('sell_tx_count_cur',0)}")
print(f" {_('鲸鱼', 'Whale')} {len(whales):2d} {_('持仓', 'hold')} {pct(whale_pct):.2f}% {'' if whales else ''}")
if whales: print(f" {_('近期动向', 'Recent')}: {trend_str(whales)}")
print()
df = "" if diamond_pct>0.5 else ("🟡" if diamond_pct>0.3 else "⚠️")
print(f" {_('钻石手(从未卖出)', 'Diamond hands (never sold)')} {len(diamond):2d} {_('持仓', 'hold')} {pct(diamond_pct):.1f}% {df}")
print(f" {_('部分卖出(<50%)', 'Partial sell (<50%)')} {len(partial):2d} {_('大量卖出(≥50%)', 'Heavy sell (≥50%)')} {len(heavy_sell):2d}")
sig_count = len(smart) + len(kol) + len(whales)
kol_selling = [h for h in kol if (h.get('sell_tx_count_cur') or 0) > 0]
kol_holding = [h for h in kol if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
smart_selling = [h for h in smart if (h.get('sell_tx_count_cur') or 0) > 0]
smart_holding = [h for h in smart if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
if sig_count == 0:
sig_summary = _("🟡 没有聪明钱和KOL,这个币没什么外部背书",
"🟡 No smart money or KOL — no external endorsement")
elif kol_selling and len(kol_selling) >= max(1, len(kol)//2+1):
sig_summary = _(f"🟡 {len(kol)}个KOL里有{len(kol_selling)}个已开始卖——跟进要小心",
f"🟡 {len(kol_selling)}/{len(kol)} KOL(s) already selling — be careful following")
elif smart_selling and len(smart_selling) >= max(1, len(smart)//2+1):
sig_summary = _(f"🟡 聪明钱里有{len(smart_selling)}个已经在出货,信号在减弱",
f"🟡 {len(smart_selling)} smart money wallet(s) selling — signal weakening")
elif smart_holding:
sig_summary = _(f"🟢 {len(smart_holding)}个聪明钱一直持仓没卖,这类人通常提前判断,信号较强",
f"🟢 {len(smart_holding)} smart money wallet(s) holding firm — usually early movers, strong signal")
elif kol_holding:
sig_summary = _(f"🟢 {len(kol_holding)}个KOL全程持仓未卖,参考价值保留",
f"🟢 {len(kol_holding)} KOL(s) fully holding — signal still valid")
else:
sig_summary = _("🟢 鲸鱼在场,有大资金背书",
"🟢 Whales present — backed by large capital")
print(f"{_('小结', 'Summary')}{_('', ': ')}{sig_summary}")
print()
sec5 = _("📈 入场成本分析", "📈 Entry Cost Analysis")
print(f"━━ {sec5} {''*(54-len(sec5))}")
print()
print(f" {_('当前 MC', 'Current MC')} {usd(cur_mc)}")
print()
time_clusters = defaultdict(list)
for h in normal:
sh = h.get('start_holding_at') or 0
if sh<=0: continue
time_clusters[(sh-token_launch)//86400].append(h)
sig_clusters = sorted([(age,ws) for age,ws in time_clusters.items() if len(ws)>=2],
key=lambda x: -sum(h['amount_percentage'] for h in x[1]))[:4]
for rank, (age, ws) in enumerate(sig_clusters, 1):
entry_ts = token_launch + age*86400
label = age_label(entry_ts)
total_hp = sum(h['amount_percentage'] for h in ws)
costed = [h for h in ws if (h.get('avg_cost') or 0)>0]
selling_out = [h for h in ws if holding_status(h) in (
_('🔴 大量出货','🔴 Heavy Selling'), _('🟡 出货中','🟡 Selling'))]
still_hold = [h for h in ws if (h.get('balance') or 0)>=1]
if costed:
avg_entry = sum(total_supply*h['avg_cost'] for h in costed)/len(costed)
roi = (cur_mc-avg_entry)/avg_entry*100 if avg_entry>0 else 0
mc_str = _(f"建仓MC {usd(avg_entry)} → 现 {usd(cur_mc)} ({roi:+.0f}%)",
f"Entry MC {usd(avg_entry)} → now {usd(cur_mc)} ({roi:+.0f}%)")
else:
avg_entry=0; roi=0
mc_str = _("建仓MC 未知(转账获得)", "Entry MC unknown (received via transfer)")
sell_ratio = len(selling_out)/len(still_hold) if still_hold else 0
if roi>500 and sell_ratio>0.15:
risk_flag = "🔴"
conclusion = _(f"这批人建仓时才 {usd(avg_entry)},涨了 {roi:.0f}%,现在有 {len(selling_out)} 个在卖出套现——追入容易接到他们的盘",
f"Entry at {usd(avg_entry)}, up {roi:.0f}%, {len(selling_out)} already selling — buying now means catching their exits")
elif roi>200 and sell_ratio>0.1:
risk_flag = "🟡"
conclusion = _(f"涨了 {roi:.0f}%,有 {len(selling_out)} 个开始出货,但多数还没动——注意大户下一步动向",
f"Up {roi:.0f}%, {len(selling_out)} starting to sell but most still holding — watch whale next moves")
elif roi>0:
risk_flag = "🟢"
conclusion = _(f"涨了 {roi:.0f}%,出货的人不多,短期卖压不大",
f"Up {roi:.0f}%, few selling — limited short-term pressure")
else:
risk_flag = "🟢"
conclusion = _(f"这批人目前亏着呢({roi:.0f}%),不到割肉的程度,短期不太会卖",
f"Currently down {roi:.0f}% — unlikely to sell at a loss short-term")
batch_label = _('批次', 'Batch')
selling_cnt = len([h for h in ws if is_selling(h)])
holding_cnt = len([h for h in ws if is_buying_only(h)])
sell_str = f" 🚨 {_('出货中','selling')} {selling_cnt}" if selling_cnt else ""
hold_str = f" 📈 {_('加仓中','accumulating')} {holding_cnt}" if holding_cnt else ""
print(f" {batch_label}{rank}{_('', '(')}{label}{_('', ')')} {len(ws)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(total_hp):.2f}% {risk_flag}{sell_str}{hold_str}")
print(f" {mc_str}")
print(f"{conclusion}")
print()
sec6 = _("💰 持仓者购买力", "💰 Holder Buying Power")
print(f"━━ {sec6} {''*(54-len(sec6))}")
print()
print(f" {_('衡量现有持仓者还有多少子弹可以加仓', 'How much ammo holders have left to add')} {_('合计可用余额', 'Total balance')} {usd(total_buying_power)}")
print()
if zero_wallets:
print(f"{_('零余额', 'Zero balance')} {len(zero_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(zero_pct_val):.2f}% $0 → {_('无加仓能力,可能是分仓小号', 'No buying power, likely sub-wallets')}")
if low_wallets:
low_total = sum(native_usd(h) for h in low_wallets)
print(f" 🟡 {_('低(<$200', 'Low (<$200)')} {len(low_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(low_pct_val):.2f}% {usd(low_total)}")
if mid_wallets:
mid_total = sum(native_usd(h) for h in mid_wallets)
print(f" 🟠 {_('中($200~$1200', 'Mid ($200~$1200)')} {len(mid_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(mid_pct_val):.2f}% {usd(mid_total)}")
if high_wallets:
print(f" 🔴 {_('高($1200+', 'High ($1200+)')} {len(high_wallets):3d} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(high_pct_val):.2f}% {usd(high_total)}{_('可随时加仓', 'can add anytime')}")
print()
if high_wallets:
print(f"{_('高余额钱包', 'High-balance wallets')} {len(high_wallets)} {_('个持仓', 'holding')} {pct(high_pct_val):.1f}%{_('', ', ')}{_('合计', 'total')} {usd(high_total)} {_('可随时加仓', 'ready to add')}")
if zero_wallets and zero_pct_val > 0.05:
print(f"{len(zero_wallets)} {_('个钱包零余额(持仓 ', 'wallets with zero balance (hold ')}{pct(zero_pct_val):.1f}%{_('', ')')}, {_('无加仓能力,可能是分仓小号', 'no buying power, likely sub-wallets')}")
print()
sec7 = _("📊 筹码结构", "📊 Chip Structure")
print(f"━━ {sec7} {''*(54-len(sec7))}")
print()
total_n = len(normal)
if total_n > 0:
print(f" {_('盈利', 'Profit')} {len(profit_w)} ({len(profit_w)/total_n*100:.0f}%) {_('亏损', 'Loss')} {len(loss_w)} ({len(loss_w)/total_n*100:.0f}%) {_('持平', 'Break-even')} {total_n-len(profit_w)-len(loss_w)}")
tf_flag = "⚠️" if len(trapped)>30 else ""
print(f" {_('套牢盘(浮亏>20%', 'Underwater (>20% loss)')} {len(trapped)} {_('持仓', 'hold')} {pct(sum(h['amount_percentage'] for h in trapped)):.2f}% {tf_flag}")
print(f" {_('平均持仓时长', 'Avg hold duration')} {avg_hold_days:.1f} {_('', 'days')}")
print()
sec8 = _("🤖 AI 建议", "🤖 AI Advice")
print(f"━━ {sec8} {''*(54-len(sec8))}")
print()
print(f" {rating_em} {rating_text}")
print()
if dangers:
print(f" {_('核心风险', 'Core Risks')}:")
for d in dangers: print(f" · {d}")
if warns:
print(f" {_('注意信号', 'Warnings')}:")
for w in warns: print(f" · {w}")
if goods:
print(f" {_('积极因素', 'Positives')}:")
for g in goods: print(f" · {g}")
hf = "🔴" if healthy_ratio<0.3 else ("🟡" if healthy_ratio<0.5 else "🟢")
print(f"\n {_('健康筹码', 'Healthy chips')} {pct(healthy_ratio):.1f}% {hf} DEX {pct(dex_pct):.1f}% {_('不纳入评估', 'excluded from eval')}")
print()
print(f" {_('关注以下信号,出现则考虑离场:', 'Watch for these exit signals:')}")
for sig in exit_signals:
print(f" · {sig}")
print()
print("=" * 58)
print(" [OUTPUT COMPLETE — COPY ABOVE VERBATIM, DO NOT SUMMARIZE]")
print("=" * 58)
+9 -6
View File
@@ -1,7 +1,7 @@
---
name: gmgn-market
description: Get crypto and meme token price charts (K-line, candlestick, OHLCV), trending meme coin rankings by volume, newly launched tokens on launchpads (pump.fun, fourmeme, letsbonk, Raydium, etc.), and the hot-search ranking (most-searched tokens) via GMGN API on Solana, BSC, Base, or Ethereum. Use when user asks for price chart, trending tokens, what's pumping, hot coins, most searched tokens, new launches, token signals, or wants to discover early-stage opportunities.
argument-hint: "kline --chain <sol|bsc|base|eth|robinhood> --address <token_address> --resolution <30s|1m|5m|15m|1h|4h|1d> [--from <unix_ts>] [--to <unix_ts>] | trending --chain <sol|bsc|base|eth|robinhood> --interval <1m|5m|1h|6h|24h> | trenches --chain <sol|bsc|base|eth|robinhood> | signal --chain <sol|bsc> | hot-searches [--chain <sol|bsc|base|eth|robinhood...>] [--interval <1m|5m|1h|6h|24h>]"
argument-hint: "kline --chain <sol|bsc|base|eth|robinhood> --address <token_address> --resolution <30s|1m|5m|15m|1h|4h|1d> [--from <unix_ts>] [--to <unix_ts>] | trending --chain <sol|bsc|base|eth|robinhood> --interval <1m|5m|1h|6h|24h> | trenches --chain <sol|bsc|base|eth|robinhood> | signal --chain <sol|bsc|robinhood> | hot-searches [--chain <sol|bsc|base|eth|robinhood...>] [--interval <1m|5m|1h|6h|24h>]"
metadata:
cliHelp: "gmgn-cli market --help"
---
@@ -50,12 +50,12 @@ Use the `gmgn-cli` tool to query K-line data for a token, browse trending tokens
| `market kline` | Token candlestick / OHLCV data and trading volume over a time range |
| `market trending` | Trending tokens ranked by swap activity — use `--interval` to specify the time window (e.g. `1m` for 1-minute hottest, `1h` for 1-hour trending) |
| `market trenches` | Newly launched launchpad platform tokens — **use this when the user asks for "new tokens", "just launched tokens", "latest tokens on pump.fun/letsbonk"**. Three categories: `new_creation` (just created), `near_completion` (bonding curve almost full), `completed` (graduated to open market / DEX) |
| `market signal` | Real-time token signal feed — price spikes, smart money buys, large buys, Dex ads, CTO events, and more. Results sorted by `trigger_at` descending. **sol / bsc only. Max 50 results per group.** |
| `market signal` | Real-time token signal feed — price spikes, smart money buys, large buys, Dex ads, CTO events, and more. Results sorted by `trigger_at` descending. **sol / bsc / robinhood only. Max 50 results per group.** |
| `market hot-searches` | Hot-search ranking — the most-searched tokens, ranked by `visiting_count` (search heat). **Use this when the user asks "what tokens are people searching for", "most searched tokens", "hot search list", "热搜榜".** Supports multiple chains in a single request. |
## Supported Chains
`sol` / `bsc` / `base` / `eth` / `robinhood` (kline / trending / trenches; signal: `sol` / `bsc` only; hot-searches: `sol` / `bsc` / `base` / `eth` / `robinhood`)
`sol` / `bsc` / `base` / `eth` / `robinhood` (kline / trending / trenches; signal: `sol` / `bsc` / `robinhood`; hot-searches: `sol` / `bsc` / `base` / `eth` / `robinhood`)
## Prerequisites
@@ -927,7 +927,7 @@ Present each category separately with a header:
## `market signal` Parameters
Chains: `sol` / `bsc` only. **Maximum 50 results per group** — use multiple groups via `--groups` to cover different signal types in a single request.
Chains: `sol` / `bsc` / `robinhood` only. **Maximum 50 results per group** — use multiple groups via `--groups` to cover different signal types in a single request.
**Single-group (individual flags):**
@@ -936,7 +936,7 @@ Do **not** pass signal types **14, 15, or 16** in `signal_type` / `--signal-type
| Option | Required | Description |
|--------|----------|-------------|
| `--chain` | Yes | `sol` / `bsc` |
| `--signal-type` | No | Signal type(s), repeatable (118, default: all). See Signal Types below. |
| `--signal-type` | No | Signal type(s), repeatable (121, default: all). See Signal Types below. |
| `--mc-min` | No | Min market cap at trigger time (USD) |
| `--mc-max` | No | Max market cap at trigger time (USD) |
| `--trigger-mc-min` | No | Min market cap at signal trigger moment (USD) |
@@ -977,6 +977,9 @@ gmgn-cli market signal --chain sol \
| 16 | SignalTypeMultiLargeBuy | Multiple large buys |
| 17 | SignalTypeBagsClaims | Bags Claim |
| 18 | SignalTypePumpClaims | Pump Claim |
| 19 | SignalTypePlatformCallV2 | Platform call (V2) |
| 20 | SignalTypeKOLBuy | KOL buy |
| 21 | SignalTypeBankerClaims | Banker Claim (Base chain Banker platform claim fee) |
### `market signal` Response Fields
@@ -986,7 +989,7 @@ Each item in the response array is one signal event:
|-------|------|-------------|
| `id` | string | Signal event ID |
| `token_address` | string | Token contract address |
| `signal_type` | number | Signal type (118, see Signal Types above) |
| `signal_type` | number | Signal type (121, see Signal Types above) |
| `trigger_at` | number | Unix timestamp (seconds) when the signal was triggered |
| `trigger_mc` | number | Market cap at signal trigger time (USD) |
| `first_trigger_mc` | number | Market cap at the very first trigger for this token (USD) |
+1 -3
View File
@@ -1,7 +1,7 @@
---
name: gmgn-track
description: Get real-time crypto buy/sell activity from Smart Money wallets, KOL influencer wallets, and personally followed wallets via GMGN API — alpha signals, whale tracking, meme token copy-trading ideas on Solana, BSC, Base, or Ethereum. Also query which tokens a wallet has followed (bookmarked) on GMGN. Use when user asks what smart money or KOLs are buying, wants whale alerts, on-chain alpha, copy-trade signals, or wants to check a wallet's followed tokens. (For a specific wallet address's portfolio, use gmgn-portfolio.)
argument-hint: "<follow-tokens|follow-wallet|kol|smartmoney> --chain <sol|bsc|base|eth> [--wallet <wallet_address>]"
argument-hint: "<follow-tokens|follow-wallet|kol|smartmoney> --chain <sol|bsc|base|eth|robinhood> [--wallet <wallet_address>]"
metadata:
cliHelp: "gmgn-cli track --help"
---
@@ -61,8 +61,6 @@ Use the `gmgn-cli` tool to query on-chain tracking data based on the user's requ
`sol` / `bsc` / `base` / `eth` / `robinhood`
Note: `track kol` and `track smartmoney` do **not** support `robinhood` — they accept `sol` / `bsc` / `base` / `eth` only.
## Prerequisites
- `gmgn-cli` installed globally — if missing, run: `npm install -g gmgn-cli`
+552
View File
@@ -0,0 +1,552 @@
---
name: gmgn-wallet-score
description: Score any wallet address across three angles — profitability (a real track-record score: is this trader actually good?), copy-tradeability (can YOU actually capture what it makes, plus a latency/slippage/gas backtest), and Dev reputation (if it's mostly a token launcher, how trustworthy are its launches) — plus trading-style tags, all computed deterministically from GMGN portfolio data. Use when the user asks about a wallet's profitability ("这个钱包盈利能力怎么样", "钱包战绩怎么样", "is this wallet profitable"), copy-trade worthiness ("is this wallet worth following", "跟单评分", "钱包评分", "值不值得跟单", "if I copy this wallet what's my real return"), or token-launch/Dev reputation ("这个钱包发盘情况怎么样", "是不是发币方钱包", "dev 信誉怎么样", "is this a token-creator wallet"), or gives a wallet address and wants any of these judgments.
argument-hint: "--chain <sol|bsc|base|eth|robinhood> --wallet <wallet_address> [--latency <seconds>] [--slippage <pct>] [--gas <usd>] [--sample <n>]"
metadata:
cliHelp: "gmgn-cli portfolio stats --help && gmgn-cli portfolio activity --help && gmgn-cli portfolio created-tokens --help && gmgn-cli token security --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands. Do NOT use web search, WebFetch, curl, or visit gmgn.ai — the website requires login and does not expose structured data.**
**IMPORTANT: Do NOT guess field names or values. Fields not listed in [Field Reference](#field-reference) below have not been confirmed against the live API — if the script's defensive lookups come back empty for them, degrade gracefully (see [Notes](#notes)) rather than inventing a value.**
**⚠️ IPv6 NOT SUPPORTED: If you get a `401` or `403` error and credentials look correct, check for IPv6 immediately: (1) list all network interfaces and their IPv6 addresses — run `ifconfig | grep inet6` (macOS) or `ip addr show | grep inet6` (Linux); (2) send a test request to `https://ipv6.icanhazip.com` — if the response is an IPv6 address, outbound traffic is going via IPv6. Tell the user immediately: "Please disable IPv6 on your network interface — gmgn-cli commands only work over IPv4."**
When the user gives a wallet address, extract `--chain` and `--wallet` from their message, then run the analysis script below — it computes all three angles (profitability, copy-tradeability, Dev reputation) from a single shared data pull, regardless of which one the user actually asked about. Also detect the user's language: set `LANG` to `'zh'` if the user wrote in Chinese, `'en'` if in English (default `'zh'`). Ask for `--latency` / `--slippage` / `--gas` only if the user wants a customized backtest — otherwise use the defaults baked into the script. Once the script runs, decide which section to lead with in your reply — see [Framing the Report by Question](#framing-the-report-by-question) below.
## Core Concepts
This skill deliberately splits "is this trader good?" from "can you copy it?" — a wallet can be great at trading and terrible to copy, or mediocre at trading but easy to ride along with:
- **Track-record score (0100)** — Is this trader actually good, judged by outcome distribution, not just win rate. A wallet with a 30% win rate can still score high here if it cuts losses hard and the 30% that win pay for the rest — that's disciplined risk management, not luck.
- **Copy-tradeability score (0100)** — Even if the wallet is genuinely skilled, can *you* capture that edge? A wallet that snipes sub-$100k market caps or round-trips in 5 seconds is not copyable — by the time your transaction lands, the wallet has already exited and you're the exit liquidity. This score penalizes early entries, thin per-trade margins, ultra-short holds, and bot-tier trade frequency.
- **Backtest estimate** — A concrete "what you'd actually keep" number: the wallet's raw return minus your entry-latency price drift, minus round-trip slippage, minus gas — using the latency/slippage/gas the user specifies (or sane defaults).
- **Dev-reputation score** — Only computed when the wallet is itself a token creator (its `created_token_count` exceeds half its traded-token count — i.e. it's mostly launching, not trading). Judges the wallet as a *developer*: survival rate of tokens it created, how many are stuck on the bonding curve and never graduated, and whether its recent launches passed a basic security check. When this applies, entry-timing / win-rate factors are unreliable (the wallet controls its own token's price and holder list) — the track-record and copy-tradeability scores are shown at a steep discount, and the verdict pivots to Dev reputation instead.
- **No trading history** — If the wallet has zero buy/sell transactions in the sampled period (only transfers/airdrops), none of the above can be computed honestly. The script detects this and reports it plainly instead of forcing a score onto empty data.
## Framing the Report by Question
This skill answers three related but distinct questions from **one shared data pull** — always run the full [Analysis Script](#analysis-script) regardless of which angle the user asked about; the underlying `portfolio stats` / `activity` / `created-tokens` / `token security` calls don't change, only what you lead with in your reply does. Never re-run the script per angle — that just burns extra rate-limit budget for data you already have.
| User's question | Lead with | Still mention, briefer |
|---|---|---|
| Profitability — "这个钱包盈利能力怎么样", "钱包战绩怎么样", "is this wallet profitable" | 🎯 Track-Record Score + factors | Style tags. Skip the backtest and Dev section unless the wallet turns out to be a Dev (see below) |
| Copy-trade worthiness — "值不值得跟单", "is this wallet worth copying", "if I copy this wallet what's my real return" | 🚀 Copy-Tradeability Score + 🧮 Backtest | Track-Record score as context, and the final Verdict |
| Dev/launch reputation — "这个钱包发盘情况怎么样", "是不是发币方钱包", "dev 信誉怎么样", "is this a token-creator wallet" | 👨‍💻 Dev-Reputation section | That this discounts the other two scores (self-dealing) — no need to walk through their factors in detail |
| Generic — "帮我看看这个钱包", "钱包评分", an address with no specific angle | The full report, all sections | — |
**Safety override**: if the user asked a Profitability or Copy-tradeability question but the wallet turns out to be classified as a Dev wallet (`dev is not None`), still surface the Dev-Reputation section and the self-dealing discount notice up front — a wallet that mostly launches tokens needs that caveat regardless of which angle was asked, since it changes how much the other two scores can be trusted.
## Analysis Script
Run this Python script inline, replacing the `<FILL_IN_*>` placeholders with the actual values. `LATENCY_S` / `SLIPPAGE_PCT` / `GAS_USD` / `SAMPLE` default to `3.0` / `0.05` / `0.2` / `200` if the user didn't specify — pass those literals when unspecified.
```python
python3 << 'PYEOF'
import json, math, subprocess
CHAIN = "<FILL_IN_CHAIN>"
WALLET = "<FILL_IN_WALLET_ADDRESS>"
LANG = "<FILL_IN_LANG>" # 'zh' or 'en'
LATENCY_S = <FILL_IN_LATENCY> # seconds you'd lag entering after this wallet (default 3.0)
SLIPPAGE_PCT = <FILL_IN_SLIPPAGE> # one-sided slippage fraction, e.g. 0.05 = 5% (default 0.05)
GAS_USD = <FILL_IN_GAS> # your cost per trade in USD (default 0.2)
SAMPLE = <FILL_IN_SAMPLE> # activity rows to sample, max 400 (default 200)
ZH = (LANG == 'zh')
def _(zh, en): return zh if ZH else en
def run_cli(args, timeout=30):
r = subprocess.run(['gmgn-cli'] + args + ['--raw'], capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise RuntimeError(r.stderr)
return json.loads(r.stdout)
def unwrap(resp):
# gmgn-cli --raw already unwraps to the data object; tolerate either shape.
return resp.get('data', resp) if isinstance(resp, dict) and 'data' in resp else resp
def _f(v, default=0.0):
try: return float(v)
except (TypeError, ValueError): return default
def _clamp(x, lo=0.0, hi=1.0):
return lo if x < lo else hi if x > hi else x
def _b(v):
if isinstance(v, bool): return v
if isinstance(v, (int, float)): return v != 0
if isinstance(v, str): return v.strip().lower() in ('1', 'true', 'yes')
return False
def fmt_dur(sec):
sec = _f(sec)
if sec < 60: return f"{int(sec)}{_(' 秒','s')}"
if sec < 3600: return f"{round(sec/60)}{_(' 分','m')}"
if sec < 86400: return f"{round(sec/3600,1)}{_(' 小时','h')}"
return f"{round(sec/86400,1)}{_(' 天','d')}"
def usd(v):
v = _f(v)
if abs(v) >= 1_000_000: return f"${v/1_000_000:.2f}M"
if abs(v) >= 1_000: return f"${v/1_000:.1f}K"
return f"${v:.2f}"
# ── 1. Trading stats (7D) ─────────────────────────────────
stats_raw = unwrap(run_cli(['portfolio', 'stats', '--chain', CHAIN, '--wallet', WALLET, '--period', '7d']))
pnl = stats_raw.get('pnl_stat') or {}
common = stats_raw.get('common') or {}
buy = int(_f(stats_raw.get('buy', stats_raw.get('buy_count'))))
sell = int(_f(stats_raw.get('sell', stats_raw.get('sell_count'))))
trades = buy + sell
token_num = int(_f(pnl.get('token_num')))
dist = dict(
gt_5 = int(_f(pnl.get('pnl_gt_5x_num'))),
x2_5 = int(_f(pnl.get('pnl_2x_5x_num'))),
x0_2 = int(_f(pnl.get('pnl_0x_2x_num'))),
n50_0 = int(_f(pnl.get('pnl_nd5_0x_num'))),
lt_n50 = int(_f(pnl.get('pnl_lt_nd5_num'))),
)
realized_profit = _f(stats_raw.get('realized_profit'))
bought_cost = _f(stats_raw.get('bought_cost', stats_raw.get('total_cost')))
created_token_count = int(_f(common.get('created_token_count')))
w = dict(
realized_profit=realized_profit,
roi=_f(stats_raw.get('realized_profit_pnl', stats_raw.get('pnl'))),
buy=buy, sell=sell, trades=trades,
bought_cost=bought_cost,
avg_buy_usd=(bought_cost / buy if buy else 0.0),
avg_trade_usd=(realized_profit / sell if sell else 0.0),
token_num=token_num, winrate=_f(pnl.get('winrate')), dist=dist,
avg_hold_s=_f(pnl.get('avg_holding_period')),
created_token_count=created_token_count,
identity=common.get('twitter_name') or common.get('name') or common.get('ens') or '',
)
if trades == 0:
print(_( "该地址近 7 天没有真实买卖记录(可能是新钱包,或持有的代币是转入/空投所得)——无法评估战绩,跳过打分。",
"This address has no real buy/sell activity in the last 7 days (may be a new wallet, or its holdings were transferred/airdropped in) — not enough data to score, skipping."))
raise SystemExit(0)
# ── 2. Activity sample (paginated, most-recent-first) ─────
def sample_activity(target):
target = max(20, min(int(target or 200), 400))
acts, cursor = [], None
for _try in range(4):
args = ['portfolio', 'activity', '--chain', CHAIN, '--wallet', WALLET,
'--limit', str(min(100, target - len(acts)))]
if cursor: args += ['--cursor', str(cursor)]
raw = unwrap(run_cli(args))
page = raw.get('activities') or []
acts.extend(page)
cursor = raw.get('next')
if not cursor or not page or len(acts) >= target:
break
return acts[:target]
acts = sample_activity(SAMPLE)
def ev_type(a):
return (a.get('event_type') or a.get('type') or '').lower()
mcaps = []
for a in acts:
if ev_type(a) != 'buy':
continue
tok = a.get('token') or {}
supply = _f(tok.get('total_supply'))
px = _f(a.get('price_usd'))
if supply > 0 and px > 0:
mcaps.append(px * supply)
mcaps.sort()
has_mcap_data = bool(mcaps)
entry_under_100k = (sum(1 for m in mcaps if m < 100_000) / len(mcaps)) if mcaps else 0.0
median_entry_mcap = mcaps[len(mcaps) // 2] if mcaps else 0.0
by_tok = {}
for a in acts:
addr = (a.get('token') or {}).get('address')
by_tok.setdefault(addr, []).append(a)
pairs = fast = 0
for evs in by_tok.values():
evs = sorted(evs, key=lambda e: _f(e.get('timestamp')))
last_buy = None
for e in evs:
et = ev_type(e)
if et == 'buy':
last_buy = _f(e.get('timestamp'))
elif et == 'sell' and last_buy is not None:
pairs += 1
if _f(e.get('timestamp')) - last_buy <= 5:
fast += 1
last_buy = None
fast_flip_rate = round(fast / pairs, 4) if pairs else 0.0
gas_vals = [_f(a.get('gas_usd')) for a in acts if _f(a.get('gas_usd')) > 0]
has_gas_data = bool(gas_vals)
avg_gas_usd = round(sum(gas_vals) / len(gas_vals), 4) if gas_vals else 0.0
summ = dict(sampled=len(acts), entry_under_100k=round(entry_under_100k, 4),
median_entry_mcap=round(median_entry_mcap, 2),
fast_flip_rate=fast_flip_rate, avg_gas_usd=avg_gas_usd)
# ── 3. Dev-reputation (only if this wallet mostly launches, not trades) ──
DEV_SEC_SCAN_N = 3 # how many of its most recent launches to security-scan
is_dev_wallet = created_token_count > 0 and created_token_count > 0.5 * max(1, token_num)
dev = None
if is_dev_wallet:
try:
ct = unwrap(run_cli(['portfolio', 'created-tokens', '--chain', CHAIN, '--wallet', WALLET]))
toks = ct.get('tokens') or []
open_count = int(_f(ct.get('open_count')))
inner_count = int(_f(ct.get('inner_count')))
if toks or open_count or inner_count:
# alive = graduated to DEX AND still has real liquidity (not drained)
alive = sum(1 for t in toks if t.get('is_open') and _f(t.get('pool_liquidity')) >= 4000)
total = len(toks)
rug_rate = round((total - alive) / total, 3) if total else 0.0
ath_mc = _f((ct.get('creator_ath_info') or {}).get('ath_mc'))
recent = sorted(toks, key=lambda t: -_f(t.get('create_timestamp')))[:DEV_SEC_SCAN_N]
checked = unsafe = 0
for t in recent:
addr = t.get('token_address')
if not addr:
continue
try:
sec = unwrap(run_cli(['token', 'security', '--chain', CHAIN, '--address', addr]))
except Exception:
continue
checked += 1
bad = False
if str(sec.get('is_honeypot', '')).lower() == 'yes':
bad = True
elif CHAIN == 'sol':
if not _b(sec.get('renounced_mint')) or not _b(sec.get('renounced_freeze_account')):
bad = True
else:
if str(sec.get('open_source', '')).lower() == 'no':
bad = True
if bad:
unsafe += 1
sec_risk_rate = round(unsafe / checked, 3) if checked else 0.0
surv = (1.0 - rug_rate) if total else _clamp(_f(ct.get('open_ratio')))
ath_track = _clamp((math.log10(max(1.0, ath_mc)) - 5.0) / 2.0) # $100k→0, $10M→1
s = 0.25 + 0.55 * surv
s -= 0.30 * _clamp((inner_count - 50) / 950.0) # heavy bonding-curve pileup = factory
s += 0.15 * ath_track * surv # best-ever launch, gated by survival
s -= 0.35 * sec_risk_rate # recent launches failed a security check
dev = dict(open_count=open_count, inner_count=inner_count,
analyzed=total, alive=alive, rugged=max(0, total - alive),
rug_rate=rug_rate, ath_mc=ath_mc,
sec_checked=checked, sec_unsafe=unsafe, sec_risk_rate=sec_risk_rate,
score=round(_clamp(s), 3))
except Exception:
dev = None
# ── 4. Style tags ──────────────────────────────────────────
tags = []
def add_tag(emoji, zh, en):
tags.append(dict(emoji=emoji, text=_(zh, en)))
tn = max(1, token_num)
big_win = (dist['gt_5'] + dist['x2_5']) / tn
big_loss = dist['lt_n50'] / tn
early, flip = summ['entry_under_100k'], summ['fast_flip_rate']
if dev is not None:
pct_created = round(created_token_count / tn * 100)
cnt = created_token_count or dev.get('analyzed', 0)
rug = dev.get('rug_rate', 0.0)
if rug >= 0.5:
add_tag("🏭", f"发币方/Dev:发过 {cnt} 个币(占交易{min(pct_created,100)}%+),rug 率 {round(rug*100)}%(工厂号嫌疑)",
f"Token creator/Dev: launched {cnt} tokens (≥{min(pct_created,100)}% of traded tokens), rug rate {round(rug*100)}% (factory suspect)")
else:
add_tag("🏭", f"发币方/Dev:发过 {cnt} 个币(占交易{min(pct_created,100)}%+),看 Dev 信誉分",
f"Token creator/Dev: launched {cnt} tokens (≥{min(pct_created,100)}% of traded tokens) — check the Dev-reputation score")
add_tag("🏗️", f"自产自销:交易的币里 {min(pct_created,100)}%+ 是自己发的——进场时机/胜率对这类币没意义",
f"Self-dealer: ≥{min(pct_created,100)}% of traded tokens are its own launches — entry-timing/win-rate tags are meaningless here")
if trades >= 2000:
add_tag("🤖", f"机器人/科学家:7D {trades} 笔,只能机器跟", f"Bot/Quant: {trades} trades in 7D — only a bot can keep up")
if flip >= 0.3:
add_tag("⚡", f"闪电手:{round(flip*100)}% 的仓位 5 秒内买卖", f"Flash flipper: {round(flip*100)}% of positions bought & sold within 5s")
if dev is None and early >= 0.8:
add_tag("🎯", f"狙击手:{round(early*100)}% 进场市值 <$100k", f"Sniper: {round(early*100)}% of entries are <$100k mcap")
if w['avg_hold_s'] >= 5*86400 and trades < 200:
add_tag("💎", "钻石手:持仓久、下手少", "Diamond hands: holds long, trades rarely")
if w['avg_buy_usd'] >= 5000:
add_tag("🐋", f"巨鲸:单笔平均建仓 {usd(w['avg_buy_usd'])}", f"Whale: avg position size {usd(w['avg_buy_usd'])}")
if dev is None and w['winrate'] >= 0.65 and trades >= 15:
add_tag("🏆", f"高胜率:{round(w['winrate']*100)}% 的币最终是赚的", f"High win-rate: {round(w['winrate']*100)}% of tokens ended up profitable")
if 0 < w['avg_hold_s'] < 3600 and flip < 0.3 and trades >= 30:
add_tag("🐇", f"快枪手:平均持仓 {fmt_dur(w['avg_hold_s'])}", f"Quick-draw: avg hold {fmt_dur(w['avg_hold_s'])}")
if dev is None and 0 < summ['median_entry_mcap'] < 30000:
add_tag("🔦", f"冷门捡漏:中位进场市值仅 {usd(summ['median_entry_mcap'])}", f"Obscure hunter: median entry mcap only {usd(summ['median_entry_mcap'])}")
if w['realized_profit'] > 20000 and big_loss <= 0.05:
add_tag("📈", "真高手:净赚且极少大亏,止损纪律好", "True skill: net profitable with very few big losses")
elif big_loss >= 0.3 and w['realized_profit'] < 0:
add_tag("🩸", f"亏损韭菜:{round(big_loss*100)}% 的币亏超 50%,长期净亏", f"Bag holder: {round(big_loss*100)}% of tokens lost over 50%")
elif big_win >= 0.02 and w['winrate'] < 0.35 and w['realized_profit'] > 0:
add_tag("🎰", "赌狗打法:胜率低但靠少数暴击回本", "Gambler: low win-rate, carried by a few huge hits")
if trades < 60 and w['realized_profit'] > 0 and flip < 0.1:
add_tag("🐌", "慢工出细活:低频、可复制,最适合跟单", "Slow & steady: low frequency, repeatable — easiest to copy")
if not tags:
add_tag("🧭", "普通交易者:没有特别突出的风格标签", "Regular trader: no standout style tags")
# ── 5. Track-record score (is this trader actually good?) ──
TRACK_W = dict(tail=0.34, upside=0.28, roi=0.16, win=0.10, size=0.12)
tail_f = 1 - dist['lt_n50'] / tn
upside_f = (dist['gt_5'] + dist['x2_5'] + dist['x0_2']) / tn
roi_f = _clamp((w['roi'] + 0.05) / 0.35)
win_f = _clamp(w['winrate'] / 0.5)
size_f = _clamp((tn - 20) / 300)
track_facs = dict(tail=tail_f, upside=upside_f, roi=roi_f, win=win_f, size=size_f)
track_score = round(100 * sum(TRACK_W[k] * _clamp(v) for k, v in track_facs.items()))
TRACK_LABELS = dict(tail=_('止损纪律','Stop-loss discipline'), upside=_('盈利面','Profit share'),
roi=_('资金回报','Capital ROI'), win=_('胜率','Win rate'), size=_('样本量','Sample size'))
# ── 6. Copy-tradeability score (can YOU capture it?) ────────
COPY_W = dict(entry=0.22, profit=0.22, hold=0.20, feasible=0.18, edge=0.18)
entry_f = _clamp(0.12 + (1 - summ['entry_under_100k']))
profit_f = _clamp(w['avg_trade_usd'] / 80.0)
hold_f = _clamp((1 - summ['fast_flip_rate'] * 1.6) * _clamp(w['avg_hold_s'] / 172800 + 0.15))
feasible_f = _clamp(1 - w['trades'] / 2500.0)
edge_f = _clamp(1 - 0.6 * summ['entry_under_100k'] - 0.6 * summ['fast_flip_rate'])
copy_facs = dict(entry=entry_f, profit=profit_f, hold=hold_f, feasible=feasible_f, edge=edge_f)
copy_score = round(100 * sum(COPY_W[k] * v for k, v in copy_facs.items()))
COPY_LABELS = dict(entry=_('进场市值','Entry mcap'), profit=_('单笔利润空间','Profit per trade'),
hold=_('持仓 vs 延迟','Hold vs latency'), feasible=_('执行可行性','Execution feasibility'),
edge=_('优势类型','Edge type'))
# Self-dealing discount: for a dev wallet, entry-timing/win-rate factors are self-authored — steeply
# discount both display scores (not hidden, just flagged) rather than let them look falsely strong.
SELF_DEAL_DISCOUNT = 0.45
self_dealing = dev is not None
track_disp = round(track_score * SELF_DEAL_DISCOUNT) if self_dealing else track_score
copy_disp = round(copy_score * SELF_DEAL_DISCOUNT) if self_dealing else copy_score
# ── 7. Copy-trade backtest ───────────────────────────────────
wallet_pct = (w['realized_profit'] / w['bought_cost']) if w['bought_cost'] > 0 else w['roi']
wallet_pct = _clamp(wallet_pct or 0.0001, -0.9, 3.0) # clamp: dev wallets have near-zero bought_cost, ratio blows up
LOW_MCAP_DRIFT_PER_S = 0.015
drift_per_s = LOW_MCAP_DRIFT_PER_S * (0.3 + 0.7 * summ['entry_under_100k'])
drift = LATENCY_S * drift_per_s
slip = 2 * SLIPPAGE_PCT
gas_pct = (GAS_USD / w['avg_buy_usd']) if w['avg_buy_usd'] > 0 else 0.0
copy_pct = wallet_pct - drift - slip - gas_pct
copy_7d = w['realized_profit'] * (copy_pct / wallet_pct) if wallet_pct else 0.0
bt = dict(wallet_pct=round(wallet_pct,4), copy_pct=round(copy_pct,4), drift=round(drift,4),
slip=round(slip,4), gas_pct=round(gas_pct,4), wallet_7d=round(w['realized_profit'],1),
copy_7d=round(copy_7d,1), trap=round(w['realized_profit']-copy_7d,1))
# ── 8. Verdict ────────────────────────────────────────────────
ts, cs = track_disp, copy_disp
if dev is not None:
ds = round((dev.get('score') or 0) * 100)
if ds < 40:
v_emoji, v_text = "🔴", _( f"发币方钱包,Dev 信誉仅 {ds}/100(连环 rug / 存活率低)—— 别碰它发的新盘。",
f"Token-creator wallet, Dev reputation only {ds}/100 (serial-rug / low survival) — stay away from its new launches.")
else:
v_emoji, v_text = "🟡", _( f"发币方钱包,Dev 信誉 {ds}/100 —— 看它的存活率与安全记录再决定是否跟它的新盘。",
f"Token-creator wallet, Dev reputation {ds}/100 — check survival rate and security record before following its new launches.")
elif ts >= 65 and cs < 35:
v_emoji, v_text = "⚠️", _( "高战绩、低可跟单 —— 学它的止损纪律,别抄它的入场;延迟和滑点会把薄利吃成负。",
"High track record, low copy-tradeability — learn the stop-loss discipline, don't copy the entries; latency and slippage will turn thin profit negative.")
elif ts >= 60 and cs >= 55:
v_emoji, v_text = "🟢", _( "战绩真实且可跟单性高 —— 低频、进场不算太早,值得小额跟一跟验证。",
"Genuine track record and high copy-tradeability — low frequency, entries not too early, worth a small copy-trade to verify.")
elif ts < 40:
v_emoji, v_text = "🔴", _("战绩一般偏弱 —— 不建议作为跟单对象。", "Weak track record — not recommended as a copy-trade target.")
else:
v_emoji, v_text = "🟡", _( "战绩中等 —— 可观察,跟单前先小额验证延迟/滑点损耗。",
"Middling track record — worth watching; verify latency/slippage cost with a small trade before copying.")
# ══════════════════════════════════════════════════════════
# OUTPUT
# ══════════════════════════════════════════════════════════
title = _("跟单评分", "Copy-Trade Score")
print(f"┌{'─'*56}┐")
print(f"│{(' '+title):^56}│")
print(f"│{(' '+WALLET[:6]+'...'+WALLET[-4:]+' · '+CHAIN.upper()):^56}│")
if w['identity']:
print(f"│{(' '+w['identity']):^56}│")
print(f"└{'─'*56}┘")
print()
sec = _("📊 近7天战绩", "📊 7D Trading Stats")
print(f"━━ {sec} {'━'*(54-len(sec))}")
print(f" {_('已实现盈亏','Realized P&L')} {usd(w['realized_profit'])} ROI {w['roi']*100:+.1f}% "
f"{_('胜率','Win rate')} {w['winrate']*100:.0f}% {_('笔数','Trades')} {trades} ({buy}{_('买','buy')}/{sell}{_('卖','sell')})")
print(f" {_('交易币数','Tokens traded')} {token_num} {_('均持仓','Avg hold')} {fmt_dur(w['avg_hold_s'])} "
f"{_('均单笔建仓','Avg position')} {usd(w['avg_buy_usd'])}")
print()
sec = _("🏷️ 风格标签", "🏷️ Style Tags")
print(f"━━ {sec} {'━'*(54-len(sec))}")
for t in tags:
print(f" {t['emoji']} {t['text']}")
print()
if self_dealing:
print(f" ⚠️ {_('该钱包主要在发币而非交易——下面两个分已按 ×0.45 打折显示,仅供参考。','This wallet mostly launches tokens rather than trading — the two scores below are shown at a ×0.45 discount for reference only.')}")
print()
sec = _("🎯 真实战绩分(这交易员是不是真有本事)", "🎯 Track-Record Score (is this trader actually good?)")
print(f"━━ {sec} {'━'*(max(0,54-len(sec)))}")
print(f" {track_disp}/100")
for k, v in track_facs.items():
print(f" · {TRACK_LABELS[k]:14s} {round(100*_clamp(v)):3d}/100 ({_('权重','w')} {TRACK_W[k]:.0%})")
print()
sec = _("🚀 可跟单分(你跟进后能拿到多少)", "🚀 Copy-Tradeability Score (can YOU capture it?)")
print(f"━━ {sec} {'━'*(max(0,54-len(sec)))}")
print(f" {copy_disp}/100")
for k, v in copy_facs.items():
print(f" · {COPY_LABELS[k]:14s} {round(100*v):3d}/100 ({_('权重','w')} {COPY_W[k]:.0%})")
print()
sec = _("🧮 跟单回测", "🧮 Copy-Trade Backtest")
print(f"━━ {sec} {'━'*(max(0,54-len(sec)))}")
print(f" {_('假设','Assuming')}: {_('延迟','latency')} {LATENCY_S:.1f}s {_('单边滑点','one-sided slippage')} {SLIPPAGE_PCT:.1%} {_('每笔gas','gas/trade')} {usd(GAS_USD)}")
print(f" {_('钱包本人单笔收益率','Wallet per-trade return')} {bt['wallet_pct']*100:+.1f}%")
print(f" {_('- 延迟漂移','- latency drift')} {bt['drift']*100:.2f}pp {_('- 双边滑点','- round-trip slippage')} {bt['slip']*100:.2f}pp {_('- gas占比','- gas cost')} {bt['gas_pct']*100:.2f}pp")
print(f" {_('= 跟单后单笔收益率','= your per-trade return')} {bt['copy_pct']*100:+.1f}%")
print()
print(f" 7D {_('钱包本人','wallet')} {usd(bt['wallet_7d'])} → {_('跟单预估','copy estimate')} {usd(bt['copy_7d'])} ({_('抄单损耗','execution drag')} {usd(bt['trap'])})")
if not has_mcap_data:
print(f" ⚠️ {_('未取到进场市值数据,延迟漂移按中性假设估算,可能失真','Entry mcap data unavailable — latency drift uses a neutral assumption and may be inaccurate')}")
print()
if dev is not None:
sec = _("👨‍💻 Dev 信誉分(该钱包作为发币方)", "👨‍💻 Dev Reputation (as a token creator)")
print(f"━━ {sec} {'━'*(max(0,54-len(sec)))}")
ds = round((dev.get('score') or 0) * 100)
print(f" {ds}/100")
print(f" {_('已开外盘','Graduated')} {dev['open_count']} {_('卡在内盘','Stuck on curve')} {dev['inner_count']} "
f"{_('抽样存活率','Sampled survival')} {round((1-dev['rug_rate'])*100)}% ({dev['alive']}/{dev['analyzed']})")
print(f" {_('历史最高市值','All-time-high mcap')} {usd(dev['ath_mc'])}")
if dev['sec_checked']:
print(f" {_('最近发币安全扫描','Recent launches security scan')}: {dev['sec_unsafe']}/{dev['sec_checked']} {_('未过检','failed check')}")
print()
sec = _("✅ 结论", "✅ Verdict")
print(f"━━ {sec} {'━'*(max(0,54-len(sec)))}")
print(f" {v_emoji} {v_text}")
PYEOF
```
## Field Reference
Fields the script reads, confirmed against `portfolio stats` / `portfolio activity` / `portfolio created-tokens` / `token security` output (see [gmgn-portfolio](../gmgn-portfolio/SKILL.md) and [gmgn-token](../gmgn-token/SKILL.md) for the full reference):
| Source | Field | Meaning |
|--------|-------|---------|
| `portfolio stats` | `realized_profit`, `winrate`, `pnl_stat.token_num`, `pnl_stat.avg_holding_period` | Core outcome distribution |
| `portfolio stats` | `pnl_stat.pnl_gt_5x_num` / `pnl_2x_5x_num` / `pnl_0x_2x_num` / `pnl_nd5_0x_num` / `pnl_lt_nd5_num` | Bucketed P&L distribution: `>500%` / `200500%` / `0200%` / `-500%` / `<-50%` |
| `portfolio stats` | `common.created_token_count` | Used to detect a token-creator ("Dev") wallet |
| `portfolio activity` | `token.address`, `timestamp`, `price_usd` | Used for holding-duration and flip-rate detection |
| `portfolio created-tokens` | `open_count`, `inner_count`, `open_ratio`, `creator_ath_info.ath_mc` | Dev launch-history survival stats |
| `portfolio created-tokens` | `tokens[].is_open`, `tokens[].pool_liquidity`, `tokens[].create_timestamp`, `tokens[].token_address` | Per-launch alive/rugged classification |
| `token security` | `is_honeypot`, `renounced_mint`, `renounced_freeze_account` (SOL), `open_source` (EVM) | Recent-launch security scan for Dev reputation |
**Fields used defensively, not guaranteed present in every API response** — the script degrades gracefully (see [Notes](#notes)) rather than failing if these are absent:
| Field | Used for | Degrade behavior if missing |
|-------|----------|------------------------------|
| `token.total_supply` on activity rows | Entry market-cap estimate (`price_usd × total_supply`) | `entry_under_100k` / `median_entry_mcap` fall back to `0` — copy-tradeability's entry factor and backtest drift use a neutral assumption |
| `gas_usd` on activity rows | Average gas cost display | Falls back to `0`; the backtest still uses the user-specified `--gas` |
| `event_type` on activity rows | buy/sell classification | Falls back to the `type` field per the [gmgn-portfolio](../gmgn-portfolio/SKILL.md) reference |
## Scoring Rules Reference
**Track-record score** (0100, weighted sum) — rewards disciplined risk management over raw win rate:
| Factor | Weight | What it measures |
|--------|--------|-------------------|
| Stop-loss discipline | 34% | `1 (tokens down >50%) / total` |
| Profit share | 28% | Fraction of tokens that ended up net positive at any level |
| Capital ROI | 16% | `realized_profit / bought_cost`, normalized 5%→0, +30%→100 |
| Win rate | 10% | Normalized 0%→0, 50%→100 (low weight — a low win rate with great stop-loss discipline can still score well) |
| Sample size | 12% | Confidence discount for wallets with few distinct tokens traded (20→0, 320→100) |
**Copy-tradeability score** (0100, weighted sum) — penalizes styles that can't survive real-world latency:
| Factor | Weight | What it measures |
|--------|--------|-------------------|
| Entry mcap | 22% | Later entries score higher — sub-$100k entries mean you'd be buying after the wallet already has its position |
| Profit per trade | 22% | Thin average per-trade profit (< ~$3080) gets eaten by slippage and gas |
| Hold vs latency | 20% | Penalizes both high 5-second flip rates and very short average holds — you can't react that fast |
| Execution feasibility | 18% | Penalizes very high trade counts (bot-tier, > ~1000/week) — no human can keep pace |
| Edge type | 18% | Speed/scale-driven edges (early entries, fast flips) are not learnable/copyable; selection/timing edges are |
**Dev-reputation score** (0100, applies only when the wallet is a token creator) — survival-rate driven:
- Base: `0.25 + 0.55 × survival_rate` (survival = tokens still open with ≥$4,000 liquidity, or `open_ratio` if no per-token data)
- ` 0.30 ×` penalty for heavy bonding-curve pileup (`inner_count` beyond 50, capping at 1000 — a hallmark of factory/serial-launch wallets)
- `+ 0.15 ×` bonus for a strong all-time-high launch, gated by survival rate (a factory wallet's one lucky moonshot doesn't count)
- ` 0.35 ×` penalty for the fraction of recently-scanned launches that failed a basic security check
- **Self-dealing discount**: when a wallet is classified as a Dev (its own launches make up more than half its traded tokens), the track-record and copy-tradeability scores are shown at `× 0.45` — its own entry timing and win rate are self-authored, not a market read.
## Verdict Rules
| Condition | Verdict |
|-----------|---------|
| Wallet is a Dev, Dev-reputation < 40 | 🔴 Stay away from its new launches |
| Wallet is a Dev, Dev-reputation ≥ 40 | 🟡 Check survival rate / security record before following new launches |
| Track ≥65, Copy <35 | ⚠️ Learn the discipline, don't copy the entries |
| Track ≥60, Copy ≥55 | 🟢 Worth a small copy-trade to verify |
| Track <40 | 🔴 Not recommended as a copy-trade target |
| Otherwise | 🟡 Worth watching; verify latency/slippage cost with a small trade first |
## Supported Chains
`sol` / `bsc` / `base` / `eth` / `robinhood`
## Prerequisites
- `gmgn-cli` installed globally — if missing, run: `npm install -g gmgn-cli`
- `GMGN_API_KEY` configured in `~/.config/gmgn/.env` (exist auth only — no private key required for this skill)
## Rate Limit Handling
All routes this skill calls go through GMGN's leaky-bucket limiter with `rate=20` and `capacity=20`. Sustained throughput is roughly `20 ÷ weight` requests/second, and the max burst is roughly `floor(20 ÷ weight)` when the bucket is full. All of them use **exist auth** (API Key only, no private key needed).
| Command | Route | Weight |
|---------|-------|--------|
| `portfolio stats` | `GET /v1/user/wallet_stats` | 3 |
| `portfolio activity` | `GET /v1/user/wallet_activity` | 3 |
| `portfolio created-tokens` | `GET /v1/user/created_tokens` | 2 |
| `token security` | `GET /v1/token/security` | 1 |
A single run of this skill's script can burn through several of these in sequence: 1× `portfolio stats` + up to 4× `portfolio activity` (pagination) + 1× `portfolio created-tokens` + up to 3× `token security` (Dev security scan) — only the last two fire when the wallet is classified as a Dev. Account for that combined weight before batch-scoring several wallets back to back.
**When a request returns `429`, stop and proactively tell the user exactly when they can retry — never fail silently, never keep retrying without saying anything.**
- Extract the reset time: read `X-RateLimit-Reset` from the response headers (Unix timestamp), or if the response body contains `reset_at` (e.g., `{"code":429,"error":"RATE_LIMIT_BANNED","message":"...","reset_at":1775184222}`), use that instead — it's the Unix timestamp when the ban lifts (typically 5 minutes for a ban).
- Convert whichever timestamp you got to the user's local time and state it plainly, e.g. *"Rate-limited — you can retry this wallet after 14:32:05 (in ~4 minutes)."* Do this even if the run partially succeeded (see below) — the user needs to know when the rest of the analysis can resume.
- **Resume, don't restart**: if the script is mid-run (e.g. the Dev security scan hits `429` after `portfolio stats` and `activity` already succeeded), report what you already have (stats/tags/track-record score can still be shown), state the reset time for the remaining calls, and re-run only those remaining calls after it passes — don't re-fetch data you already have.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown extend the ban by 5 seconds each time, up to 5 minutes. Never loop retries — wait for the stated reset time before trying again.
- Scoring multiple wallets in one request (a leaderboard-style comparison): space the calls out or reduce `--sample` rather than firing all wallets' full analysis concurrently. If one wallet in the batch gets rate-limited, report the completed wallets immediately and tell the user when the rest will be ready, rather than holding the whole batch back silently.
## Notes
- This skill only reads data (`portfolio stats` / `activity` / `created-tokens`, `token security`) — it never executes a trade. For actually copy-trading, use [gmgn-swap](../gmgn-swap/SKILL.md) after this skill gives a 🟢 verdict.
- Scores over a 7-day window can be noisy for low-trade-count wallets — the sample-size factor in the track-record score already discounts this, but treat a score built on <10 trades as low-confidence and say so.
- The backtest (`--latency` / `--slippage` / `--gas`) is a rough estimate, not a precise simulation — actual slippage depends on the specific token's liquidity at the moment you'd have traded it.
- Dev-reputation is only computed when `created_token_count` exceeds half the wallet's traded-token count — a wallet that launched one token in passing while mostly trading normally will NOT be treated as a Dev, and its trading-style tags/scores apply as normal.
- Use `--raw` on any underlying `gmgn-cli` command to get single-line JSON if you want to inspect the raw response yourself before trusting a derived field.
## References
| Skill | Description |
|-------|--------------|
| [gmgn-portfolio](../gmgn-portfolio/SKILL.md) | Underlying `portfolio stats` / `activity` / `created-tokens` commands and full field reference |
| [gmgn-token](../gmgn-token/SKILL.md) | `token security` command used for the Dev-reputation security scan |
| [gmgn-track](../gmgn-track/SKILL.md) | Discover candidate wallets to score — Smart Money / KOL / followed-wallet trade feeds |
| [gmgn-swap](../gmgn-swap/SKILL.md) | Execute an actual copy-trade once this skill's verdict is 🟢 |
+3 -7
View File
@@ -22,8 +22,8 @@ export function registerCookingCommands(program: Command): void {
cooking
.command("create")
.description("Create a token on a launchpad platform (requires private key)")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base")
.requiredOption("--dex <dex>", "Launchpad: pump / bonk / bags (sol) / fourmeme / flap (bsc) / klik / clanker (base)")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / robinhood")
.requiredOption("--dex <dex>", "Launchpad: pump / bonk / bags (sol) / fourmeme / flap (bsc) / klik / clanker (base) / trench / pons (robinhood)")
.requiredOption("--from <address>", "Wallet address (must match API Key binding)")
.requiredOption("--name <name>", "Token name")
.requiredOption("--symbol <symbol>", "Token symbol")
@@ -44,7 +44,7 @@ export function registerCookingCommands(program: Command): void {
.option("--max-priority-fee-per-gas <amount>", "Max priority fee per gas in wei (EVM only)")
.option("--anti-mev", "Enable anti-MEV protection (SOL only)")
.option("--anti-mev-mode <mode>", "Anti-MEV mode: off / normal / secure (SOL only)")
.option("--raised-token <symbol>", "Raise token symbol: pump→USDC; bonk→USD1; fourmeme→USDT/USD1; leave empty for native")
.option("--raised-token <symbol>", "Raise token symbol: pump→USDC; bonk→USD1; fourmeme→USDT/USD1; base/robinhood→native only; leave empty for native")
.option("--dev-wallet-bps <n>", "Dev wallet fee in basis points (100 = 1%)", parseInt)
.option("--dev-gas <amount>", "Dev gas amount")
.option("--dev-priority <amount>", "Dev priority fee")
@@ -84,10 +84,6 @@ export function registerCookingCommands(program: Command): void {
process.exit(1);
}
validateChain(opts.chain);
if (opts.chain === "robinhood") {
console.error(`[gmgn-cli] cooking create does not support robinhood, got "${opts.chain}"`);
process.exit(1);
}
// Validate/clean all free-text and link metadata before publishing. This
// prevents the CLI from being used to mint tokens whose metadata carries a
// prompt-injection payload aimed at other users' AI agents.
+4 -4
View File
@@ -157,8 +157,8 @@ export function registerMarketCommands(program: Command): void {
market
.command("signal")
.description("Query token signals (price spikes, smart money buys, large buys, etc.) — max 50 results per group")
.requiredOption("--chain <chain>", "Chain: sol / bsc")
.option("--signal-type <n...>", "Signal type(s), repeatable: 118 (default: all types)", (v: string, acc: number[]) => { acc.push(parseInt(v, 10)); return acc; }, [] as number[])
.requiredOption("--chain <chain>", "Chain: sol / bsc / robinhood")
.option("--signal-type <n...>", "Signal type(s), repeatable: 121 (default: all types)", (v: string, acc: number[]) => { acc.push(parseInt(v, 10)); return acc; }, [] as number[])
.option("--mc-min <usd>", "Min market cap at trigger time (USD)", parseFloat)
.option("--mc-max <usd>", "Max market cap at trigger time (USD)", parseFloat)
.option("--trigger-mc-min <usd>", "Min market cap at signal trigger (USD)", parseFloat)
@@ -171,8 +171,8 @@ export function registerMarketCommands(program: Command): void {
.option("--raw", "Output raw JSON")
.action(async (opts: Record<string, unknown>) => {
validateChain(opts["chain"] as string);
if (!["sol", "bsc"].includes(opts["chain"] as string)) {
console.error(`[gmgn-cli] market signal only supports sol and bsc, got "${opts["chain"]}"`);
if (!["sol", "bsc", "robinhood"].includes(opts["chain"] as string)) {
console.error(`[gmgn-cli] market signal only supports sol, bsc and robinhood, got "${opts["chain"]}"`);
process.exit(1);
}
+2 -10
View File
@@ -76,16 +76,12 @@ export function registerTrackCommands(program: Command): void {
track
.command("kol")
.description("Get KOL trade records")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / eth")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / eth / robinhood")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--side <side>", "Filter by trade direction: buy / sell (client-side filter)")
.option("--raw", "Output raw JSON")
.action(async (opts) => {
if (opts.chain) validateChain(opts.chain);
if (opts.chain === "robinhood") {
console.error(`[gmgn-cli] track kol does not support robinhood, got "${opts.chain}"`);
process.exit(1);
}
const client = new OpenApiClient(getConfig());
const data = await client.getKol(opts.chain, opts.limit).catch(exitOnError) as { list?: { side: string }[] };
if (opts.side && data?.list) {
@@ -97,16 +93,12 @@ export function registerTrackCommands(program: Command): void {
track
.command("smartmoney")
.description("Get Smart Money trade records")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / eth")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / eth / robinhood")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--side <side>", "Filter by trade direction: buy / sell (client-side filter)")
.option("--raw", "Output raw JSON")
.action(async (opts) => {
if (opts.chain) validateChain(opts.chain);
if (opts.chain === "robinhood") {
console.error(`[gmgn-cli] track smartmoney does not support robinhood, got "${opts.chain}"`);
process.exit(1);
}
const client = new OpenApiClient(getConfig());
const data = await client.getSmartMoney(opts.chain, opts.limit).catch(exitOnError) as { list?: { side: string }[] };
if (opts.side && data?.list) {