* feat(wallet-analysis): add four-gate wallet copy-trade dossier skill Answers "should I copy this wallet, and what happens to me if I do?" with four independent pass/fail gates instead of a blended score: authenticity, currency (is the edge still working this week), reachability (can you get filled), survivability (does it cut losses). Each gate prints the single number that decided it, and the verdict is a pure function of the gates. analyze.py is read-only — portfolio stats/profits/activity/holdings/ created-tokens only, no signing. gen_fixtures.py builds eight synthetic wallets, each engineered to fail exactly one gate, so analyze.py can be verified offline with no API key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): stop letting third-party labels decide the verdict A live run on 0xa7d4ffc4eca3c71af150ce302560a9d04a1d2b9f returned `🔴 别碰` on a wallet with $459K of genuine realized profit. Two independent false positives caused it, and both were refutable from data already present in the same response. - `wash_trader` now has to be corroborated before it can veto G1. `conviction_share` measures the fraction of realized gains coming from positions that netted more than their own cost basis or cleared $1k per exit — self-dealing nets ~0 per round trip and cannot reach either bar. At >=50% the tag is demoted to a visible caution carrying the number that refuted it; below 50% G1 fails and prints the share; unmeasurable (no holdings) is now G1 ⚪, never a manufactured ❌. - `token.is_honeypot` is refuted by `history_total_sells > 0` on the same row. Seven "honeypots" failed G4 on one wallet; it had sold one of them 101 times. They were transfer-restricted tokenised-stock contracts. - Position scale is read from holdings, not from clip size. A wallet laddering a $54K position out of $3.4K clips was labelled "ordinary trading wallet" on `avg_buy_usd` alone. Report changes, from review feedback: - Identity moves above the gates and prints the full `x.com/<handle>`; a missing X account is stated rather than silently omitted. - New profit-engine attribution (撒网命中 / 周转磨利 / 选币重仓 / 分散积累) from trade cadence x gain concentration. A return figure alone does not say whether the edge is speed or selection, and the two are copied in completely different ways. - Verdict layer rewritten for plain language: a verb the reader can act on, everyday words for the cause, one imperative sentence for the action. `token_num < 5` is ⚪, not 🔴 — unmeasured is not measured-bad. G3 ❌ no longer short-circuits G4 ❌; both problems get stated. `put()` now budgets for the wider of prefix and `hang`, closing the one way a caller could exceed the column limit while still going through it. Fixtures 9 -> 12: `tagged-not-washing`, `thin-sample`, `unverifiable-wash`, `unreachable-and-no-cut`. The wash_trader tag now has three fixtures covering corroborated / uncheckable / refuted; a change that makes any two agree is a regression. All 11 verdict branches are covered. 12 fixtures x 2 languages, zero lines over the width limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(portfolio): correct holdings/activity field names against live API The documented names for `portfolio holdings` and `portfolio activity` did not match gmgn-cli 1.5.8, and are not accepted as aliases. This is where gmgn-wallet-analysis got its original field names from, and why its first cut read zeros off a live wallet: a wrong name returns `undefined`, which becomes 0, which renders as "this wallet made nothing" — an error that looks like an answer. Corrected against real responses: holdings array -> `list` cost -> `accu_cost` profit_change -> `total_profit_pnl` buy_tx_count / sell_tx_count -> `history_total_buys` / `history_total_sells` token.address -> `token.token_address` (that name is activity's) transaction_hash -> `tx_hash` type -> `event_type` Also: `avg_cost` is not returned (derive from `accu_cost / balance`), and `--sell-out` does not exist — gmgn-cli rejects it as an unknown option. Adds a response-envelope table, since the five routes do not share a shape: stats is bare, profits wraps a single row in `{"list":[…]}`, activity uses `activities`, holdings uses `list`, created-tokens puts rows under `tokens`. Documents fields that were missing entirely and are load-bearing for wallet analysis: `token.is_honeypot` (noting that `history_total_sells > 0` on the same row contradicts it — a honeypot cannot be sold), `token.launchpad_platform`, `token.liquidity`, `buy_cost_usd` (a sell row's cost basis), the gas/fee fields, and the realized/unrealized pnl split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(wallet-analysis): merge the style-label layer into the dossier The dossier had numbers, gates and a verdict but no name a reader could repeat out loud. The 4x5 title grid (frequency x P&L) and the speed subtitle come in from gmgn-wallet-style, with four corrections made on the way, each because the original mis-labelled a wallet we had already cross-checked against gmgn.ai's own leaderboard. - No ⭐「官方认证」badge. It fired on any non-empty `common.tags`, so it rendered `wash_trader` under a commendation glyph — the exact thing this skill's tag rules forbid. Tags keep going through TAGS/severity and the corroboration check. - The speed subtitle reads the median copy window, not `avg_holding_period`. The mean counts bags never sold, so it called a 2-minute scalper 「波段流 1-7 天」; someone acting on that holds for days a position the wallet exits in minutes. - P5 needs ROI > 50% plus ONE of {win rate >= 50%, heavy-loss share < 15%}, not all three. Requiring all three demoted a wallet sitting at #3 on GMGN's BSC 7D leaderboard to P4: memecoin P&L is low-hit-rate with a fat right tail, so a 50% win-rate gate penalises exactly the wallets that work. The `token_num >= 5` floor on P5 is kept. - The P5 gloss names the corroborator that carried it, so 「强盈」 is never a bare claim. 「高频且高胜率低回撤」 above a 33.3% win rate was a heading contradicting its own contents. Also: no label at all when `token_num < 5` — the verdict already says ⚪ 看不出来, and 「稳步选手 · 正收益,无明显短板」 beside it contradicts it. Badges merged: 彩票型, 分批止盈, 集中押注, 固定时段. The last two are gated on sample size, which the original was not: top-3 of 3 tokens is 100% by arithmetic, and any sample spanning under 12 hours "clusters" inside a 6-hour window by arithmetic. The original fired both on a 14.2-hour sample and emitted no warning. Not merged: 囤积中 / 派发中 duplicate the existing 24h posture reading, and 抢跑党 needs a per-chain gas constant that would go stale silently — gas_share and gas_drag already carry that signal, normalised against the wallet's own trade size. 12 fixtures x 2 languages, 8 distinct grid cells exercised, zero lines over the width limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(wallet-analysis): sync trigger words with the merged style layer The description still said the skill gives "a decision rather than a label or a score" — as of the style merge it gives both. That routed 「这个钱包什么风格」 to gmgn-wallet-style, which returns a strict subset: the same title and subtitle without the four gates, the profit engine, or the size cap. Adds the style trigger phrases, and states that a bare wallet address defaults here rather than being contested between the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(wallet-analysis): English-only source, translations in lang/zh.json analyze.py carried every user-facing string twice, as `_(zh, en)`, which made Chinese prose part of the program: 250 call sites, two tables holding both languages inline, and seven `if ZH` branches picking a list separator. Adding a third language meant editing every line of the file, and CLAUDE.md's "SKILL.md is English only" rule had quietly stopped holding. English is now the source of truth. `T("english template", *args)` looks the string up in `lang/<code>.json` and falls back to English when the key is absent, so a partial or missing translation degrades to English rather than to a crash or a blank line. Templates use positional placeholders ({0}, {1}) so a translator can move a value that reads in a different position in their language. - 247 call sites rewritten mechanically via the AST, so the English text and its interpolations are preserved exactly. - TAGS (20 rows), TITLES (20 cells), GATE_NAMES and GATE_GLOSS reduced to English-only; their Chinese moved to the table. - The 7 `"、".join(...) if ZH else ", ".join(...)` branches collapse into joinsym(), keyed on an explicit `__list_separator__` rather than on ", " — too generic to be safe as a table key. - The `ZH` global is gone; `load_lang(code)` replaces it. - Four call sites that concatenated conditionals into a sentence were restructured first, so every string is one whole translatable template rather than a sum of fragments. Two real bugs fell out of the conversion: - `archetype()` returned a rendered fallback string that two callers then sniffed with `startswith("普通") or startswith("ordinary")`. A language-dependent test inside language-agnostic code, correct only because both translations had been checked by hand. It now returns [] and each caller supplies its own fallback. - An English template carried full-width CJK brackets around a tag name; now plain quotes. This is the one intentional output change. Verification: all 12 fixtures x 2 languages were snapshotted before the refactor and diffed after every step. Output is byte-identical apart from the bracket fix and one fixture's romanised twitter_name. Deleting lang/zh.json renders clean English, confirming the fallback path. SKILL.md and gen_fixtures.py are English throughout. Two Chinese fragments are deliberately kept in SKILL.md and annotated as such: the trigger phrases in `description:` and the 「CA」/「合约」/「代币」 list in Step 1 are things a user literally types, and translating them would stop the skill triggering on Chinese input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(wallet-analysis): stop committing generated fixtures gen_fixtures.py rebuilds all 12 fixtures deterministically and is the single source of truth for what each one is meant to test. Committing the generated JSON next to it lets the two drift apart silently: a reviewer reading the generator cannot see that the checked-in copy no longer matches, and every threshold change produced a diff in 12 files that carried no information. Verified regenerable: deleting fixtures/, re-running gen_fixtures.py, and diffing all 12 fixtures x 2 languages against the pre-deletion snapshot gives 24/24 byte-identical output. lang/zh.json stays committed — it is not generated, and without it analyze.py has no Chinese output at all. The .gitignore says so, so the distinction is not left to be re-derived. The verification snippets in SKILL.md now run gen_fixtures.py first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): measure emoji width correctly and align the report dwidth() was wrong about emoji in both directions, and every column in the report inherited the error. ⚙️ U+2699 + U+FE0F measured 3, renders 2 🕸️ U+1F578 + U+FE0F measured 4, renders 2 ⚡ ⚪ ✅ U+26A1/26AA/2705 measured 1, renders 2 The variation selector was counted as two columns and the U+2600-27BF emoji as one. The first padded every column they appeared in short; the second is the dangerous direction — lines carrying those glyphs could exceed COL in a real terminal while the width check called them safe. The rule is that a base character followed by U+FE0F renders two columns wide, which is what the selector means, so this needs no enumeration of `⚙ ⚔ ✂ ✈ ...`. Box drawing (U+2500-257F) stays one column: the report's own rules and bars are built from it. wrap() had the same disagreement in a worse place: it summed cwidth per codepoint, so `⚔️` measured 1 there and 2 in dwidth(). Two such glyphs on a line put it 2 columns over — the English `grinder` speed read reached 78. Both now apply the same pairing rule. Layout, all of it alignment rather than taste: - The label column is measured, not hardcoded to 10. "key numbers" is 11 columns, so that row went unpadded and its continuation lines sat a column off from every other row in the block. - WHO IT IS is one label grid instead of three indents (style at 10, its gloss at 13, identity and every badge at 2). It read as a wall, and each badge took a line of its own; they now share one wrapped row. - Risk-flag and reassurance continuations hang under their text. At the bullet's own indent a wrapped flag read as a second flag. - A wrap never splits an emoji from the phrase it labels — lines used to end in a bare "✂️" with its name orphaned below. Tracked as a flag rather than read back from the buffer, because an emoji sequence ends in a zero-width selector whose base may not be wide on its own. - The "→" row's continuations hang under the arrow's text. 12 fixtures x 2 languages, all 24 outputs within 76 columns measured with the corrected width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(wallet-analysis): ship three files, drop the dev-only ones The shipped skill is SKILL.md + analyze.py + lang/zh.json. gen_fixtures.py and .gitignore are removed: neither is reachable at runtime, and the .gitignore existed only to ignore the fixtures/ that gen_fixtures.py produced, so the two go together. This is a real loss, stated plainly rather than glossed. gen_fixtures.py was the only offline verification this skill had — twelve synthetic wallets covering all eleven verdict branches, the three-way wash_trader matched set (corroborated / uncheckable / refuted), and the column-width rule. It needed no API key, and it is what proved the English-only refactor behaviour-preserving across 250 rewritten call sites. Without it every change to analyze.py has to be checked against live wallets, where the data moves between runs and the route budget rate-limits after a handful of attempts. It stays recoverable in git history on this branch. SKILL.md's Verification section now says so and documents how to verify by hand, instead of describing a suite that is no longer here. The width-check snippet in SKILL.md was also carrying the naive `sum(2 if ord(c) > 0x2E7F else 1 ...)` formula — the exact bug fixed in the previous commit, which reports a 78-column line as safe. It now imports dwidth() from analyze.py so the check and the renderer can never disagree again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): report why holdings failed, not one guess Every `holdings` failure was reported as "needs GMGN_PRIVATE_KEY / critical auth", whatever the cause. Observed live several times in a row: the dossier told the reader to go and configure a credential that was already configured, while the real cause — a rate-limit refusal — went unmentioned. Wrong instruction, and it hid the actual reason. Now split three ways. A limiter refusal says so and says to re-run; an auth failure keeps the credential wording; anything else is reported verbatim rather than guessed at, because a wrong attribution sends a reader down the wrong path more expensively than no attribution does. This matters beyond the limiter. `holdings` can fail on a network error, an auth change or a renamed field, and each of those would have been labelled a missing private key. Verified against all three branches offline (429 / missing key / 401 / network), with the Chinese placeholders substituting correctly in each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(wallet-analysis): lead with a decision card, keep the evidence below it Two audiences were fighting over the same screen. Someone who pasted an address needs to stop reading after a few lines; whoever checks the work needs every number. Evidence-first served neither, and the review feedback was blunt about it: too wordy, hide the judgement logic, give conclusions. The report now opens with a decision card and puts the reasoning after it. `--brief` prints the card alone; the default prints both. Nothing is removed — the entire previous output is the second layer, byte-identical (verified across 12 fixtures x 2 languages). The headline is money rather than a ratio: "$1,000 -> $1,621" instead of "+62.1%". Same figure, no conversion asked of the reader, and no extra API call — it is roi_7d wearing clothes a newcomer already owns. Hiding the reasoning obliges the card to be MORE careful than the evidence layer, not less. Five rules, each of which the first cut broke: - The four marks read the gates. They were hardcoded to ✓, so a "🔴 DO NOT COPY" card carried "✓ the record is real" — asserting the opposite of its own headline, on the very check that caused it. - No headline figure when G1 fails. The card showed "$1,000 -> $1,122" for a wash-trading wallet; G1 failing means the P&L is the thing in dispute, so quoting it presents a disputed number as an achieved one. - No "how to follow" under a red verdict. It printed a size cap and a copy window beneath DO NOT COPY — directions for doing the thing the headline forbids. - No editorial the flags contradict. "Not a wallet that only churns" appeared on a card flagged for wash trading. - No card at all when any gate is ⚪. The card has nowhere to put "not measured", and one missing tick reads as a complete verdict with one fewer reason. card_blocked() withholds it; the evidence layer, which can print ⚪, carries the whole answer. Also fixes a translation-key collision this change walked into: the card's gate label reused the key "cuts losses", which the numbers panel's win-rate chip already owned, silently rewriting that chip in eight fixtures. The table has one slot per English string; the card's label is keyed "it cuts losses" and SKILL.md now warns about the hazard. usd_exact() exists because usd() renders the headline as "$1.0K -> $1.6K", which both rounds away the thing being demonstrated and returns to the abstraction the card was built to avoid. The size cap uses it too — that number gets typed into a swap. Verification: evidence layer byte-identical across all 24 fixture/language combinations; 48 runs (both modes) within the 76-column limit; card withheld on all four fixtures with an unmeasured gate; both polarities inspected by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(wallet-analysis): put cadence, return and amount on the card Reviewing the card against the fields the old first page carried turned up three numbers that nothing else on the card implies: - trades per day — 16/day and 725/day are the difference between something a person can do and something only a script can. The copy window hints at it; it does not say it. - the 7d return as a percentage — the figure people actually quote to each other. The money headline replaced it as the hook, not as the number. - the realized amount — the only thing on the card that shows the scale THIS wallet works at. The size cap describes the reader's size, not its. Everything else the old page 1 carried is unchanged in the evidence layer, which remains byte-identical across all 24 fixture/language combinations. Adding the line walked straight back into the defect fixed one commit ago, one line lower down: the card said "its profit figures are not trustworthy" and then printed "7d 12.2% · made $101.1K". Trade cadence is a fact about behaviour and survives a failed G1; a return and an amount are P&L claims and do not, so they are dropped there. The key-collision guard added last commit earned its place immediately — it refused `7d {0}`, already owned by the numbers panel's form curve, which would have silently rewritten that row the way `cuts losses` did. The card's keys are `{0} over 7 days` and `made {0} in that week`. Verified on the live wallet 0xa7d4…2b9f: 5.4s, zero data gaps, all four gates measured, card rendered. A prior run of the same wallet was rate-limited into two unmeasured gates and correctly produced no card at all rather than one with four green ticks over an unverified P&L. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): reconcile the win rate with the outcome chart The report printed two numbers that contradict each other and left the reader to notice. On a live wallet: 188 of 209 tokens in the 0-200% band, next to a 23.9% win rate. If that band meant "gained 0-200%", the win rate would be 90%. Checked against the raw response rather than guessed at: winrate 0.2388 -> about 50 of 209 tokens pnl_0x_2x_num 188 sell 48 50 winners against 48 sells in the window. The only reading that satisfies both numbers is that the 0-200% band absorbs every token with no realized result yet — bought, not yet sold, so realized ROI is 0, which sits on that band's lower edge. Its size is therefore not a count of wins. Left alone, the chart actively misleads: a full-width green bar over 188 tokens makes a 24%-win-rate wallet look like it wins nine times out of ten, and a reader who spots the clash concludes one of the two figures is broken. Now stated in both places a reader meets it: - under the chart, naming the arithmetic — how many the band holds, how many the win rate implies, and how many are simply unsettled - in the win-rate row, which gives the realized-winner count instead of a bare percentage over a denominator that means something else Gated on >=20 tokens and the gap being >=25% of the band, so a wallet whose two figures already agree gets no note. One of twelve fixtures triggers it — the one modelled on this wallet. Verified: ignoring whitespace, the only substantive change to the evidence layer is the win-rate row. Three neighbouring panel rows shift by a column because the panel is column-aligned and that cell got longer. 48 runs within the width limit. This is the skill's own "reconcile contradictory numbers rather than printing both" rule, which the distribution chart had been violating since it was added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): use the fees actually paid, and make --size do something Two defects found by auditing what the skill computes against what it shows, and what the API returns against what it reads. **Friction was estimated while the real figure sat unread.** `portfolio stats` reports `bought_fee` and `sold_fee` for the window. Both were ignored. Friction was inferred instead from the gas median of a 300-row activity sample multiplied by the trade count — a sample-based estimate standing in for an exact number in a response already in hand. On 0xa7d4…2b9f the gap is not subtle: estimated gas $0.02 avg -> "0.0% of profit, friction manageable" actual bought_fee $2,924 + sold_fee $1,484 = $4,408 against $167,237 realized = 2.6% Two orders of magnitude, and the report was asserting "friction manageable" off the wrong one. It now uses the paid fees where the fields exist, keeps the sample estimate as the fallback where they do not, and labels which of the two it is showing — the estimate now reads "(estimated)" rather than passing as measurement. This feeds G3, whose reachability test fails at >=25% of profit, so the gate is now decided by the real number too. No fixture changes verdict: they carry no fee fields and take the fallback path. **--size accepted a value and did nothing with it.** It was parsed, stored on m["my_size"] and never read again — a silently inert flag. It has an obvious job, since size_cap already encodes the rule: above the wallet's own clip your fills are worse than the ones its record was built on, so its results stop describing you. Passing --size now states the multiple in the card's action block and in the numbers panel: the $5,000 you asked about is 11.7x its own clip of $427 — at that size your fills are worse than the ones this record was built on Nothing prints when the flag is absent. The flag is documented in SKILL.md, which had never mentioned it. Verified: across 24 fixture/language runs the only line that changes is the friction row, and only by gaining the "(estimated)" label — every percentage is identical. Neighbouring panel rows shift a column because that cell got longer. All runs within the width limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(wallet-analysis): surface four figures that were computed and discarded The audit that found the friction defect also found four things the skill either computed or received and then never showed. Each answers a question a reader asks out loud and the report could not previously answer. native_balance — the dry powder, in the chain's native token. GMGN's own leaderboard puts it in column two; this fetched it and dropped it. Now on the provenance line: "159.1 BNB on hand". last_timestamp — freshness. Every other figure in the report is silent about whether the wallet is still trading, so a dossier on a wallet that went quiet three days ago read exactly like one on a wallet trading right now. Now stated under the 24h posture, and past 48 hours it says so plainly: "last trade 5.0 days ago — every figure here describes a wallet that has since gone quiet". realized_all / unrealized — the all-time figures existed only as a percentage. "How much has this wallet ever made" is the most natural question there is, and the amount was computed and thrown away. New panel row: "all time $1.90M realized · $4.0K on paper". entry_sub100k / accum_window_s — both computed, neither shown. The first turns G3's "median entry $20K" into "…and 100% of its entries are under $100k", which is the stronger statement. The second gives the ladder badge a duration: asserting a wallet ladders in on a buy COUNT alone never said whether that happens over minutes or over days. Of 98 metrics, 11 were computed and never read. Four are now shown; the remaining seven (pcr_source, tag_sev, follow_count, gas_share, dev_ath_mc, realized_1d, my_size's raw form) are internal bookkeeping or genuinely redundant. Verified across 24 fixture/language runs: every changed line is either the friction row or G3's entry row, which are exactly the two this and the previous commit touch. 48 runs including --brief stay within the width limit. The new fields degrade silently when absent — fixtures carry no native_balance or last_timestamp and print neither. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(wallet-analysis)!: emit native markdown instead of terminal columns The skill printed column-aligned terminal text and every markdown version of it in this project was produced by hand afterwards. That is a defect with two edges: what an agent pipeline receives is not what a reader was shown, and SKILL.md's own "paste stdout verbatim" instruction could not be followed by anyone who wanted readable output. analyze.py now emits markdown directly — headings, tables, blockquotes, bold. Same two layers, same content, same metrics; only the renderer changed. The entire monospace grid is deleted with it: COL, BREAK_AFTER, wrap(), put(), wpad(), dwidth(), cwidth(), ZERO_WIDTH, WIDE_RANGES, WIDE_SYMBOLS and is_emoji_cp(). Roughly 120 lines existed to pad columns with spaces to a 76-display-column budget, and they kept producing a bug class of their own: a variation selector measured two columns while its emoji measured one, and wrap() and dwidth() disagreed by two, putting a line past the limit while every check called it safe. Markdown tables align themselves, so none of it is needed and none of it can rot. Verification changed shape accordingly. The old check counted display columns; the new one checks that the markdown is valid — every table has a delimiter row and a constant column count, `#` is followed by a space, bold markers pair, cell values are escaped so a pipe in a symbol cannot split a row, and the only raw HTML is the `<br>` used inside table cells. Clean across 48 runs (12 fixtures x 2 languages x with/without --brief). SKILL.md carries the checker. Two things the conversion surfaced and fixed: - "RISK FLAGS (2)" carried three bullets, because the reassurances were appended to the same list. They now sit under their own CLEARED heading, so the count matches its contents. - "0.0% of entries under $100k" printed a zero as if it were a finding. Guarded at the call site. SKILL.md's paste-verbatim rule now says what it means: do not reformat the output into your own tables, because that is exactly how the two drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): translate the five strings the markdown rewrite reworded The markdown renderer rephrased five strings — adding bold markers, a table header — and a reworded string is a new key. All five fell back to English mid-report, so the Chinese output carried "50 positions · $14.5K total", "24h bought / sold", "profit concentration ..." and "live book: unavailable" verbatim. Nothing failed: T() falls back to English by design, which is right for a missing translation and silent for a regressed one. Also splits the holdings table's column key. It used `value`, which the numbers panel already owns, and the table has one slot per English string; it is now `market value`. Same collision class as `cuts losses` before it, caught this time by checking before writing. Adds a completeness check to SKILL.md — walk the AST for literal T() keys and diff against the table. And a warning not to run that check in reverse. Using it to prune "unreferenced" entries deleted live translations here: several strings reach T() through a variable and the scan cannot see them — the TAGS, TITLES, GATE_NAMES, GATE_GLOSS and GATE_PLAIN tables, plus the holdings sentence built as a conditional expression. Unused entries are harmless bloat; a deleted live one breaks exactly one language. Verified on the live wallet: no English leaks in the Chinese report, and 48 fixture/language/mode runs still emit valid markdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): drop empty-header tables and give the report an H1 Three layout defects in the markdown output, all visible the moment it is rendered rather than read as source. **Four tables had an empty header row.** The speed read, the identity block and the 24h buy list are key/value pairs, not tabular data, and md_table() emitted `| | |` for them — which renders as a blank header band above the content and reads as a broken table. They are definition lists: `- **label** — value` says the same thing with no empty band. The outcome distribution genuinely is tabular and now has real headers instead of blanks. **`<br>` leaked out of its table cell.** It existed so a cell could hold two lines; once those blocks stopped being tables it rendered as literal text in the middle of a sentence. A continuation line indented two spaces stays inside its list item and needs no tag, so the output now contains no raw HTML at all and the checker rejects every tag rather than allowing one. **The document had no H1 when the card was withheld.** With a card the report opens `# verdict` … `# EVIDENCE`; without one it opened at `##`, so nothing was the top level. The verdict heading is now `#` when there is no card above it. The checker in SKILL.md gains all three: empty header rows, any raw HTML, and a document whose first heading is not `#`. Clean across 48 runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): label each gate once, and fix the heading hierarchy Readability pass on the markdown output. **A gate was labelled three times.** `✅ G1 真实性(数据可信吗)` carries an internal code, a formal noun and a plain-language gloss, all naming the same test. The code is bookkeeping that belongs in the source; the formal noun ("AUTHENTICITY") is not the question a reader is asking. The gloss is — so the gloss is now the whole label, and it reads as the question it always was: ### ✅ 数据可信吗 ### ✅ 现在还在赚吗 ### ✅ 你吃得到吗 ### ✅ 它会砍仓吗 The section heading also carried a `⚪G1 ✅G2 ❌G3 ✅G4` strip that restated the four marks appearing immediately below it. Gone. **Gates were bold paragraphs, not headings**, so four of them in sequence had no hierarchy and did not appear in any outline. They are `###` under their section now. **Heading levels skipped.** The card's own sections were `###` directly under a `#`, which reads as a missing level in any outline view. They are `##`. The distribution moves from `##` to `###` because it is a footnote to the numbers panel rather than a peer of it — eight top-level sections in the evidence layer was more navigation than the content needs. The checker in SKILL.md gains a level-sequence walk, so a future skip fails rather than being noticed by eye. Clean across 48 runs in both languages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): explain the size cap on every path, not just one The size cap's derivation lived on the `p["G3"] is True` branch of actions(), so it printed only when reachability passed. The run that most needs it is the other one: G3 fails, the reader is told not to mirror the wallet, and the card still shows "start no larger than $132" — a bare number sitting next to "do not copy", with the reasoning nowhere on the page. Sizing and reachability answer different questions. Gating one on the other was a control-flow mistake, not a missing sentence: the text existed and was unreachable exactly when it mattered. It now prints whenever a cap exists, worded so it holds under "do not mirror" as well: "keep it at or under $132 — half of the $263 this wallet puts on a buy. Past its own size your fill is worse than the ones its record was built on, so its numbers stop describing you." The quote-before-sending line stays on the G3-pass branch, since that one is genuinely about being able to get filled. Worth recording what this was not: the report DOES explain why the copy window uses a median rather than the mean — the hold_conflict note says the mean is dragged up by bags never sold. Both numbers looked unexplained; only one was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wallet-analysis): issue the decisive call first, not last A full dossier costs weight 26-28 against a rate-limit bucket of 20, so on a cold-ish bucket something gets refused. `holdings` was issued last, at cumulative weight 26, which made the most decisive call the guaranteed casualty — and it is decisive three times over: G1's corroboration of a `wash_trader` tag, G4's honeypot check and the profit engine all read it. Lose it and the verdict falls to HOLD OFF on a wallet whose tag was in fact refutable. Observed on five consecutive live runs of the same wallet, across waits of 280s, 290s, 400s and 420s. Waiting does not fix it: one run cannot fit in one bucket, so the tail is refused whatever the starting level. The gate-critical set now goes first and fits inside one bucket: stats_7d(3) -> profits_all(3) -> holdings(5) = 11 verdict decidable activity(3 x 3 = 9) = 20 window, entry band stats_30d(3), profits_1d(3) = 26 depth only profits_all ranks above the two windows because G2 is unevaluable without it, while stats_30d and profits_1d only add depth to readings that already exist — which makes them the correct things to lose. Same routes, same parameters, same page count: only the sequence moved. All 24 fixture/language outputs are byte-identical across the change, which is the claim that matters — no threshold, formula or verdict row is touched. This is the ordering half of the closed #2. The pacing and caching in that PR were rejected on the grounds that the API being integrated does not rate-limit; the ordering costs nothing either way and fixes the failure that is happening today against gmgn-cli. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: chenjunfeng <chenjunfeng@gmgn.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
23 KiB
name, description, argument-hint, metadata
| name | description | argument-hint | metadata | ||
|---|---|---|---|---|---|
| gmgn-portfolio | Analyze one or many crypto wallets by address — holdings, batch realized/unrealized P&L, win rate, trading history, performance stats, specific token balance, and tokens created by a developer wallet (with ATH market cap and DEX graduation status) via GMGN API on Solana, BSC, Base, or Ethereum. Use when user asks about wallet holdings, P&L (including comparing up to 100 wallets), win rate, what tokens a dev has launched, the highest ATH token a dev ever created, or wants a wallet report to decide whether to copy-trade or follow. | <info|holdings|activity|stats|profits|token-balance|created-tokens> [--chain <sol|bsc|base|eth|robinhood|arc|stable>] [--wallet <wallet_address...>] |
|
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 below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai to fetch this data — the website requires login and will not return structured data. The CLI is the only correct method.
⚠️ 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."
Use the gmgn-cli tool to query wallet portfolio data based on the user's request.
For full wallet analysis (holdings + stats + activity + verdict), follow docs/workflow-wallet-analysis.md
Core Concepts
-
realized_profitvsunrealized_profit—realized_profit= profit locked in from completed sells (cash in hand).unrealized_profit= paper gains on positions still held, calculated at current price. These are separate numbers — do not add them unless answering "total P&L including open positions." -
profit_change— A multiplier ratio, not a dollar amount.1.5= +150% return.0= break-even.-0.5= -50% loss. Computed astotal_profit / cost. Do not display this as a raw decimal — convert to percentage for user-facing output. -
pnl— Profit/loss ratio fromportfolio stats:realized_profit / total_cost. Same multiplier format asprofit_change. Apnlof2.0means the wallet doubled its money on completed trades over the period. -
winrate— Ratio of profitable trades over the period (0–1).0.6= 60% of trades were profitable. Does not reflect the size of wins vs losses — a wallet can have high winrate but net negative if losses are large. -
costvsusd_value— In holdings:costis the historical amount spent buying this token (your cost basis);usd_valueis the current market value of the position. The difference is unrealized P&L. -
history_bought_costvscost—history_bought_costis the all-time cumulative spend on this token (including positions already sold).costis the cost basis of the current open position only. -
Pagination (
cursor) — Activity results are paginated. The response includes anextfield; pass it as--cursorto fetch the next page. An empty or missingnextmeans you are on the last page.
Sub-commands
| Sub-command | Description |
|---|---|
portfolio info |
Wallets and main currency balances bound to the API Key |
portfolio holdings |
Wallet token holdings with P&L |
portfolio activity |
Transaction history |
portfolio stats |
Trading statistics (supports batch) |
portfolio profits |
Batch wallet P&L for 1–100 wallets |
portfolio token-balance |
Token balance for a specific token |
portfolio created-tokens |
Tokens created by a developer wallet, with market cap and ATH info |
Supported Chains
sol / bsc / base / eth / robinhood / arc / stable
Prerequisites
gmgn-cliinstalled globally — if missing, run:npm install -g gmgn-cliGMGN_API_KEYconfigured in~/.config/gmgn/.env
Rate Limit Handling
All portfolio routes used by this skill 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.
Critical auth (GMGN_API_KEY + GMGN_PRIVATE_KEY required):
| Command | Route | Weight |
|---|---|---|
portfolio holdings |
GET /v1/user/wallet_holdings |
5 |
Exist auth (GMGN_API_KEY only):
| Command | Route | Weight |
|---|---|---|
portfolio info |
GET /v1/user/info |
1 |
portfolio activity |
GET /v1/user/wallet_activity |
3 |
portfolio stats |
GET /v1/user/wallet_stats |
3 |
portfolio profits |
POST /v1/user/wallet_profits |
3 |
portfolio token-balance |
GET /v1/user/wallet_token_balance |
1 |
portfolio created-tokens |
GET /v1/user/created_tokens |
2 |
When a request returns 429:
-
On
RATE_LIMIT_EXCEEDED, tell the user exactly:已达到当前套餐的限频上限,点击 https://gmgn.ai/ai?chain=bsc&tab=paid_plans 升级套餐,获得更高速率限制. Show this upgrade guidance at most once per user task. Do not repeat it for subsequentRATE_LIMIT_BANNEDresponses during the same cooldown. -
Read
X-RateLimit-Resetfrom the response headers. It is a Unix timestamp in seconds that marks when the limit is expected to reset. -
If the response body contains
reset_at(e.g.,{"code":429,"error":"RATE_LIMIT_BANNED","message":"...","reset_at":1775184222}), extractreset_at— it is the Unix timestamp when the ban lifts (typically 5 minutes). Convert to local time and tell the user exactly when they can retry. -
The CLI may wait and retry once automatically when the remaining cooldown is short. If it still fails, stop and tell the user the exact retry time instead of sending more requests.
-
For
RATE_LIMIT_EXCEEDEDorRATE_LIMIT_BANNED, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes. Do not spam retries.
Usage Examples
# API Key wallet info (no --chain or --wallet needed)
gmgn-cli portfolio info
# Wallet holdings (default sort)
gmgn-cli portfolio holdings --chain sol --wallet <wallet_address>
# Holdings sorted by USD value, descending
gmgn-cli portfolio holdings \
--chain sol --wallet <wallet_address> \
--order-by usd_value --direction desc --limit 20
# Include sold-out positions
gmgn-cli portfolio holdings --chain sol --wallet <wallet_address> --sell-out
# Transaction activity
gmgn-cli portfolio activity --chain sol --wallet <wallet_address>
# Activity filtered by type
gmgn-cli portfolio activity --chain sol --wallet <wallet_address> \
--type buy --type sell
# Activity for a specific token
gmgn-cli portfolio activity --chain sol --wallet <wallet_address> \
--token <token_address>
# Trading stats (default 7d)
gmgn-cli portfolio stats --chain sol --wallet <wallet_address>
# Trading stats for 30 days
gmgn-cli portfolio stats --chain sol --wallet <wallet_address> --period 30d
# Batch stats for multiple wallets
gmgn-cli portfolio stats --chain sol \
--wallet <wallet_1> --wallet <wallet_2>
# Batch P&L for multiple wallets (default 7d)
gmgn-cli portfolio profits --chain sol \
--wallet <wallet_1> --wallet <wallet_2>
# All-time P&L for up to 100 wallets
gmgn-cli portfolio profits --chain sol \
--wallet <wallet_1> --wallet <wallet_2> --period all
# Token balance
gmgn-cli portfolio token-balance \
--chain sol --wallet <wallet_address> --token <token_address>
# Tokens created by a developer wallet
gmgn-cli portfolio created-tokens --chain sol --wallet <wallet_address>
# Created tokens sorted by all-time high market cap
gmgn-cli portfolio created-tokens \
--chain sol --wallet <wallet_address> \
--order-by token_ath_mc --direction desc
# Only migrated tokens
gmgn-cli portfolio created-tokens \
--chain sol --wallet <wallet_address> --migrate-state migrated
# ETH wallet holdings
gmgn-cli portfolio holdings --chain eth --wallet <0x_wallet_address>
# ETH wallet transaction activity
gmgn-cli portfolio activity --chain eth --wallet <0x_wallet_address>
# ETH token balance
gmgn-cli portfolio token-balance \
--chain eth --wallet <0x_wallet_address> --token <0x_token_address>
portfolio created-tokens Options
| Option | Description |
|---|---|
--order-by <field> |
Sort field: market_cap / token_ath_mc |
--direction <asc|desc> |
Sort direction (default desc) |
--migrate-state <state> |
Filter by migration status: migrated (graduated to DEX) / non_migrated (still on bonding curve) |
portfolio holdings Options
| Option | Description |
|---|---|
--limit <n> |
Page size (default 20, max 50) |
--cursor <cursor> |
Pagination cursor |
--order-by <field> |
Sort field: usd_value / last_active_timestamp / realized_profit / unrealized_profit / total_profit / history_bought_cost / history_sold_income (default usd_value) |
--direction <asc|desc> |
Sort direction (default desc) |
--hide-abnormal <bool> |
Hide abnormal positions: true / false (default: false) |
--hide-airdrop <bool> |
Hide airdrop positions: true / false (default: true) |
--hide-closed <bool> |
Hide closed positions: true / false (default: true) |
--hide-open |
Hide open positions |
portfolio activity Options
| Option | Description |
|---|---|
--token <address> |
Filter by token |
--limit <n> |
Page size |
--cursor <cursor> |
Pagination cursor (pass the next value from the previous response) |
--type <type> |
Repeatable: buy / sell / transferIn / transferOut / add / remove |
The activity response includes a next field. Pass it to --cursor to fetch the next page.
portfolio stats Options
| Option | Description |
|---|---|
--period <period> |
Stats period: 7d / 30d (default 7d) |
portfolio profits Options
| Option | Description |
|---|---|
--wallet <address...> |
One or more wallet addresses (required, max 100) |
--period <period> |
P&L period: 1d / 7d / 30d / all (default 7d) |
Response Field Reference
Response envelopes — each route wraps differently
Check the envelope before reaching for a field name. Reading a field off the wrong level returns
undefined, which becomes 0, which reads as a real answer ("this wallet made nothing") rather
than as an error.
| Route | Top-level shape | Where the rows are |
|---|---|---|
portfolio stats |
bare object (array for batch) | the object itself |
portfolio profits |
{"list": [ {…} ]} |
list[0] — a single row, still inside an array |
portfolio activity |
{"activities": [...], "next": …} |
activities — not list |
portfolio holdings |
{"list": [...], "next": …} |
list — not holdings |
portfolio created-tokens |
bare object | tokens, plus aggregate counts at the top level |
Some deployments additionally wrap the whole body in {"data": …}.
portfolio holdings — Key Fields
Rows come back under list, with a next cursor. Confirmed against gmgn-cli 1.5.8 live
responses — several names differ from what earlier versions of this doc claimed, and the old
names are not accepted as aliases.
| Field | Description |
|---|---|
token.token_address |
Token contract address (not token.address — that name is activity's) |
token.symbol / token.name |
Token ticker and full name |
token.price |
Current token price in USD |
token.is_honeypot |
Ships inline — no gmgn-token security call needed. A true here is contradicted by history_total_sells > 0 on the same row: a honeypot cannot be sold, and transfer-restricted RWA / tokenised-stock contracts trip naive simulators |
token.launchpad_platform / token.launchpad |
Where the token came from — the basis for "where does this wallet hunt" |
token.liquidity, token.max_supply, token.total_supply, token.creation_timestamp |
Also inline |
balance |
Current token balance (human-readable units) |
usd_value |
Current USD value of this position |
accu_cost |
Cost basis of the position still held (not cost) |
history_bought_cost / history_sold_income |
All-time buy cost / sell proceeds |
realized_profit |
Profit from completed sells (USD) |
unrealized_profit |
Profit on current unsold holdings at current price (USD) |
total_profit |
realized_profit + unrealized_profit (USD) |
total_profit_pnl |
Total profit ratio (not profit_change); realized_profit_pnl / unrealized_profit_pnl are the split |
history_total_buys / history_total_sells |
Buy / sell transaction counts (not buy_tx_count / sell_tx_count) |
history_total_transfer_ins / _outs |
Transfer counts — airdrops and internal moves, not trades |
start_holding_at / end_holding_at / last_active_timestamp |
Position lifetime |
wallet_token_tags |
Per-position tags |
There is no --sell-out flag — gmgn-cli 1.5.8 rejects it as an unknown option. avg_cost
is not returned; derive it from accu_cost / balance.
portfolio activity — Key Fields
Rows come back under activities, with a next cursor for pagination.
| Field | Description |
|---|---|
tx_hash |
On-chain transaction hash (not transaction_hash) |
event_type |
Transaction type: buy / sell / transferIn / transferOut. Some chains return type instead — read event_type ?? type. Transfer rows are airdrops and internal moves, not trades — exclude them from any ratio |
buy_cost_usd |
On a sell row, the cost basis of what was sold — cost_usd - buy_cost_usd is that exit's realized P&L |
gas_usd / priority_fee / tip_fee |
Friction. Compare gas_usd against per-trade net, not against nothing |
launchpad_platform |
Where the token came from |
token.address |
Token contract address |
token.symbol |
Token ticker |
token_amount |
Token quantity in this transaction |
cost_usd |
USD value of this transaction |
price |
Token price denominated in the quote token of the trading pair at time of transaction |
price_usd |
Token price in USD at time of transaction |
timestamp |
Unix timestamp of the transaction |
next |
Pagination cursor — pass to --cursor to fetch the next page |
portfolio stats — Key Fields
The response is an object (or array for batch). Key fields:
| Field | Description |
|---|---|
realized_profit |
Total realized profit over the period (USD) |
unrealized_profit |
Total unrealized profit on open positions (USD) |
winrate |
Win rate — ratio of profitable trades (0–1) |
total_cost |
Total amount spent buying in the period (USD) |
buy_count |
Number of buy transactions |
sell_count |
Number of sell transactions |
pnl |
Profit/loss ratio = realized_profit / total_cost |
The response also includes a common object when available (absent if the upstream identity service is unavailable):
| Field | Description |
|---|---|
common.avatar |
Wallet avatar URL |
common.name |
Display name |
common.ens |
ENS domain (EVM chains only) |
common.tag |
Primary wallet tag |
common.tags |
All wallet tags (e.g. ["smart_money"]) |
common.twitter_username |
Twitter handle |
common.twitter_name |
Twitter display name |
common.followers_count |
Twitter follower count |
common.is_blue_verified |
Twitter blue-verified badge |
common.follow_count |
Number of GMGN users following this wallet |
common.remark_count |
Number of GMGN users who have remarked this wallet |
common.created_token_count |
Tokens created by this wallet |
common.created_at |
Wallet creation time (Unix seconds) — records when the first funding transaction arrived; use this as the wallet's age indicator |
common.fund_from |
Funding source label |
common.fund_from_address |
Address that funded this wallet |
common.fund_amount |
Funding amount |
Use common.tags and common.twitter_username when building a wallet profile narrative. If common is absent in the response, omit identity fields silently — do not report it as an error.
portfolio profits — Key Fields
The response has a list array with one item per wallet. Monetary values are decimal strings; parse them with decimal arithmetic rather than binary floating point when exact calculations matter.
| Field | Description |
|---|---|
wallet_address |
Wallet address |
realized_profit |
Realized profit in the selected period |
realized_profit_cost |
Cost basis associated with selected-period realized profit |
buy / sell |
Buy and sell counts in the selected period |
unrealized_profit |
Unrealized profit on current holdings |
total_realized_profit |
All-time realized profit |
total_realized_profit_cost |
Cost basis associated with all-time realized profit |
total_profit |
Total profit |
total_cost |
Total cost basis |
portfolio created-tokens — Key Fields
The response data object has a tokens array plus aggregate stats.
Top-level fields:
| Field | Description |
|---|---|
last_create_timestamp |
Unix timestamp of the most recent token creation |
inner_count |
Number of tokens still on the bonding curve (NOT graduated) |
open_count |
Number of tokens that have graduated to DEX |
open_ratio |
Graduation rate (string, e.g. "0.25") |
Total created =
inner_count + open_count. Do NOT uselen(tokens)as the total — thetokensarray is capped at 100 entries and may be truncated. |creator_ath_info| Best-performing token created by this wallet (ATH market cap) | |tokens| Array of created tokens — see below |
creator_ath_info fields:
| Field | Description |
|---|---|
creator |
Wallet address |
ath_token |
Token address with highest ATH market cap |
ath_mc |
ATH market cap (USD string) |
token_symbol / token_name |
Token ticker and name |
token_logo |
Logo URL |
Per-token fields (tokens[*]):
| Field | Description |
|---|---|
token_address |
Token contract address |
symbol |
Token ticker |
chain |
Chain name |
create_timestamp |
Unix timestamp of creation |
is_open |
true if graduated to DEX |
market_cap |
Current market cap (USD string) |
token_ath_mc |
All-time high market cap (USD string) |
pool_liquidity |
Current liquidity (USD string) |
holders |
Current holder count |
swap_1h |
Swap count in the last hour |
volume_1h |
Trading volume in the last hour (USD string) |
launchpad_platform |
Launch platform name (e.g. Pump.fun) |
is_pump |
true if launched on Pump.fun |
bundler_rate |
Bundler participation rate (0–1) |
cto_flag |
true if community-takeover token |
Do NOT guess field names not listed here. If a field appears in the response but is not in this table, do not interpret it without reading the raw output first.
Output Format
Do NOT dump raw JSON. Always parse and present data in the structured formats below. Use --raw only when piping to jq or further processing.
portfolio holdings — Holdings Table
Present a table sorted by usd_value (descending). Show total portfolio value at the top.
Wallet: {wallet} | Chain: {chain}
Total value: ~${sum of usd_value across all positions}
# | Token | Balance | USD Value | Total P&L | P&L% | Avg Cost | Buys / Sells
Flag positions where profit_change is strongly negative (e.g. < -50%) or positive (e.g. > 200%) with a brief note.
portfolio activity — Activity Feed
Present as a chronological list (newest first). Use human-readable timestamps.
{type} {token.symbol} | {token_amount} tokens | ${cost_usd} | {timestamp} | tx: {short hash}
Group by token if the user asks about a specific token.
portfolio stats — Stats Summary
Wallet: {wallet} | Period: {period}
Realized P&L: ${realized_profit}
Unrealized P&L: ${unrealized_profit}
Win Rate: {winrate × 100}%
Total Spent: ${total_cost}
Buys / Sells: {buy_count} / {sell_count}
PnL Ratio: {pnl}x
[Identity: {common.name or common.twitter_username} | Tags: {common.tags}]
Show the [Identity: ...] line only if common is present in the response. For batch queries (multiple wallets), present one summary block per wallet.
Notes
portfolio holdingsuses critical auth (GMGN_API_KEY+GMGN_PRIVATE_KEYrequired — CLI signs the request automatically). All other portfolio commands use exist auth (API Key only, no signature required).portfolio statssupports multiple--walletflags for batch queries- Use
--rawto get single-line JSON for further processing - Input validation — Wallet and token addresses are validated against the expected chain format at runtime (sol: base58 32–44 chars; bsc/base/eth:
0x+ 40 hex digits). The CLI exits with an error on invalid input. - For follow-wallet, KOL, and Smart Money trade records, use the
gmgn-trackskill (track follow-wallet/track kol/track smartmoney)
Workflow
For full wallet analysis including trade history and follow-through on top holdings, see docs/workflow-wallet-analysis.md
For in-depth trading style analysis, copy-trade ROI estimation, and smart money leaderboard comparison, see docs/workflow-smart-money-profile.md
When to use which:
- User asks "is this wallet worth following" →
docs/workflow-wallet-analysis.md - User asks "what's this wallet's trading style", "when does he take profit", "smart money profile", "if I copied this wallet what would my return be" →
docs/workflow-smart-money-profile.md - User wants to compare multiple smart money wallets by winrate/PnL →
docs/workflow-smart-money-profile.mdStep 5 (leaderboard) - User asks "what tokens did this dev create", "dev 发过哪些币", "查一下这个 dev 的代币", "dev 创建记录" → use
portfolio created-tokens --chain <chain> --wallet <creator_address>directly. Get the creator address first viatoken infoif only a token address is given.