Commit Graph
72 Commits
Author SHA1 Message Date
chenjunfengandClaude Opus 5.5 ce145159f0 feat(wallet-analysis): judge a launcher by where its profit came from, not by how many tokens it launched
G1 decided "launcher" by a count (created_token_count > half of
token_num), which says nothing about where the money came from. It now
attributes positive realized gains by token contract for any wallet that
launched a token: lo = confirmed own share, hi = own + suspect share.
lo >= 50% vetoes, a range straddling 50% holds G1 open, hi < 20% lifts
the count veto. Closed positions are read via holdings sorted by
last_active_timestamp and also feed the wash-trade conviction share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:45:47 +08:00
chenjunfengandClaude Opus 5.5 e6574f95e4 feat(wallet skills): add gmgn-wallet-review and gmgn-wallet-style, route both
Adds two wallet skills and wires them into the routing.

gmgn-wallet-review (new): a glanceable trading portrait of one wallet. It
gives one style tag, a one-sentence recap of the record and a one-sentence
next step, for 1D / 7D / 30D / all, voiced 你 for the user's own wallet and
他 for anyone else's. It is a single SKILL.md with read-only gmgn-cli
portfolio calls, fixed rules and closed sentence templates. It covers SOL and
all 8 EVM chains, probing the chain for a 0x address.
- The recap names only problems in the trader's own trading: fees, holding
  time, loss cutting, sizing, adds, open bags, return trend and launches.
  Follower count appears only as a warning in visitor advice.
- Discipline rules from holdings, checked on 22 wallets: losers held 2x
  longer, losers sized 1.5x bigger (median per-buy ticket), adds 10 points
  worse, and 3+ open bags down 50% with $50+ cost.
- The next step never repeats the recap's problem.

gmgn-wallet-style (new to the repo): the full style tagging, with a 20-cell
title, speed subtitle and badges, each tag stating its triggering number.
It has a nine-chain allowlist and address patterns checked before any
command, and escapes every string it renders.

Both skills' tag rules match freq_level / pnl_level / style_title /
style_speed in gmgn-wallet-analysis/analyze.py. P5 needs ROI > 50% plus
token_num >= 5 plus ONE of {win rate >= 50%, heavy-loss share < 15%}. A diff
against the imported functions over 5,000+ cases gave 0 mismatches. The one
intended difference is gmgn-wallet-review's 3% 净盈 line at machine speed.

Routing:
- CLAUDE.md lists both skills and sends 这个钱包什么风格 / 交易风格 /
  wallet style to gmgn-wallet-style instead of the smart-money-profile
  workflow.
- gmgn-wallet-analysis hands style-only asks and one-line recap / next-step
  asks to the new skills.
- gmgn-wallet-score hands one-line recap asks to gmgn-wallet-review. Its
  description becomes a >- block (the old plain scalar failed yaml.safe_load)
  with identical text.
- Readme.md and Readme.zh.md list both skills.
No trigger phrase is claimed by two skill descriptions.

Both skills stop on a rejected API key (401 AUTH_KEY_INVALID) as well as on a
rate limit, and reference only skills that exist in this repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:20:45 +08:00
chenjunfengandClaude Opus 5 ff663aa3b9 fix(holder-analysis): a missing buy count was read as proof of zero-cost chips
`buy_tx_count_cur == 0` was taken to mean "these chips were transferred or
airdropped in, the holder paid nothing". Measured data refutes that inference.

On musebook (robinhood) 38 wallets read `buy_tx_count_cur: 0` and
`avg_cost: null`, but 34 of them carry a finite `unrealized_pnl` ratio — +2.7%,
+76.5%, +546% — and a chip with no cost cannot produce a finite PnL ratio; a
+2.7% ratio is only possible against a cost basis near the current price. 32 of
the 38 also carry a `fomo` frontend tag and 2 a `gmgn` tag, which is the trace of
active trading, the opposite of receiving an airdrop. What is actually missing is
buy-transaction indexing on that chain, while the PnL fields come from a separate
computation and arrive populated.

The error was systematic per chain rather than per token. Never-bought share of
supply across one batch: robinhood 27.0% and 41.2%, arc 26.3%, sol 4.8%. The 20%
airdrop warn gate therefore fired on essentially every robinhood and arc token and
essentially never on a sol token — a difference in index coverage, not in chip
structure. Reporting an unpopulated field as a risk conclusion is what this skill
explicitly forbids elsewhere.

So zero cost now requires positive evidence of no cost:

    def has_cost_basis(h):
        return (h.get('avg_cost') or 0) > 0 or h.get('unrealized_pnl') is not None

Six consumers of the old predicate move to it, all the same root cause:

  - the `airdrop` cohort and its >20% warn gate, plus the chip-quality buckets and
    the exit-signal line that read it
  - `diamond`, whose own stated rationale is "a wallet that never paid has no cost
    to hold through" — a proven cost basis satisfies that, and requiring an indexed
    buy count discarded most real diamond hands (JOLLY: 34 → 75 wallets,
    21.1% → 55.7% of float)
  - `idle_airdrop`, whose "zero cost" caption was a false statement for those rows
  - Top5 sell-risk classification, which labelled a wallet sitting at +6.6x as
    "zero-cost airdrop — can dump anytime" and simultaneously dropped it from the
    trapped and in-profit counts

Wallets with no indexed buy count but a real cost basis are not dropped from the
report — they get their own neutral line, `买入未记录 / Buy count unindexed`, with
their float share. That upstream did not index their buys is true and stays
visible; it just no longer drives a zero-cost gate.

Known limit, stated in SKILL.md: a genuine airdrop wallet whose `unrealized_pnl`
is a huge ratio computed against a dust-level cost is missed here. No second ratio
threshold is added — there is no measured sample to calibrate one on.

Also in the Advice section: a ⚠️ Caution rating needs two or more warns by
definition, but only `warns[0]` was printed, so a reader could never see why the
rating was what it was. Every triggered reason is now listed.

Ratings, up/main → this branch, measured end to end:

  musebook robinhood  🔴 Not Recommended → 🟡 Light Position
  JOLLY    robinhood  🟡 Light Position  → ✅ Normal
  ARGUS    arc        ⚠️ Caution         → 🟡 Light Position
  SI       sol        🟡 Light Position  → 🟡 Light Position   (control)

SI is the control: sol indexes buy counts normally, so it barely moves and its
🔴 risk-wallet reading of 45.36% is untouched. This is not a blanket loosening —
it only takes effect where the index is thin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:06:23 +08:00
chenjunfengandClaude Opus 5 338b0efdb9 fix(holder-analysis): the token's own contract address was scored as a holder wallet
Upstream returns the token's own CA in `token holders` with `addr_type: 0`, i.e.
as an ordinary wallet. Measured on musebook (robinhood,
0x91a2dae9699f0b82540b5886b0d8759c22820ba3): `addr_type: 0`, 0 buys, 0 sells,
0 transfer-out, 15.01% of total supply, $5.06M, carrying a `fresh_wallet` tag.

Left in the wallet cohort that one row polluted `biggest`, Top10, Top20, airdrop,
fresh wallets, risk wallets and the Top5 display at once, and on its own tripped
the danger gate as "largest wallet holds 16.30% — extreme concentration", rating
the token 🔴 Not Recommended. The largest genuine wallet there is 4.45% of float.

Controls on the same batch — JOLLY (robinhood), SI (sol), ARGUS (arc) — carry no
such row, so this is a per-token upstream classification gap, not a chain
convention, and cannot be handled by skipping a chain.

The address is partitioned out of `normal` and reported on its own line on a
total-supply basis, with its own warn gate at >10% of supply. It is not removed
from the judgement: contract-held supply reaches the market as soon as one release
transaction lands, so it stays visible and still counts toward the rating. Warn
rather than danger because that release is an observable prior step, unlike a
whale who can sell at will; no higher danger tier is added, because one measured
sample cannot calibrate a second threshold.

It is deliberately not deducted from the float denominator. Burn is permanent and
the DEX pool is the market itself, so neither can dump; self-held supply can.
Removing it from the denominator would raise every other wallet's percentage on
every token carrying this row, manufacturing new false positives while fixing one.

The same address is also excluded from `top100_map`, which feeds the dev
sock-puppet danger gate: chips sent back to the token contract, or gas paid to it,
is not "transferred to an internal wallet".

musebook: 🔴 Not Recommended → ⚠️ Caution. Top10 34.7% → 19.4%, Top20 43.4% →
27.8%, risk wallets 21.01% → 4.71%, fresh wallets 18.25% → 1.95%.
Regression on the three control tokens: JOLLY and SI byte-identical, ARGUS differs
only on the market-cap header line (live price drift between runs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 16:05:19 +08:00
chenjunfengandClaude Opus 5 117884db9c fix(gmgn-heat-rank): growth rates off creation time, and stop one bad created.json entry discarding the rest
Three fixes found by testing the age split, not by reading it.

1. holders/day and KOLs/day still divided by the pool-open age. Those counts
   accumulate from the moment the token exists, so a token that spent 18 days on
   a bonding curve and migrated yesterday reported 10,836 holders/day and 22.0
   KOLs/day against a true 993 and 2.0 -- a wrong number printed on the address
   line and fed into an axis carrying weight 0.15. They now divide by the same
   creation age the track and the freshness axis read, which falls back to the
   open-time age when Step 3b has no answer and is clamped never to read younger,
   so a lookup can only lower a row's own rate. `c['age']` had no other reader
   and is retired rather than left beside `cage` as a second age.

2. The created.json loader judged every entry under one try block, and comparing
   a non-number against the plausibility window raises. So a single unreadable
   entry threw away every entry after it, and those rows returned in the next
   NEEDS-CREATED block -- a loop with no exit if the feed keeps answering the same
   value. Each entry is now judged on its own and no comparison can raise.
   Measured: ten bad values x first and last position, 20 runs, every one exits 0,
   the readable entries survive, and each unreadable one falls back to open time.

3. Pass 1 stopped with SystemExit(0), so "stopped to ask for Step 3b" and "ran to
   completion" were the same status. It now exits 3, leaving 0 = the list is the
   answer, 3 = incomplete, 1 = could not read its inputs.

Net effect on the same saved sweep: the same four names in the same order, ARGUS
72.1 -> 69.5 as its growth rate corrects. Two claims the first commits left
imprecise are amended: the second pass is not purely subtractive, since the
growth axis is a within-chain percentile, and only gate-surviving rows are looked
up, so a listed migrated token is measured on its true rate against neighbours
still on their inflated one -- an error that costs listed names points rather
than granting them, now stated under `## Known limits`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:19:47 +08:00
EdwardchennnnandClaude Opus 5 5eecc8574b fix(gmgn-heat-rank): close four holes found reviewing the age split
Re-audit of the change in this PR, all four in code or claims the PR itself
introduced.

1. A creation time that was not a readable number crashed the whole run. A
   string-typed timestamp in `created.json` raised `TypeError: unsupported
   operand type(s) for -: 'float' and 'str'` out of `creage()` and took the
   entire scoring pass with it, instead of dropping one row. The loader now
   coerces once, up front.

2. Worse than the crash: the values that did NOT crash were accepted and
   silently moved the list. `NaN` compares False against everything, so it
   passed the track test as "not young" AND came out of the freshness clamp
   at 1.0 — the easier mature track and a full newborn bonus at the same
   time, which is precisely the bug this PR exists to remove. A negative or
   absurdly small number read as "very old" and also bought the mature
   track. Unreadable is now stored as None (asked, no answer), which falls
   back to open time and keeps the row on the stricter track. Verified
   against string / list / NaN / negative / float / null / bool / zero /
   future-dated: nine cases, no crash, and every unreadable one now resolves
   in the strict direction instead of the generous one.

3. A lookup that never landed was indistinguishable from one that came back
   empty, and both were written as null. Exit status cannot separate them —
   `gmgn-cli token info` answers 0 with a fully zeroed body for an address it
   does not know (measured: `creation_timestamp: 0`, empty stderr). So the
   body itself decides: one that parses and echoes the address back is a real
   answer and a zero creation time is written as null, while one that does not
   parse or does not echo it back is a call that never landed, is left out of
   `created.json` entirely, and is named on stdout so Step 3 lists it again
   rather than scoring the row off an age nobody read. Step 3b also keeps its
   stderr now, in an `.err` file beside the response, the way Step 1 does,
   instead of sending it to /dev/null.

4. The report could state an age that was off by weeks. The table's age column
   is the pool-open age, which is right for the 7-day ceiling, but after this
   PR a token can be listed whose real age is far older — and nothing printed
   that number, so the writer could not have known. arc/ARGUS would have been
   reported as "1.9d" on a list whose headline promise is recency, when the
   token is 19.5 days old. The address line now prints `created=N.Nd ago`
   whenever the two differ by more than a day, and stating it is required in
   `## What the answer has to contain`.

Two claims elsewhere in the file that this PR had made false are corrected
rather than left to contradict it: the ceiling bullet said `MAX_AGE_D` stops a
months-old token reaching the list, which is now true only of months of
trading and not of the token's age, and the `YOUNG_D` cliff note did not say
which of the two ages the cliff is measured on.

No threshold, weight, gate constant or routing description changes. Against
the same saved 7-chain sweep, pass 1 names the same 5 rows and pass 2 returns
the same 4 names in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:09:13 +08:00
EdwardchennnnandClaude Opus 5 5c4841342a fix(gmgn-heat-rank): read the track and the freshness axis off creation time, not pool-open time
`open_timestamp` was doing all three of the age's jobs: the 7-day recency
ceiling, the new-launch/mature track choice, and the freshness axis. For a
token that sat on a bonding curve for weeks before it migrated, that one
number pulls in two opposite directions at once -- the token reads as hours
old, so it is held to the new-launch run-rate floors (thresholds set for
tokens that have no history to measure) while simultaneously being paid a
newborn's freshness bonus.

Measured on a live sweep this cost a correct listing: arc/ARGUS, real age
18.4d, was dropped for missing the $150,000 1h floor by $824 while carrying
a 21.8h freshness score.

The jobs are now split onto the timestamps that actually mean them:

- the 7-day ceiling (`--max-created` / `MAX_AGE_D`) still reads
  `open_timestamp`, because "recently hot" is a claim about the tape and
  migration is that event;
- the track choice (`YOUNG_D`) and the freshness axis read
  `creation_timestamp` from `token info`, because those are claims about the
  token's own history.

Cost is bounded rather than per-candidate. Step 3 now runs twice: pass 1
emits a `NEEDS-CREATED` block naming only the rows whose outcome can still
turn on their age -- a row already dead on liquidity, rug score or a
one-sided tape gets no call -- new Step 3b fetches exactly those, pass 2
prints the list. On the verification sweep that was 5 lookups against 58
candidates, so a whole run is 21-27 calls instead of 21. The block warns
itself if it ever asks for more than 20.

Guards on the new code:

- a creation time that reads newer than the pool open time is clamped, so a
  lookup can never move a row onto the easier track or buy it a freshness
  bonus it did not already have;
- a key present with a null value means "the lookup ran and there is no
  answer" and falls back to open time, which keeps the row on the stricter
  track instead of re-asking forever;
- Step 3b refuses to run with `DATA` unset, validates each chain name
  against the literal seven and rejects any address that is not
  alphanumeric, and refuses to write an empty `created.json` that would send
  pass 1 back to the same `NEEDS-CREATED` block in a loop.

No thresholds, weights, gate constants or report rules change, and the
frontmatter is untouched. Verified against a saved 7-chain sweep: pass 1
names the same 5 rows, pass 2 returns the same ranking, and the only list
difference the rule itself makes is the one row it was written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:25:22 +08:00
chenjunfengandClaude Sonnet 5 be98adaeaa fix(gmgn-narrative): final self-audit — stale test-status claim, DNS-rebinding gap
Two findings from a final read-through before requesting merge:

- SKILL.md's Supported Chains section still said arbitrum/hyperevm were
  "untested" — stale as of this skill's own development, where 4 real
  tokens (2 per chain) were actually run through the full workflow with
  no chain-specific issue found. Corrected to describe that spot-check
  honestly (a signal, not a validation pass) instead of leaving a claim
  that had already been overtaken by the skill's own test history.
- interfaces.md's web-fetch address-range control named the private/
  loopback/link-local ranges to block but didn't say to pin the resolved
  address between the check and the connection — a hostname re-resolved
  a second time at connect can return a different (and unblocked)
  address, which is exactly what a DNS-rebinding attack targets. Added
  the one line closing that gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 17:45:45 +08:00
chenjunfengandClaude Sonnet 5 35a0d812ee fix(gmgn-narrative): declare the web-fetch interface and fix rule/workflow contradiction
Review caught a real internal contradiction: step 4 requires opening a
linked source (a post's URL, a project's site) to verify a claim, but hard
rule 6 said the skill may only read from the two declared interfaces —
neither of which is a general web fetch. Declares a third interface
(web content fetch) with explicit controls: http/https only, private/
loopback/link-local address ranges rejected (closes the SSRF vector a
token's own attacker-controlled link.website field would otherwise open),
a timeout, a response-size cap, and re-validation on redirect. Rule 6 now
scopes "no browser automation" against open-ended browsing rather than
against fetching one already-known URL, which is what it was actually
contradicting.

Also, per the same review:
- New hard rule 7: a hard ~30-tool-call ceiling per card, so an
  adversarially ambiguous token can't turn one card into unbounded tool
  use — write the card with what's verified so far and say the cap was
  hit, same "neutral over fabricated" principle applied to cost/time.
- New hard rule 8: a product-facing note that a hedged single-sourced
  negative claim about a named/identifiable party still carries
  reputational and legal exposure, even when the hedge itself is correct.
- interfaces.md's turn-budget row and "what this skill must never do"
  section updated to match (three interfaces, not two; the 30-call cap
  is this skill's own stop condition, not one for the runtime to
  second-guess with a lower limit of its own).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 17:38:49 +08:00
chenjunfengandClaude Sonnet 5 130711ec2f feat(gmgn-narrative): add independent token narrative/due-diligence card skill
Produces a two-field narrative card (叙事背景 + 传播观察) for a token from
on-chain data (gmgn-cli) and independently-verified social posts, built to
catch the failure modes automated narrative cards on the market commonly
have: under-sampling social search, uncited/misattributed claims, launchpad
vs issuer confusion, and fabrication when evidence is thin.

Provider-agnostic by design: the social evidence step is a pluggable
interface (X API v2 or xAI Grok API), never browser automation or a
personal login — see references/interfaces.md for the configuration
contract and a validation-pass checklist for testing on a new model/runtime.
references/patterns.md is the red-flag checklist accumulated during
development (ticker collisions, similarly-named token mixups, creator
wallet history, paired-token mechanics, citation failures).

Explicitly out of scope for gmgn-contract-dd (safety score),
gmgn-heat-rank (screened hot list), and gmgn-token-buy/gmgn-swap (buy
decisions) — this only answers "what story is this token telling and
what's verifiable about its spread."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 17:25:13 +08:00
chenjunfengandClaude Sonnet 5 dd6413c88f fix(token-buy): sync stale direction-gate wording in thresholds.md and pitfalls.md
Two leftover passages still described direction as a hard veto gate
that "must pass" alongside volume, contradicting the new rule in
section 三 where only the 5m ≥10% drawdown hard-stops and everything
else downgrades execution instead of vetoing the order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:37:43 +08:00
chenjunfengandClaude Opus 5 48d61990cb gmgn-contract-dd: scope the EVM lock tightening to the six meme-native chains
Upstream added `arbitrum` and `hyperevm` (#227), making the file's chain counts
nine / eight rather than seven / six. Three things follow.

1. Chain counts in the passages this branch added still said six EVM chains and
   seven supported chains. Corrected, and the `burn_status: ""` coverage claim is
   extended with a 2026-09-11 measurement on the two new chains: ARB, WBTC, ODYS,
   SIGNAL, CPU and CHAMELEON all return `""`, so the burn half is missing there too.

2. The Step 3 tightening — deduct −12 and cap 79 on `lock_summary.is_locked ===
   false` alone — was calibrated on the six chains that existed then, where 12 of
   12 sampled tokens returned `is_locked: true`. It does not hold on the two new
   ones: measured 2026-09-11 on the top three of each chain's 24h trending list,
   ARB and WBTC on `arbitrum` and SIGNAL and CPU on `hyperevm` all return
   `is_locked: false` — 4 of 6, including the chain's own governance token and
   wrapped Bitcoin. Deep DEX liquidity from real market makers is not locked and
   is not meant to be, so there the deduction would fire hardest on the tokens it
   exists to clear. The row is now skipped on `arbitrum` and `hyperevm` and the
   tightening applies only to `bsc`, `base`, `eth`, `robinhood`, `arc`, `stable`.
   Cross-references in the cap summary, the cap-cause list and the Step 3 table
   row name the scope.

3. The two dated bsc trenches passages contradicted each other on the same
   180-row sample — 130 flap / 50 four.meme against flap 143 / fourmeme 37. The
   per-platform split is not a stable quantity: two captures of the same 180 rows
   hours apart on 2026-09-11 gave 150 / 30 and 137 / 43. The load-bearing claim is
   the mapping, not the mix, so the suffix passage now states the mapping with
   today's figures and says outright that a platform's share must not be read off
   one capture, and the non-load-bearing counts are dropped from the launchpad
   passage, whose actual claim is that `market trenches` never emits `openfour`.

No band boundary, weight or cap value changes. Inventory totals are unchanged at
23 on `sol` and 25 on the EVM chains — the skipped lock row stays in the
denominator as a skipped check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:35:29 +08:00
chenjunfengandClaude Opus 5 7b51b191d4 Merge up/main into contract-dd display-templates branch
Brings in the arbitrum + hyperevm chain support merged upstream in #227, so the
chain counts in this file are the nine-chain / eight-EVM ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:32:30 +08:00
chenjunfengandClaude Opus 5 91088a8cf5 fix(token-buy): direction and volatility decide how to place the order, not whether to place it
The four gates were one flat "any single failure means no order" list, but they
answer two different questions. Volume, depth and security answer "can I get
back out once I am in" — structural facts that do not flip within hours.
Direction and volatility answer "where will a market order fill this second" —
which changes minute by minute. Merging them makes the same token get opposite
verdicts on the same day.

Measured: VC on BSC (0x36f01746a86639d0c3b6a850d328be1b634a4444) scored 91.4
"buyable" from gmgn-contract-dd, one-sided depth $91,442, 95% of LP burned to
the black hole, $6.6M 24h volume, 2,560 holders, zero taxes, owner renounced —
and was still reported as "not recommended" purely because 5m was -8.2% and all
three net-flow windows were negative. Two separate rules produced that: the
compound hard rule (three windows negative AND 5m drawdown >= 5%), and the
sentence that merged this section's warnings into the security section's
">= 2 warnings fails" count.

Section 三 now has three exits instead of two: 5m drawdown >= 10% remains the
one hard stop (a market order genuinely cannot execute there), everything else
downgrades execution — the order card is still produced, but it must name the
degraded execution path. Added a table of the three downgrade actions so none
of them can be given by feel: the limit trigger price is derived as
price.price * (1 - |5m drawdown|) and handed to gmgn-swap as
`order strategy create --order-type limit_order --sub-order-type buy_low
--check-price <price>`; waiting names the condition that has to converge (5m
drawdown back inside 5%, or buy_volume_5m - sell_volume_5m turning positive);
reducing size reuses the existing price-impact wording. The downgrade needs its
own separate confirmation — nodding yes to buying a token is not the same as
knowing you are no longer getting a market order.

Direction now produces no veto count at all, and missing direction data
downgrades to the strictest tier rather than failing the order: not being able
to read direction is not the same as a quiet market.

Nothing about the original intent is relaxed. The slippage formula is
untouched (its |5m change| * 0.5 term already prices volatility), the
>= 100x-with-negative-flow warning still refuses to widen slippage, and the
security gate's ten warnings and red flags are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 14:48:08 +08:00
chenjunfengandClaude Opus 5 de9ce030db gmgn-contract-dd: reconcile four live rules the reword left inconsistent
Step 6 still told the reader to print the section weights while Step 8 and
Display Templates both forbid printing coverage and weights, so the same
response could ship two different report shapes depending on which sentence
was read last. Step 5B's rug_ratio cap table still named the pre-reword band
("high risk") in a live rule, which also falsified the neighbouring claim that
every live occurrence had been corrected — that count is now three, with the
grep procedure that found the last one written down. Step 8's cap list named
Step 2, 5B and 7 but not Step 3's EVM lock cap, which the assembly list in
Step 6 already carries.

No threshold, weight or band boundary moves; every measured score in the file
is unchanged. Dated measurement passages keep the original audit vocabulary by
the policy stated in the file — all 32 remaining occurrences were classified
and every one of them is a record of a run, not a rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:19:02 +08:00
chenjunfengandClaude Opus 5 176eb126ac gmgn-heat-rank: print contract addresses without a gmgn.ai link
The CA block was headed `CA (verify at https://gmgn.ai/<chain>/token/<CA>)`.
That path shape is the one thing in this file nobody was allowed to check: the
skill's own rules forbid reaching gmgn.ai by web search, WebFetch, curl or a
browser, so the URL was printed on the strength of an assumption. If the shape
is wrong the reader lands on a 404 and doubts the list rather than the link.

The addresses themselves were already complete and one per line, so dropping
the link costs nothing -- an address pastes into whatever front-end the reader
actually uses. The heading now says exactly that and names no host.

Behaviour is otherwise unchanged: replayed against five stored sweeps, the
scorer's output differs on that single heading line and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 17:02:18 +08:00
chenjunfengandClaude Opus 5 685ce2177b gmgn-contract-dd: label the stat percentages by their real denominator
Five problems found by rendering the report against a live bsc token
(4Stock, 2026-09-10) and checking every printed line back against the
response. All five are output-wording problems; no scoring table and no
composite in this file moves.

- The stat percentages were rendered as shares of the supply. They are
  not: bot_degen_count 1068 / bot_degen_rate 0.5416 gives a denominator
  of 1971.9 on a token with 20,930 holders, and top_bot_degen_percentage
  returned exactly 0.5416 alongside it, which ties the whole
  top_*_trader_percentage family to that same analysed-trader cohort.
  The worked example in Display Templates carried the wrong wording and
  a live report inherited it, telling the reader that 61.1% of the float
  sat in entrapment wallets — a claim the response never made.
- The Limits of this read skeleton asked for off-chain presence
  unconditionally, so an all-empty link block was written up as "no
  off-chain presence at all". An empty link block cannot be told apart
  from GMGN not having collected the socials, so the bullet is now
  dropped unless a social field or the verification status has a value.
- An isolated stat zero was being printed as a passing check. The report
  now needs a second field from the same response to corroborate it:
  creator_hold_rate 0 qualifies (creator_token_balance "0" plus
  creator_token_status "creator_close"), top70_sniper_hold_rate 0
  qualifies (wallet_tags_stat.sniper_wallets 0), private_vault_hold_rate
  and dev_team_hold_rate do not and are dropped. Scoring is untouched —
  the zero is still an executed check in coverage.
- Market cap was being printed with no rule saying where it comes from,
  and the response carries no market_cap key at all. It is now fixed as
  price * circulating_supply, verified against a figure GMGN does
  publish: ath_price * circulating_supply = 82,633,663 matches
  dev.ath_token_info.ath_mc exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:53:27 +08:00
chenjunfengandClaude Opus 5 1ab16cb76e fix(heat-rank): iterate the chain list as an array, and match whole names
Step 1 held the chain list in a plain string and iterated it as
`for ch in $CHAINS`. That works under bash and silently does not under zsh,
which performs no word splitting on an unquoted parameter expansion: the loop
runs once with all seven names in one variable. The sweep then collapses to a
single call with `--chain "sol bsc base eth robinhood arc stable"`, which the
CLI refuses -- no data, no list, on every run in a zsh shell.

The whitelist did not catch it either. `case " sol bsc ... " in *" $ch "*` is a
substring test, so the whole concatenated string matched its own haystack and
was waved through; it accepts any run of adjacent names. It is now an exact
alternation over whole names, which also refuses `sol bsc` and anything
carrying shell metacharacters. `$ch` and the two output redirections are
quoted.

Introduced by b4b54ad, whose own verification ran the extracted block under
bash and so could not see it.

Verified: `-n` clean under zsh, bash and bash --posix; seven iterations with
the correct per-chain filters in all three; and a live run under zsh now makes
15 calls across all seven chains (eth/arc/stable short-circuiting on an empty
24h window as designed), 56 candidates -> 10 passed -> 3 listed. The scorer
block is byte-identical, so scoring is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:37:34 +08:00
chenjunfengandClaude Opus 5 dfbbb7c688 fix(gmgn-heat-rank): an unreadable age is unknown, not zero
`rage` fell back to `now` when a row carried neither `open_timestamp` nor
`creation_timestamp`, so a row with no age read as "launched this instant":
it walked through the `MAX_AGE_D` ceiling -- the one gate the whole list
rests on -- was judged on the wrong (new-launch) track, and would have taken
full freshness credit had it cleared that track's gates. That is the same
mistake as reading a missing risk field as clean, which this skill refuses
to make anywhere else.

An unreadable age is now placed past the ceiling and refused as
`no timestamp (age unknown)`, reported as the data fault it is rather than
as a token that failed a check. Both timestamp fields are normalised as
counts, so an unparseable value arrives as 0 and is caught by the same test
as an absent one.

Verified: on a row with both timestamps stripped, before/after is
rage=0.000 age=0.500 young=True (age gate silent) -> rage=8.000 young=False
fail=['no timestamp (age unknown)'], with no duplicate age-backstop reason.
Output is byte-identical on four unmodified fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:19:19 +08:00
chenjunfengandClaude Opus 5 17d312bd1b fix(heat-rank): drop --filter is_out_market, which the API does not recognise
`is_out_market` is not a filter tag the API knows, and an unrecognised tag is not
refused -- it is silently ignored. Worse, a filters list made only of unrecognised
tags does not fall back to the server's defaults: it replaces them with nothing.

Measured on sol with the skill's own pool parameters: `--filter is_out_market` and
`--filter zzz_fake_tag_qqq` returned the identical 22 rows, both a superset of the
18 returned with no --filter at all.

On sol / bsc / base / eth the tag sat alongside real tags and was inert -- dropping
it returns the identical address set on both chains tested. On robinhood / arc /
stable it was the *only* tag, so those three chains were fetched with server-side
screening switched off. On one robinhood sweep that admitted 6 rows the defaults
exclude: 4 with is_honeypot=1, plus CLUBY and BRIAN, neither renounced nor open
source and neither caught by any local gate in this file.

So the fourth branch now sends no --filter and lets the server's defaults apply,
with a comment saying not to invent a tag to fill it, and `## Known limits` records
the measurement.

Verified end to end on a live sweep with the fix in place: 15 calls, the three
empty chains short-circuited, robinhood 20 rows instead of 26, and zero
is_honeypot=1 rows anywhere in the candidate pool. 52 candidates, 10 passed,
3 listed. Step 1 passes bash -n; an empty F array expands to no arguments; the
scorer is byte-identical to the previous commit's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:56:53 +08:00
chenjunfengandClaude Opus 5 7fe841b9fb gmgn-contract-dd: put the verdict on the buyability axis, drop the launchpad claim
The verdict band words were audit vocabulary — `relatively clean · mixed,
needs manual review · high risk · very high risk`. "Mixed" names the average
of the three sections, which is the one reading that hides a one-rotten
two-clean profile, and "needs manual review" hands the work back without
naming what to review. A reader who pastes an address is asking whether they
can buy it, so the bands now answer that: `buyable` / `buyable with
conditions` / `not advised` / `do not buy`, each paired with a clause derived
by rule from which sections fell under 60, so the pair carries how buyable
from the word and what is in the way from the clause.

The 80 / 60 / 40 boundaries did not move, so no composite in this file
changes grade. Dated measurement passages keep the original words as records
and a decoder maps them one-for-one; two live rules that still named a band
in the old vocabulary are corrected.

Three guards come with the new axis, because `buyable` is easier to
over-read than the audit words were: it means the token can be bought and
sold back, never that it will appreciate; `Buy or not` may not contradict the
band word nor merely repeat it; and the band word is not permission to size a
position, which stays gmgn-token-buy's job.

Also drops the launchpad line from the report. `info.launchpad` is not
reliable enough to name: the vocabulary is unstable across endpoints (4Stock
returns `openfour` from `token info` while all 180 `market trenches` bsc rows
use only `flap` and `fourmeme`), and 9 of 100 bsc trending rows return an
empty string with no way to tell "no launchpad" from "unknown launchpad".
Measured coverage is complete on sol and unmeasured on base/eth/robinhood/
arc/stable, so the claim goes rather than ships hedged. The address-suffix
rule stays — it rests on its own 180-row tally.

And corrects an over-absolute claim: `pool.initial_liquidity` is *usually* 0
on a migrated pool, not always. Measured on 4Stock, GMGN had switched the
reported main pool to a pancake_v3 pool created about half an hour after
launch, which returns 19893.57. A non-zero value is that pool's own opening
depth and must never be reported as the token's launch depth; a shrink ratio
above 1 means the pool deepened and is never a deduction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:42:23 +08:00
chenjunfengandClaude Opus 5 dd485face1 perf(heat-rank): fetch 24h first and skip a chain's other two windows when it is empty
A candidate has to be present in the 24h window to count at all -- the scorer
only uses a chain when a 24h file loaded, and keys candidates off it. So for a
chain whose 24h window comes back empty, the 1h and 6h calls cannot produce a
candidate no matter what they contain. The sweep was spending them anyway.

Reorder the window loop to 24h first and break out when the 24h response holds
an empty rank array. On a measured sweep eth, arc and stable were empty in all
three windows, so this drops 21 calls to 15 and ~30s to ~21s. Worst case is
unchanged: when all seven chains are alive the sweep still costs its full 21.
This cuts waste, not coverage -- no window that could hold a candidate is skipped.

Teach the loader to tell a deliberate skip from a failure, so the saving does not
print as two failed calls. `missing (excluded)` keeps meaning one thing: a call
that failed or returned unparseable JSON. A 24h window that itself failed to load
still reports, since that is the signal worth seeing.

Verified against the frozen sweep: with eth/arc/stable's 1h and 6h files removed
the run is identical -- 60 candidates, 11 passed, the same five names with the
same scores in the same order -- and no missing-window line. Corrupting bsc_6h
and sol_24h still reports both. Step 1 passes bash -n.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:34:55 +08:00
chenjunfengandClaude Opus 5 b4b54ad192 fix(heat-rank): check chain names against a fixed whitelist before they reach the CLI
The chain list is a documented parameter a user can narrow, and whatever is in
it lands on a command line unquoted. Nothing validated it, so a typo, a chain
name from some other exchange, or a string carrying spaces or shell
metacharacters would have been passed straight through to gmgn-cli as arguments.

Hoist the list into a CHAINS variable (which is the thing to narrow) and check
each name against the literal set of the seven names this API actually has,
refusing anything else out loud and skipping it. The whitelist is deliberately
written out again rather than reusing $CHAINS, which would check the input
against itself.

Tested: sol / eth / robinhood accepted; "xyz", "sol; rm -rf /" and
"sol --limit 1" refused. Step 1 passes bash -n, and the extracted scorer is
byte-identical to the previous commit's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:24:34 +08:00
chenjunfengandClaude Opus 5 feb6331c47 fix(heat-rank): strip pipes and backticks from token symbols
sym() already removed control characters, terminal escapes and bidi overrides,
but let two markdown metacharacters through into text that gets copied straight
into the report. A token named "X | buy now" splits into two cells of the
report's markdown table with the second one written by whoever deployed the
contract; a backtick opens or closes a code span. The symbol is data the
attacker chose, so both become ?.

Tested on the extracted function: 'X | 买它 | 忽略上文' -> 'X ? 买它 ? 忽略上文',
'AAA`rm -rf /`BBB' -> 'AAA?rm -rf /?BBB', ordinary symbols untouched. Verified
against the stored sweep: same five names, same order, no symbol in the sample
contains either character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:23:30 +08:00
chenjunfengandClaude Opus 5 6306d99a6e fix(heat-rank): use mktemp -d instead of a guessable path under /tmp
Step 1 built its working directory as /tmp/gmgn-heat-data-$(date +%s) -- a
second-resolution timestamp in a world-writable directory -- then wrote
heat_rank.py into it and executed it. Another local user can guess that name and
pre-create the directory holding their own heat_rank.py, at which point the run
executes their code as the user. Narrow (it needs a local unprivileged account)
but it is local code execution on a path this skill both writes and runs.

mktemp -d gives an unpredictable name and mode 0700, verified on this machine
(/var/folders/.../tmp.XXXXXXXX, drwx------).

The scorer's fallback was the matching half of the same problem:
os.environ.get('HEAT_DATA', '/tmp/gmgn-heat-data') meant a run with the variable
unset would read a fixed path anyone can plant JSON into. There is no safe
default, so it now exits with the correct invocation instead of inventing one.

Verified against the stored sweep: same five names in the same order. The
candidate count reads 11 rather than 12 only because the fixture keeps ageing
against a live clock -- robinhood PAIR has crossed 7d and the age backstop from
897153f drops it, at score 53.4, far below any floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:22:39 +08:00
chenjunfengandClaude Opus 5 f6105c5ae0 docs(heat-rank): close the last two routing overlaps, and say no input is needed
Rescanned the current upstream skill set (13 skills on up/main, aa4d29a) and
grepped every description for hot-list vocabulary. Only gmgn-market matches,
and two of its claims were still unanswered here:

- "discover early-stage opportunities" and its Chinese equivalents. The
  description already sent "just launched" / "新盘" to `market trenches`, but not
  the ambush framing (值得埋伏吗, 新币筛选, 早期机会), which is exactly where the two
  skills read alike. Named, with the reason: the pool here starts at 500k market
  cap and 100k liquidity and never holds a bonding-curve token.
- gmgn-track's "what smart money is buying" / "on-chain alpha" /
  "copy-trade signals". The body table routed it; the description, which is what
  a router actually reads, did not.

Also added what the description never said about using it: no input is needed at
all -- the trigger phrase alone runs it, no address, chain or amount -- and the
four values a user can narrow. Runtime and pacing stay out of the description,
where no skill in this repo puts them; they are already in ## Pacing.

Documentation only: the extracted scorer is byte-identical to the previous
commit's and the stored sweep returns the same five names in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:10:01 +08:00
chenjunfengandClaude Opus 5 bc53c7d34c docs(heat-rank): stop advertising four flags the CLI does not have
argument-hint offered "--chains sol,bsc,... --max-age 7d --top 10
--min-score 60". Measured against `gmgn-cli market trending --help`, none of
the four exists: the CLI takes one `--chain` at a time (the seven-chain sweep
is the Step 1 loop, not a list argument), the age flag is spelled
`--max-created`, and TOP_N / MIN_SCORE are Python constants no flag reaches.
So a request like "this time only sol and bsc" or "raise the floor to 70" led
straight to an invalid command line, even though all four values are in fact
tunable and already listed in Parameters under their real names.

Rename the hint to the names used in the body, and add a paragraph under
Parameters saying these are spoken options rather than flags and mapping each
one to the single place it changes -- including that max-created now moves
together with MAX_AGE_D.

Documentation only: the extracted scorer is byte-identical to the previous
commit's and the stored sweep produces the same five names in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:04:36 +08:00
chenjunfengandClaude Opus 5 897153fddd fix(heat-rank): re-check age locally instead of trusting the server alone
"Recently hot" is the premise of this list, and ## Rules calls age a hard gate
with no compensation logic. Its only enforcer was --max-created 7d in Step 1 --
a server-side filter. The script computes each row's true age from
open_timestamp and used it for the young/mature track split and the 24h-change
blanking, but never compared it to the ceiling, so an endpoint that ignored or
re-interpreted the parameter would put a months-old token on the list under the
word "recent" with nothing local to stop it.

Add MAX_AGE_D = 7.0 and reject any row above it alongside the other
unconditional gates, with its own rejection reason. Document it in Parameters as
tied to --max-created, and say in ## Rules that the gate is enforced in both
places and the two values move together.

Verified against the stored seven-chain sweep: no row is affected -- the oldest
candidate is 6.99d, so the gate is silent, which is the intended steady state.
Re-running the same data with MAX_AGE_D lowered to 5.0 drops robinhood ZZZ
(5.3d) and MEME (6.4d) with reason "age>5d(local backstop)", 5 listed -> 3,
confirming the gate fires and reports itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:03:44 +08:00
chenjunfengandClaude Opus 5 dc69d8fc08 fix(heat-rank): print both score floors instead of only the lower one
The floor is two-valued -- MIN_SCORE for a row every gate could judge,
MIN_SCORE + U_SCORE_ADD for one no manipulation gate could -- and floor_for()
applies it correctly. The diagnostic line printed only MIN_SCORE, so the one
number carried into the report was wrong for half the rows.

The visible damage was in the near-miss block, which exists so the boundary can
be inspected and instead made the skill look broken: on a measured sweep bsc
BREW at 63.4 was dropped while sol LOOM at 63.3 was listed. Correct -- BREW
sits on a chain with no rug model and needed 68 -- but a report claiming a
floor of 60 alongside those two lines reads as padding or a bug.

Three changes, none touching selection:
- the diagnostic line names both floors and how many rows fall in the higher
  tier ("floor 60, or 68 for the 6 of 12 rows no manipulation gate could judge")
- each near miss prints the bar it actually had to clear
- the reporting checklist forbids writing the floor as a single number, and
  still forbids explaining the two tiers, which would disclose what ## Rules
  keeps out of the report

Verified against the stored seven-chain sweep: same five names, same order,
same three near misses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:02:54 +08:00
chenjunfengandClaude Opus 5 9ddda22279 fix(heat-rank): blank the 24h change cell for tokens under a day old
The 24h% column is only meaningful once the token has 24h of history. Below
that the window opens before the token existed, so price_change_percent is
measured from the launch price and prints figures like +128168.0% -- correct
arithmetic, no information about momentum, and wide enough to break the
column's alignment.

This was latent until the previous commit: the reference row used to be
whichever window loaded last, so a young token showed its 1h change under the
24h heading and happened to look plausible. Reading the right window exposed
the real number.

Print n/a instead when age < 1 day, matching how an unknown ATH position is
already rendered. The age column sits immediately left of the cell, so the
reason the cell is empty is visible without a footnote. Rows a day or older
are untouched.

Verified against the stored seven-chain sweep: same five names in the same
order, the four rows aged 2.2d-6.4d keep their 24h figures unchanged
(-21.2 / -0.4 / -18.7 / +39.7), and the 8.2h-old row now reads n/a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:53:46 +08:00
chenjunfengandClaude Opus 5 ad9ff57741 fix(heat-rank): take the reference row from the 24h window, not whichever loaded last
`U.setdefault(key,{})['ref']=t` only guaranteed the key existed -- the assignment
that followed ran unconditionally, so the reference row ended up being whichever
window was loaded last. Iteration order is 24h, 6h, 1h, so for any token that
appeared in the 1h list the reference row was the 1h row.

Most fields read off that row are a current snapshot and are unaffected, but
`price_change_percent` is the change over that row's own window, so the column
headed `24h%` printed a 1h move -- and printed a 24h move for tokens that only
appeared in the 24h list, so the column was not even consistent between rows.
Measured on a stored 7-chain sweep, all five listed tokens were showing their 1h
change: 4Stock printed +29.7% where its 24h move was -21.2%, and MEME printed
+0.1% against a real -18.7%. A reader comparing the list against the exchange
would have found two of five names moving the opposite way.

setdefault now keeps the first window that carried the token, which is 24h for
every listed name (a token absent from the 24h list is already rejected).
Re-scored against the same sweep: same five names, same order, and all five
percentages now match the 24h files exactly. Scores shift by up to 0.5 because a
few snapshot fields differ slightly between the 24h and 1h responses, which were
fetched seconds apart.

One thing this exposes rather than fixes: a token younger than a day now shows
its true 24h change, and for an 8-hour-old token that is +128168%. The number is
correct and useless; a separate change should blank the cell below a day of age.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:46:39 +08:00
chenjunfengandClaude Opus 5 3c49980398 fix(heat-rank): defend the boundary against gmgn-market, and drop three dead symbols
The routing collision was one-sided. This skill's description told the reader
where a raw ranking dump belongs, but `gmgn-market` still claims "trending
tokens, what's pumping, hot coins" and cannot point back here, so an English
"hot coins" or a bare Chinese 热门币 could land on either. Rather than edit that
skill, the deciding test now lives here: what shape of answer is being asked
for. An untouched ranking of N rows in the exchange's own order is
`gmgn-market trending`; a short list that survived risk gates and carries a
score is this skill. A bare ambiguous ask defaults here, because this skill can
point at the raw ranking while the raw ranking cannot screen itself. The
"what this skill is not" table now names the colliding wording too.

Also removed three symbols that never did anything:

- `MAX_BUNDLER = 0.40`, defined and never read. The ceiling that actually fires
  is each chain's leave-one-out p90, and a flat 40% is precisely the mistake
  that deleted whole chains before -- leaving the constant in suggests
  otherwise to the next reader.
- `RANK`, a dict of per-window ranks that nothing consumes.
- `import glob`, unused.

And `near` compared whole candidate dicts by value to decide what was already
listed; it now compares (chain, address), which is what identity means here.

No threshold, weight or gate changed. Re-scored against a stored 7-chain sweep:
the same five names in the same order, the same three near-misses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:45:46 +08:00
chenjunfengandClaude Opus 5 153b72320c gmgn-contract-dd: reshape the report to the dev-score output template
The Display Templates section shipped as a shape table — which section holds
what — and Step 8 asked for `field path → measured value → points → reason`
plus a `executed N / skipped M / held-out K of T` coverage line. Together they
produced a report written for whoever audits the scorer rather than for the
person deciding whether to buy: two dozen field paths, the section weights, and
a full list of every field that could not be read, on a token whose actual
answer was "the contract is fine, the holders are bots".

Replace it with the shape `gmgn-dev-score` already uses for its own report: a
header block carrying the number, narrative sections that each answer one
question a buyer has, and a fixed four-question decision block at the end. The
section is now a fill-in skeleton rather than a description of one, so the same
JSON lays out the same way every run.

Three rules the new section adds:

- No field paths and no internal accounting in the output. Coverage is still
  computed exactly as Step 6 specifies, because it sets the confidence label —
  it just is not printed, and neither are the weights.
- Numbers stay, jargon goes. Every measured value is still printed; what
  changes is the label beside it.
- Exactly one table in the report (`Key numbers`). Everything else is prose.

Step 8 items 2 through 6 are rewritten to match, since as written they mandated
the output the new section forbids. The `stat` disclosure stays mandatory and
moves to plain wording; absent fields are described in words instead of listed
with a reason column; fields that are not applicable on the chain are no longer
listed at all, so a Solana report stops padding itself with a honeypot
non-finding. A `[filtered]` string or a `neutralized … suspicious metadata`
stderr notice remains impossible to drop.

No scoring table, threshold, weight or cap changes. The composite, the grade
bands, the coverage arithmetic and every measurement in "Where the thresholds
came from" are untouched — this is the presentation layer only. Verified on
4Stock (bsc, 0xd270d4e1ec6e6e0d28c0ecb8be966ec75997ffff): 85.7, contract 90.0 /
holders 72.0 / price 100.0, identical before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 19:20:26 +08:00
chenjunfengandClaude Opus 5 a0ef19d543 feat(skills): add gmgn-heat-rank — screened cross-chain hot list
A screening skill that starts from no address: it sweeps all seven chains
across three time windows (21 paced `market trending` calls), gates every
candidate on liquidity, real volume, turnover, concentration and the
rug/dev risk fields, scores the survivors on one cross-chain scale, and
returns however many clear the score floor, capped at ten. It never pads
the list to fill the quota.

Two things it is careful about:

- Bot share is applied as a volume discount rather than a switch, and the
  bundler ceiling is each chain's own leave-one-out p90, so no flat
  threshold silently deletes a whole chain.
- Several manipulation fields read a literal 0 on chains GMGN's analytics
  do not cover — `bot_degen_rate` and `bundler_rate` on base/eth/arc/stable,
  and `rug_ratio` on six of the seven chains. That is "never measured", not
  "clean", so those rows must clear a set of compensating thresholds and a
  score floor 8 points higher instead of inheriting a pass. A row that
  cannot is dropped.

Self-contained: one SKILL.md, no extra files. Data comes only from
`gmgn-cli market trending`; the embedded scorer is read-only and uses the
Python standard library only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 19:07:35 +08:00
chenjunfengandClaude Opus 5 949ecff138 gmgn-token-buy: 按 dev-score 模版整形,并补上实盘跑出来的四个缺口
结构对齐 skills/gmgn-dev-score/SKILL.md 的骨架,流程对齐 DefiLlama
token-research 的 step 形状:

- 去掉 H1 与 `## 适用与边界`(后者三段散文与 description 逐字重复),
  边界改成 `## Run` 里一张四行表。
- `## 流程` → `## Workflow`,`### 第 N 步 · X` → `### Step N — X`,
  每步只留「跑什么 / 关键字段 / 出口」加几条短要点,长散文移进 references。
- `## 订单卡必须说到的东西` → `## What the answer has to contain`。
- 删掉 `### 任一闸门未通过时`(Display Templates 与 Rules 已覆盖)。
- 新增 `references/resolution.md`:Step 1 的消歧判据全文。
- description 修掉一处 he/his 指代,并补上模版要求的收尾一句
  「边界按「这个请求还差什么没做」划分」。

100u 买 MEME 的实盘演练暴露四个缺口,一并补上(`references/thresholds.md`):

1. 新增第三道闸「方向与波动」。实测 robinhood 上的 MEME 24h 成交
   $63.14M、1h 32,446 笔,四道闸里的量闸全过,而它 5 分钟内从
   $0.1107 跌到 $0.0924(−16.5%),且 1m/5m/1h/24h 四个窗口卖出额
   全部大于买入额。量的大小和量的方向是两件事,原来只判前者。
2. 警告项从 6 条补到 10 条,加上响应里本来就有、却没被任何清单读到的
   `top_entrapment_trader_percentage`(实测 55.57%)、
   `top_bot_degen_percentage`(76.78%)、
   `wallet_tags_stat.bundler_wallets / holder_count`(8.5%)、
   `stat.fresh_wallet_rate`。补之前这个币只命中 1 项警告、过了安全闸,
   补之后命中 4 项、正确判不通过。
3. 滑点公式加波动项 `+ |5 分钟涨跌幅| × 0.5`。原公式只吃池深和税率,
   给这个币算出 2.0%,而它 5 分钟动了 16.5%,市价单必然失败。
4. EVM 的 gas 用量按 `pool.exchange` 给默认值(v2 150K / v3 180K /
   v4 200K / 未知 200K),并要求在卡里标明是估算——它是订单卡里唯一
   一个不来自任何字段的数,不给默认值就只能靠模型自己编。

另外记下两条实测字段语义:`price_5m` 是 5 分钟前的起点价而不是涨跌幅;
`lock_summary` 会自相矛盾(`is_locked=true` + `lock_percent="0"` +
`lock_detail[0]` 95% 打进黑洞),判定顺序以 `lock_detail` 为准。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:01:15 +08:00
chenjunfeng 20baf982d6 fix(token-buy): hand a launcher question to gmgn-dev-score
gmgn-dev-score landed upstream 18 minutes after this branch's last commit,
so its buy-intent phrases were never reconciled with this skill's. Both
claim a buy decision; the subject differs. This skill's subject is a COIN
("PENGU 能不能买"), dev-score's is the PERSON who launched it ("这个 dev 的
新盘能不能买", "should I buy his next launch"). A launcher question carries no
token name to resolve and no amount to size, which is everything this skill
does, so it goes to dev-score and comes back here once the user names the
coin.

Adds that clause to the description and one 与 gmgn-dev-score 的分工 line to
the boundaries section. Description 1990 -> 2299 chars (dev-score's own is
2300). Nothing else changed.
2026-09-04 14:33:10 +08:00
chenjunfeng 7d38be6dce refactor(token-buy): make the four-step workflow scannable
Borrows the workflow layout from DefiLlama's token-research skill
(DefiLlama/defillama-skills, token-research/SKILL.md): every step states the
command to run right at the step, then the fields it reads, then its exit
condition — instead of listing all four commands up top and describing them
in prose further down.

- `## Run` opens with a four-row overview table (step / what it does / what it
  runs / its exit), so the whole gated flow is visible at a glance, and states
  the invariant the old text left implicit: each step IS a gate, and a step
  that does not pass never reaches the next one
- each `### 第 N 步` now leads with **跑什么** (a fenced command), **关键字段**
  (what to read, pointing at references/fields.md), **出口** (continue on what,
  stop on what) before the reasoning prose
- step headings name the action: 名字解析成唯一合约 / 三道硬性筛选(量 / 深度 /
  安全)/ 组装订单卡并请求确认; 筛选未通过时 → 任一闸门未通过时
- drops the lead sentences the command blocks made redundant, and the
  restatement of the 7-chain boundary inside step 0

No threshold, field or routing boundary changed.
2026-09-04 14:33:10 +08:00
chenjunfeng 30217f4dc4 refactor(token-buy): reshape to the gmgn-dev-score skill template
The skill was one 369-line file with the command reference, the field
mappings and the whole threshold table inlined as two appendices, and no
fixed shape for the order card. gmgn-dev-score is the template every skill
in this repo follows, so adopt its skeleton:

- description becomes a `>-` block scalar (same content, now readable)
- add the three preamble notices every other skill has and this one was
  missing: `config --check`, only ever `gmgn-cli` (no web search / WebFetch
  / curl / gmgn.ai), and IPv6 is unsupported
- `## Run` — the four read commands, the fixed 4-request budget, the 7-chain
  boundary, and the fact that the CLI only returns numbers: the report is
  written by the model, in the user's language
- `## 订单卡必须说到的东西` — a checklist of what must be *said*, each item
  naming the field it reads
- `## Display Templates` — the order card's shape is now fixed (8 sections,
  when each may be omitted) with the wording left free, replacing the old
  space-padded ASCII block with emoji ticks. Same formatting rules as
  dev-score: plain ascii dollar sign, one decimal on percentages, no emoji,
  no column padding, bold only where named
- `## Rules` — the three hard rules plus the three that follow from the
  7-chain boundary, the hostile-metadata rules, and the report-is-the-whole-
  answer / never-state-a-number-the-fields-do-not-carry rules
- `## References` — appendix A moves to references/fields.md, appendix B to
  references/thresholds.md, and 常见错误 to references/pitfalls.md

No threshold, field mapping or routing boundary changed value; SKILL.md goes
from 369 to 258 lines with the moved text intact in the three reference
files.
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 a0ba4c6a2d fix(token-buy): frame gmgn-swap as this skill's exit, not its rival
gmgn-swap's own description says "Use when user asks to buy, sell, or swap a
token" with no qualifier, and carries a [FINANCIAL EXECUTION] label. This
skill answered that by asserting a plain buy request "is MINE by default",
which is a claim about precedence and gives a reader nothing to check.

Replace the assertion with the two facts that actually decide it. First, the
two skills are not after the same outcome: gmgn-swap signs and submits, this
skill holds no private key and places no order, so a vetted buy ends AT
gmgn-swap by design. Second, gmgn-swap cannot start from a name either — its
--output-token takes a contract address and the only names it resolves are
the SOL/BNB/ETH/USDC currencies — so a request naming a token has to arrive
here whichever skill is asked first.

Then name what genuinely belongs to gmgn-swap instead of gesturing at it:
an explicit skip of the pre-buy check, and everything this skill does not do
at all — selling, percentage sells, limit orders, stop loss, take profit,
trailing orders, multi-wallet batch trading, order status, gas-price lookups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 79b8aa471e fix(token-buy): hand a bare address with no amount to gmgn-contract-dd
The description claimed gmgn-contract-dd "only look[s] up an address and
return[s] no verdict", which is false — it returns a 0-100 composite capped
by GMGN's own rug label — and it contradicted this skill's own body, which
already calls that skill for the safety verdict and defers to it on
disagreement.

It also claimed the bare phrase 能不能买, which gmgn-contract-dd claims
verbatim in its own description and its neighbouring-skills table. Both
skills can serve a bare address, so the phrase alone cannot route.

Split the trigger by whether a buy is being prepared instead: a NAME or
symbol to resolve, or an AMOUNT to size, is this skill — neither of which
gmgn-contract-dd can accept, since it has no search step and no amount
parameter. A bare address with neither is a verdict ask, and taking it
would hand the user an order card he never asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 33e7e62836 fix(skills): defer the safety verdict to gmgn-contract-dd, and refuse to
guess a contract when `market search` is unavailable

Two changes, both driven by running the skill for real rather than only
measuring routing.

1. The safety verdict now names `gmgn-contract-dd` as the owner.

   This skill and `gmgn-contract-dd` were computing two parallel safety
   verdicts on the same contract. Step 2 now states that once step 1 has
   resolved the one right contract, the 0-100 composite is
   `gmgn-contract-dd`'s job — its deductions are finer and each one names
   the field it read — and that its score and red flags are taken as this
   step's result, with its verdict winning any disagreement.

   The local gates are kept as an explicitly-labelled fallback rather
   than deleted, because a live run shows the hand-off does not fire:
   two real queries, Bash restricted to `gmgn-cli`, and
   `gmgn-contract-dd` was invoked in neither. That matches the earlier
   arm-Z result (a hand-off written into a skill body, 20 cases,
   0 invocations). So the fallback is the path that actually executes
   today; deleting the local gates would have removed the safety verdict
   without connecting the replacement. The fallback thresholds are
   unchanged, and the order card must say the verdict was computed
   locally without `gmgn-contract-dd` review.

2. A hard stop when `gmgn-cli market search` is missing.

   The released CLI has no `market search` — that command is added by
   this PR's `aa0e7fd`. On a machine with the released CLI the live run
   showed the model improvise: `market search` failed, and it moved on to
   `market hot-searches --limit 500`, `market trending`, `market trenches`
   to find the token by name. Those are popularity and gain rankings, not
   a name index, so resolving a name through them lands on whatever is
   being pumped — which is exactly the copycat this skill exists to rule
   out. Step 1 now stops on `unknown command 'search'`, names the three
   commands that must not be substituted, and tells the user to upgrade
   the CLI or supply the address directly.

Routing unchanged by all of the above: 16/16 on the 16 plain-buy cases
(11/11 recall, 100% precision) and 18/20 on the 20 due-diligence cases
(8/10 recall, 0 false positives), measured on the real installed state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 c3d0035f90 docs(skills): state the name-resolution exclusivity in the description
The description implied exclusivity by describing what the siblings
cannot do; it never said it directly, so the router had to compose two
clauses to reach it. States it in the first sentence instead.

Measured neutral, not an improvement: on the real installed state
(12 skills + the routing table) the arm with this clause scores exactly
what the arm without it scores — 16/16 on the 16 plain-buy cases
(11/11 recall, 100% precision) and 17/20 on the 20 due-diligence cases
(7/7 on the runnable positives, 0 false positives). Kept for the reader,
not for the router.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 91349504e0 fix(routing): make a plain buy request default to gmgn-token-buy
Measured on the novice phrasing the skill actually targets — a plain
"我想买 200u 的 PENGU" with no due-diligence and no skip-the-checks
wording. Four arms, 16 cases (11 positives / 5 negatives), one live
`claude -p` session per case:

  arm                                      recall     precision  score
  user's machine today (no CLAUDE.md)      3/11 27%   100%       8/16
  + the 11-line routing table              8/11 73%   100%      13/16
  repo skills + the 11-line table         11/11 100%   92%      15/16
  + these 5 lines                         11/11 100%  100%      16/16

Three changes:

- CLAUDE.md: the `gmgn-swap` registry row claimed `"buy this token"`
  unconditionally. The earlier fix only narrowed the quick-route table,
  not the registry, so a plain buy request still had a competing claim.
- CLAUDE.md: one quick-route row making `gmgn-token-buy` the explicit
  DEFAULT for any plain buy request. The failure mode being fixed is
  silence, not theft — every miss in the 8/11 arm was "no skill fired".
- SKILL.md description: lead with the plain phrasings, and state that a
  plain buy request is this skill's by default rather than framing the
  boundary defensively around what it hands to gmgn-swap.

Also adds a hard boundary for sells, limit orders and order-status
queries, which fixes the one false positive in the 15/16 arm
("帮我挂个 BONK 的限价单" bounced from gmgn-swap into this skill).

Negatives hold in every arm: "直接买 200u 的 BONK,不用检查了" routes to
gmgn-swap, never here — an explicit skip-the-checks instruction must not
be intercepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 04dadf6fa9 fix(routing): register the 5 unregistered skills and split the buy-intent route
The routing table listed only 7 of the repo's 12 skills. gmgn-contract-dd,
gmgn-cooking, gmgn-holder-analysis, gmgn-wallet-analysis and gmgn-token-buy
were absent, so an unregistered skill effectively did not exist to the router.

Three routes also sent buy-side asks somewhere that cannot answer them:

  - "is this token safe" / "check this token" went to gmgn-token, which
    returns raw fields and no verdict. Split into three rows: a token name or
    symbol with buy intent goes to gmgn-token-buy (the only skill that
    resolves a name to the one right contract and sizes the order); a bare
    address wanting a score goes to gmgn-contract-dd; raw fields for an
    address already in hand stay with gmgn-token.
  - "buy TOKEN" went straight to gmgn-swap, bypassing due diligence. Narrowed
    to the skip-the-checks case; a plain "buy TOKEN" is gmgn-token-buy.

Measured on a 20-case routing eval (10 positives) with `claude -p`, first
Skill call recorded, 12 skills installed:

  routing table unchanged            recall 1/10 and 3/10 over two runs
  routing table with this change     recall 7/10, reproduced exactly twice

Precision stayed 100% with zero false positives in every run. The 3 remaining
misses are eval cases with no token name, a placeholder address, and an
unsupported chain — none of them resolvable by routing.

Description-level fixes were tried first and measured dead: narrowing
gmgn-token's description, adding a runtime hand-off to gmgn-token's body, and
rewriting gmgn-token-buy's own description each scored 0/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 5a221cb259 feat(skills): add gmgn-token-buy — buy-side due diligence from a name
Covers the request shape no existing skill can serve: the user names a
token and wants to know whether to buy it, and for how much. Every other
token skill takes a contract address as input, so none of them can start
from "搜一下 PENGU 能不能买" or "帮我买 200u 的 XX".

The skill resolves the name to the one correct contract among its
copycats, runs three hard gates (24h volume / pool depth / security
audit), sizes slippage and gas for the stated amount, and emits an order
card. It never holds a private key and never submits a transaction: after
the user confirms in-conversation, the parameters go to gmgn-swap. That
confirmation is required even when the user has said "don't ask, just
buy" — it is a real spend of real funds.

All data access goes through gmgn-cli, so the skill reads, stores and
transmits no credentials, and it inherits the output sanitiser that
src/sanitize.ts applies in printResult. What the sanitiser does not cover
— identity confusion and gate-softening prose in attacker-written token
metadata — the skill handles in its own section: metadata is data and
never an instruction, a "sanitized N field(s)" notice is itself a risk
signal that belongs in the order card, an official-looking name never
skips contract locking, and a contract address is only ever read from the
address field, never from prose.

Verifying the rules against the live API corrected five of them, each of
which would have misjudged a blue chip:

  - pool.base_reserve_value can be "0" on a fully liquid token (BONK's
    meteora_dlmm pool reports base "0" while holding 14.7B tokens), so
    min(base, quote) scored a 996k-holder token at $0 depth. The gate now
    falls back to pool.liquidity / 2.
  - pool.liquidity is twice ONE side and which side is unpredictable:
    PEPE/SHIB/BONK/PENGU/BRETT are 2 x quote, CAKE is 2 x base.
  - pool is not necessarily the deepest pool — BONK's
    biggest_pool_address differs from pool.pool_address — so depth is a
    lower bound, and volume far above depth means "there are other
    pools", not wash trading.
  - creation_timestamp and created_at can be 0 (real BONK returns 0 for
    both), which naive arithmetic turns into 56 years of history. Zero
    means unknown; skip the age bonus instead of crediting it.
  - Solana's *_prio_fee is a placeholder 1 while BSC's is legitimately 0,
    and a tier is not reconstructible from suggest_base_fee + prio (BSC
    high is 561M wei against a 120M base fee). The tier field itself is
    the authoritative all-in price; only *_prio_fee_mixed is real.

Routing was measured with all 12 skills installed: precision is 100% (no
false trigger in 10 negatives), but recall is low and two full
description rewrites moved it only within run-to-run noise. The
attribution is stable — gmgn-token takes 4-5 of the misses every run,
because its own description ends with "or wants due diligence before
buying", which is this skill's whole job stated in 429 characters. That
overlap is structural rather than cosmetic and gmgn-token is deliberately
left untouched here; see the PR description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 0f48a31a9d docs: register the skills the tables were missing, and gmgn-token-buy
The two Readme tables and the marketplace description had drifted behind
the repo: both tables listed 8 skills and the marketplace blurb said
"9 GMGN skills", while skills/ actually shipped 11. gmgn-contract-dd,
gmgn-holder-analysis and gmgn-wallet-analysis were installed but
undiscoverable from the docs.

Rather than bump a stale count by one, complete both tables and the
blurb to the full set, including the gmgn-token-buy skill added in this
branch. Each new row is placed next to the skill it reads like, so the
tables still group by subject: contract-dd and holder-analysis after
gmgn-token, wallet-analysis after gmgn-portfolio, and token-buy
immediately before gmgn-swap, which is what it hands its order card to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:33:10 +08:00
chenjunfeng 016535ffcf feat(market): implement the documented market search command
`skills/gmgn-market/SKILL.md` has documented `market search` in full — the
capability table, the rate-limit table, parameters, response fields and usage
examples — but no implementation ever landed: `/v1/market/search` appears
nowhere in `src/`, and `gmgn-cli market --help` does not list the command. Any
agent following the skill doc hits a hard failure.

This adds the command behind the documented contract:

- `-q, --query` (required), `--chain`, `--launchpad-platform` (repeatable,
  max 50), `--is-og`, `--is-launched`, `--order-by weight`, `--raw`.
- `--chain` is shape-checked rather than matched against the 7-chain core
  allowlist, because search accepts any chain the platform has enabled
  (tron / monad / megaeth / xlayer / hyperevm) and a stale local list would
  reject valid searches.
- `--query` is stripped of C0/C1 control, zero-width and bidi-override
  characters before the documented 1-100 length check, and the cleaned string
  is what gets sent. Length is counted in code points so emoji tickers are not
  over-counted. Without this, `pe<ZWSP>pe` is a different search term than
  `pepe` and can be used to point a caller at a copycat contract.

One upstream deviation is worth calling out: the endpoint does not understand
`chain=all`. It answers with `{"coins":[],"wallets":[]}` rather than an error,
which an agent reads as "no such token" — strictly worse than failing. Since
the skill doc promises `all` works, the CLI accepts that spelling and searches
all chains by omitting the filter, warning on stderr so the substitution is
visible. This follows the existing `parseDuration` precedent of correcting a
known-ambiguous input loudly instead of rejecting it.

Verified against the live API: `-q pepe --chain eth` returns fields identical
to `token info` for the same token (`liquidity`, `volume_24h`, `swaps_1h/24h`,
`holder_count` all byte-for-byte equal), bare-contract-address lookup resolves
the chain in one call, and an address on an unsupported chain returns an empty
result set.
2026-09-04 14:33:10 +08:00
chenjunfengandClaude Opus 5 ffc8b741ce docs(gmgn-contract-dd): append the gmgn-token-buy boundary
Purely additive — the existing description, its trigger list (尽调 / 打个分 /
这个币安全吗 / 能不能买 / rug check / bare address), the neighbouring-skills
table and every scoring rule are untouched, so nothing this skill triggers on
today stops triggering.

What is added is only the missing half of one phrase. 能不能买 is claimed by
this skill and by gmgn-token-buy, and only one of them can serve it: the input
here is --address, and this skill resolves no names, so a named token plus buy
intent (帮我买 200u 的 PENGU, PENGU 能不能买, 能不能冲) has no path through it.
The appended clauses say so, and say the two are sequential — token-buy calls
this skill for the safety verdict — so a bare address still scores here exactly
as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:32:48 +08:00
chenjunfengandClaude Opus 5 4a106ee773 docs(gmgn-swap): append the gmgn-token-buy boundary to the description
Purely additive. The original description sentence is kept byte-for-byte;
the appended clauses only say which buys arrive here pre-vetted and which
start at gmgn-token-buy, so no existing trigger is removed or narrowed.

Why the boundary is stated here at all: the pre-swap check in this skill
reads only is_honeypot and rug_ratio, and --output-token takes a contract
address (the only names resolved here are the SOL/BNB/ETH/USDC currencies),
so a same-name copycat is a loss this skill has no way to catch. Selling,
swaps, batch trading, limit/stop/take-profit/trailing orders, order status
and gas prices stay unconditionally this skill's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:31:45 +08:00
chenjunfengandClaude Opus 5 9b90a3eaea docs(dev-score): fix the report's shape with a Display Templates section
Removing the script's layout engine also removed the only thing keeping the
report a stable shape. Two runs of the same JSON came out with different heading
levels, different table/bullet choices and bold on nearly every clause, and one
leaked the fullwidth dollar sign — a convention that exists only to protect this
file's own text from positional-argument substitution — into user-facing output.

Display Templates fixes the shape without fixing the wording: nine sections in a
fixed order, each with its block type (which sections are tables, which are
prose), sections 1-5 and 9 always present, 6-8 conditional on their data being
there, no reordering, no merging, no tenth section. Plus the formatting rules
that were being decided per run: plain ascii dollar sign with thousands
separators, one decimal on percentages, no emoji, no box drawing, no column
padding, and bold restricted to the verdict number, the band, and the four
labels of the action section.

Section names are given in English so they are translated into the user's
language rather than printed as-is, so the file stays English-only.

The content checklist above it said "order, headings and phrasing are yours",
which now contradicts this section: it is reworded to own what must be *said*
while Display Templates owns *where it goes*, and its bullets are reordered to
match the section order.

This is an instructional constraint, not a mechanical one — the script prints no
report, so nothing enforces the shape at runtime. That is the intended trade of
keeping output format out of the calculation code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:00:57 +08:00
chenjunfengandClaude Opus 5 9b6e7023da refactor(dev-score): split the skill into a slim SKILL.md, a calc script and references
SKILL.md was a single 1,854-line file carrying the routing, the whole 1,432-line
inline Python, the scoring rationale and a 78-column layout engine with 142
hardcoded zh/en string pairs. All of it loaded into context on every invocation.

Now:

- SKILL.md (95 lines) — routing, how to run, and what the answer must contain as
  a checklist, not a template. Pure English: the trigger phrases no longer name a
  language, and the model translates the concepts itself.
- dev_score.py — fetch and calculation only. Emits one JSON object on stdout and
  nothing else; progress notes go to stderr. The layout engine and every zh/en
  string pair are gone, so the output format is not fixed by the script.
- references/scoring.md — the model: gates, severity, the thin-record shrink,
  tiers, caps, verdict bands, and why each is shaped that way.
- references/fields.md — every JSON key emitted, every confirmed API field, the
  chain probe, rate limits, and the data caveats.
- references/glossary.md — only the trader slang that has no literal English
  equivalent, plus the tier and band renderings.

Scoring is unchanged. Verified old vs new across 8 mock scenarios (empty index,
empty-but-trading, factory, implausible peak, bot-scale, hostile input, both
factory-gate boundaries, LP drain): identical totals, conduct, power, tiers and
term breakdowns, and matching not-scored reasons. Prose exits became reason codes
(index_empty / not_a_person / peak_implausible / never_launched) and the flagship
note became a status code.

safe() is kept as input sanitisation, now also applied to token addresses and to
severity_by_token keys, so an attacker-chosen symbol still cannot forge a line.
Truncate first, sanitise second — slicing a sanitised value strips its fence.

Per-invocation context drops from 151,566 to 9,652 bytes; references are read on
demand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:09:21 +08:00
chenjunfengandClaude Opus 5 f03d44ea44 docs(gmgn-dev-score): name every concept in English, map the zh report labels
The file's prose, headings and script comments carried Chinese concept names
(信誉 / 实力 / 综合 / 割率 / 狠度 / 抽水倍数 / 代表作 …), which made the rules
readable only to a Chinese reader even though the routing and the English
report are fully supported. Every concept is now named in English, taken from
the script's own `_(zh, en)` pairs so the document and the report agree:
CONDUCT / POWER / TOTAL, dump / open-dump / dump rate, severity, pull multiple,
1st sell, the curve, flagship, walk-away & CTO rate, self-snipe rate, book
quality, unaccounted exit, untradeable rate, and the five tiers systematic /
frequent / occasional / unproven / measured clean. Report lines quoted as
evidence inside comments are translated with them, so a comment explaining a
past defect no longer requires reading the Chinese output to follow.

Adds a `### Terms` section: English concept, the exact zh wording the report
prints, and a one-line definition — 17 concepts, the 5 tiers, and the 9 section
headers. Terms that translate one-to-one (peak, drawdown, survival rate,
graduation rate, holders, market cap) are stated to be English-only and are
deliberately not listed, so the table stays short enough to read.

Also fixes three pass artifacts it uncovered: `flagshipdumped coins` in two
gate-table rows and one comment, and the doubled `dump (dump)` /
`severity (severity)` labels.

What is deliberately still Chinese: the frontmatter trigger phrases and the
routing-table phrasings (they are the literal user input the router matches —
translating them breaks Chinese routing), the `_()` zh strings that ARE the
Chinese report, the CJK punctuation the line-breaker demonstrates in its own
comments, and the zh half of the new glossary. The routing boundary lines now
lead in English with the Chinese phrasing in parentheses.

Verified: description folds with no space injected inside a Chinese phrase and
all 41 trigger phrases intact (22 zh / 19 en); script compiles in both
languages; 8 mock scenarios x 2 languages all rc=0, width <= 78, brackets
balanced, no CJK outside 「」 in English output; no ascii dollar-before-digit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:52:00 +08:00
chenjunfengandClaude Opus 5 fbf533fc9f fix(gmgn-dev-score): English-context correctness and bracket balance
Four defects found while checking whether the skill routes correctly for a
question asked in English or another language.

Routing itself was fine -- the description's spine, reasoning and DO-NOT-USE
logic are English, and 信誉/实力 carry inline English glosses -- but:

- Four hardcoded strings leaked 信誉/综合/抽水 into the English report while
  its header reads TOTAL/CONDUCT/POWER. Now CONDUCT/TOTAL/pull.

- LANG had no selection rule, only the comment "'zh' or 'en'", with 'zh'
  listed first. State it explicitly: LANG follows the language the user asked
  in, not the chain and not the model's default. The report is the whole
  deliverable, so handing an English trader a Chinese one makes it unreadable.

- Trigger phrases were lopsided: 22 zh vs 5 en positive, and every one of the
  9 DO-NOT-USE phrases was Chinese. The real English-context risk is
  over-triggering on copy-trade and profitability questions that belong to
  gmgn-wallet-score, and nothing guarded against it. Now 41 phrases, 19
  English, 10 of them on the negative side, plus a note on the
  "how is this dev's reputation" (profile) vs "dev score" (decision) boundary.
  Also folds a pre-existing Chinese phrase that YAML's >- had split across
  lines, inserting a space inside it.

- Language-independent: `to = safe(addr, 64)` printed as `m['to'][:8]` sliced
  off the closing 」 that safe() adds, so an attacker-supplied to_address
  rendered as 「判为割 100… with no visible end and the report's own words
  appeared to continue inside the quoted name -- exactly the confusion the
  quoting exists to prevent. Truncate first, sanitise second, via a separate
  to_disp; `to` stays full so the fund_from_address comparison still matches.
  Real addresses display unchanged; a hostile value shows as ?.

Verified: script compiles under both languages; 6 mock scenarios x 2 languages
all rc=0 with width <=78 and balanced brackets (hostile 4/4); zero CJK in
English output; description folds with no space inserted inside any Chinese
phrase; no ascii dollar-sign-before-digit hazards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:26:56 +08:00
chenjunfengandClaude Opus 5 9b20187d74 feat(dev-score): score a launcher on 信誉 and 实力, and say when to buy
Adds gmgn-dev-score, a risk gate you consult before buying a launch, as a
single self-contained SKILL.md.

The skill answers one question — 我要不要买他下一个盘 — and keeps two things
apart that must never cancel each other out: 信誉 (will he dump on you, built
only from what he did with his own hands in his own coins) and 实力 (has he
ever built anything, built only from what the market did with those coins).
They combine asymmetrically: 综合 = 信誉 + a bonus 实力 can earn, and the 割率
gate caps 综合 before the bonus lands, so a big coin can never buy a dumper a
passing score.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:58:15 +08:00
chenjunfengandClaude Opus 5 c4d612d2b3 docs(contract-dd): fold the routing split into this file and drop the CLAUDE.md PR
#213 fixed the collision by editing CLAUDE.md's Quick Decision Guide. Keeping the
library consistent matters more than closing that collision from outside: no other
skill in skills/ reaches into a project doc, and this skill should not be the first.
So the split now lives where the rest of the repo puts it — in the skill file — and
#213 is closed rather than merged.

The existing hand-off table only covered the outbound direction (user wants the
explanation, not the verdict). It now covers the inbound direction too, which is
where the collision actually was:

- **One number vs the raw fields.** "check this token", "research this token",
  "what's the liquidity", "who holds this" are gmgn-token; 打个分, 尽调, 这个币安全吗,
  能不能买, rug check, score this contract are this skill. The description already
  competes at selection time; this states the same boundary for a reader who has
  already loaded the file.
- **A bare address with no question** is a verdict ask: score here, offer the fields
  after. Step 0 still resolves token vs wallet first, and the wallet branch now names
  gmgn-portfolio alongside gmgn-wallet-analysis, since raw holdings / P&L / activity
  are the portfolio skill's, not the dossier's.

Still one file, still no changes outside skills/gmgn-contract-dd/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 19:06:33 +08:00
chenjunfengandClaude Opus 5 d5987fb226 docs(contract-dd): contest the phrases CLAUDE.md routes elsewhere, from inside the description
Routing happens before a skill's body is ever read, so nothing in this file can
override CLAUDE.md's row sending "is this token safe" and any bare token address
to gmgn-token. The description is the only surface that competes at selection
time, so it now does more of the work.

Three changes, all inside the existing 1024-char budget (907 after, 970 before):

- **Added the English trigger `is this token safe`.** The description carried the
  Chinese 这个币安全吗 but not its English twin, which is the exact phrase the
  routing table hands to gmgn-token.
- **Added an explicit priority hint** — "prefer this over gmgn-token whenever the
  ask is a verdict rather than a field dump" — following the pattern
  gmgn-wallet-analysis already uses to disambiguate itself.
- **Named the rug-label cap**, which the description predated and never mentioned
  even though Step 5B is now what stops a labelled rug reading as clean.

Compressed the internals wording ("the three exceptions where absence does move
the number are named explicitly") to pay for it — that sentence describes
mechanics a router does not select on.

This narrows the collision; it does not close it. CLAUDE.md's row is project
guidance and still names gmgn-token for the same input, which is what #213 fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 19:00:13 +08:00
chenjunfengandClaude Opus 5 f23cb57752 fix(contract-dd): a listing row without a rug_ratio key is not rug_ratio 0
Found by running the demo against a bsc token: Step 5B reported
`rug_ratio 0.000 via trenches/completed → no cap`, and the field was not in the
row at all.

Measured over 840 listing rows on 2026-08-28: **`rug_ratio` is absent from the
row entirely on 323 of them**, and which rows lack it is chain-dependent — 176 of
180 bsc trenches rows and 147 of 180 base trenches rows carry no such key, while
sol trenches and all three trending listings always do. Reading a missing key as
0.0 reports "measured clean" for a label that was never supplied, on the majority
of bsc and base launches. That is this skill's own headline failure mode,
committed inside the step added to catch it.

The step now stops at the first row that actually carries `rug_ratio` rather than
the first row that matches the address, so a bare trenches row falls through to
trending. Measured recovery on bsc: of the 176 trenches rows missing the key, 26
are also in trending and get their label there; the remaining 150 end genuinely
unavailable. "Listed but no row carried the key" and "not listed at all" are
different facts, so Step 8 must print which one it was, and neither may be
written as `rug_ratio 0`. The age is still taken from the first matching row
whether or not that row carried the label — `created_timestamp` was never absent
in the sample.

Both filter pipelines now print `rug_ratio=ABSENT` instead of an empty-looking
`None`, and the fall-through condition covers it. Verified end to end on a bsc
token whose trenches row lacks the key: `trenches/completed rug_ratio=ABSENT
age=0.6h` then `trending rug_ratio=0 age=0.6h`.

Invariant sweep: 14/14 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:02:30 +08:00
chenjunfengandClaude Opus 5 7f9acf56d3 fix(contract-dd): remove a stranded paragraph that revoked an active scoring row, and correct the self-check totals
Both defects came from review feedback and both are mine — incomplete patches
whose verification checked the strings I knew I had changed instead of checking
the invariants.

**A paragraph told the implementer not to apply a deduction the table applies.**
When `vol_ratio < 0.20` was promoted to a scored row, the replacement only
covered the first paragraph of the old reported-not-scored argument. The second
paragraph survived and still read "compute `vol_ratio` … and **do not deduct on
it**", plus "**That test has not been run**" — describing the rate-limit
interruption that the completed three-batch lift test had already superseded four
commits earlier. An implementer copying the scoring table would have shipped the
−18; one reading the prose would have skipped it. Deleted, and the row is now
stated as ACTIVE at that spot.

Acting on the reviewer's wider point rather than just the instance: a note under
the Step 5 table now says every row in every scoring table is active and this
skill has no candidate, disabled or "pending" rules anywhere — a rule that failed
measurement was deleted outright rather than parked, which is what happened to
`sell_volume_24h / buy_volume_24h` and to `vol_ratio`'s own 0.20–0.40 tier. The
tables are safe to implement on their own.

**The coverage self-check named the wrong totals.** It said "does not equal 22 or
24" while the inventory it checks is 23 on `sol` and 25 on the EVM chains — the
numbers were updated in the "Inventory total" sentence when the price section
gained `vol_ratio` and missed four lines below. Since executed + skipped +
held-out always sums to the inventory total by construction, the check would have
mis-fired on every correct run, which is the worst possible failure for a line
whose only job is to catch a miscount. Corrected, and the identity is now spelled
out in both directions.

To stop repeating the class of mistake, verification is now an invariant sweep
rather than a string grep: no prose may revoke a row the tables mark active
(excluding availability rules, which legitimately say "do not deduct" on struct
defaults), every inventory-total statement must say 23/25, the Step 5 table row
count must equal the inventory's Price count, all caps must be 59 or 79, every
Step reference must resolve, and the file-hygiene checks. All fourteen pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:47:03 +08:00
chenjunfengandClaude Opus 5 b4749a15f8 feat(contract-dd): report token age, the one open item that did affect the output
Checked the deliberately-unfixed list for actual user impact. One item did have
some, and it has a free fix.

**A few-hours-old launch could read "relatively clean · evidence sufficient" with
nothing on the line saying it was young.** Measured: a pump.fun launch scored
89.4 with no failed check beyond thin liquidity, a small holder count and an 8%
drawdown — all true, none of them conveying age. The `len(kline.list)` deduction
only reaches tokens under 24 candles, so six hours of history escapes it.

`token info` cannot supply the age: `creation_timestamp`, `open_timestamp` and
`pool.creation_timestamp` all read 0 there, on USDC as well as on a launch. But
the Step 5B listing row already carries it, so it costs no extra call — the
filters now extract it alongside `rug_ratio`, and Step 8 prints it next to the
grade. The field differs per listing, like the envelope does: `trenches` rows
carry `created_timestamp` (with `open_timestamp` at 0 on ungraduated
`near_completion` tokens) while `trending` rows carry `open_timestamp` and no
`created_timestamp`. Both pipelines were re-run and verified —
`trending rug_ratio=1 age=21.7h` on ANTSEM, `trenches/near_completion
rug_ratio=0 age=3.6h` on a live launch.

Also checked, and no change needed: **`stat.creator_hold_rate` is trustworthy.**
It never fired across 25 tokens, which could have been a false pass hiding a
holding dev. It is not: on six live launches the rate is non-zero exactly when
`stat.creator_token_balance` and `dev.creator_token_balance` are non-zero, and it
agrees with the independent `creator_balance_rate` on the trenches row. MERP
reads 1.35% and fone2 0.82% — real holdings that sit below the >2% tier. Whether
that tier is set too high for bonding-curve launches is a calibration question,
not a data bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:33:03 +08:00
chenjunfengandClaude Opus 5 bbfaefa14f docs(contract-dd): cite the repo's own definition of rug_ratio behind Step 5B's bands
The bands were chosen by measurement, but the field's meaning was inferred. It is
documented in this repo: src/commands/market.ts describes rug_ratio as a "rug
pull risk score (0-1, e.g. 0.3 to exclude rugs)", and the CLI's own
--filter-preset safe for market trenches is built on max_rug_ratio: 0.3.
docs/workflow-early-project-screening.md already flags > 0.3 and
docs/workflow-project-deep-report.md uses > 0.5.

So both of Step 5B's cutoffs match thresholds already in use across the project's
source and docs, arrived at independently from live measurement. That removes the
one input to this step whose semantics I had not verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:28:31 +08:00
chenjunfengandClaude Opus 5 24c0df5e91 fix(contract-dd): Step 5B must filter the listings in the shell, not read them
The step added in the previous commit was a context bomb. Measured 2026-08-28:
`market trenches --chain sol --raw` is **757,598 bytes** and
`market trending --interval 24h --limit 100 --raw` is **233,821 bytes**, against
about 5 KB for `token info` and 15 KB for a 100-candle `kline`. Reading the
trenches payload to find one address costs on the order of 200,000 tokens of
context for a single number — more than the entire rest of the procedure
combined, and enough to blow the window before any scoring happens. `--limit`
does not help: it caps rows per category at 80, and an arbitrary-address lookup
needs the whole listing.

Step 5B now specifies two verified shell pipelines that return one short line —
16 and 11 bytes measured — instead of the payload, and the Sub-commands block
marks both calls as filter-only. Both pipelines were run end to end before being
written down.

Two details that make the difference between working and silently wrong:

- The address must be `export`ed. In `VAR=x cmd | filter` the assignment applies
  only to `cmd`, so the filter would not see it.
- The two responses have different shapes — `trenches` is unwrapped with the
  bucket names at top level, `trending` keeps the `{"code":…,"data":…}` envelope
  with rows at `data.rank` — so the two scripts are not interchangeable. Pointing
  the wrong one at either endpoint finds nothing and reports `rug_ratio`
  unavailable, which is indistinguishable from an unlisted token: a silent false
  negative on exactly the tokens the step exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:25:14 +08:00
chenjunfengandClaude Opus 5 77ec2d436e feat(contract-dd): read GMGN's own rug label and cap on it (Step 5B)
Closes the failure the verification pass found: on ten tokens carrying
`rug_ratio >= 0.5`, three scored "relatively clean" — ANTSEM at `rug_ratio: 1.00`
scored 92.8 — seven scored "mixed", none reached "high risk", and nine reported
"evidence sufficient". A verdict number that calls a maximum-rug-label token
clean at high confidence is worse than no number.

**There is no address lookup for `rug_ratio`.** Verified absent from
`token info`, `token security` and `token pool`, and `gmgn-cli` has no
`market search` sub-command in any version measured (not in the installed 1.5.8,
not in the repo source at 1.5.9 — though `skills/gmgn-market/SKILL.md` documents
one, which is a separate bug in that file). So Step 5B scans the per-chain
listings and stops on the first match: `market trenches` first, where launches
live, then `market trending --interval 24h --limit 100`, where actively traded
tokens live.

Bands, each measured rather than picked:

- **`rug_ratio >= 0.50` → cap 59.** All ten labelled rugs move to "high risk",
  removing three false "relatively clean" verdicts. Of six fresh launches scored
  the same day only MERP is touched — `rug_ratio: 1.00`, composite 89.6.
- **`0.30–0.50` → cap 79.** Scored five tokens in the band: RAX 80.0, BANDOS
  83.5, POKEMON 88.0 and AFD 90.5 all read "relatively clean" and are all
  withheld; JWA at 76.5 was already below. Four of five changed.
- **`< 0.30` → nothing.** Deliberately inert; a cap there would be a guess.

Caps only, never a bonus, and `rug_ratio: 0` is not evidence of safety — 302 of
400 trending rows read exactly 0, so zero is the population default. Unlike Step
2's caps, which sit at 79 because they fire on an absent field, this one fires on
a measured positive, which is why its lower band goes to 59.

**Absence cannot hurt a token, structurally.** The listings rank churning tokens,
so established assets are not in them: all six bluechips in this file were absent
from every listing across four chains, so the cap has no path to a bluechip.
Acceptance test run live — USDC 100.0, RAY 88.4, CAKE 100.0 all unlisted and
uncapped; ANTSEM 92.8 → `rug_ratio 1.000` via trending → **59 "high risk"**.

`rug_ratio` is a cross-check, not a scored check, and is deliberately **not** in
the Step 6 inventory: coverage measures how much of the token's own contract,
holder and price data could be read, while this is GMGN's label about the token
and its availability tracks whether the token happens to be ranked. Putting it in
the denominator would dock every established token's coverage for being
established — the inversion Step 4 already rejects for the `stat` block. So the
inventory totals stay 23 and 25 and every verified coverage figure still holds.

Also recorded: the two listings have **different response shapes** — `trenches`
is unwrapped with the bucket names at top level, `trending` keeps the
`{"code":…,"data":…}` envelope — and assuming either for both reads as empty
data, i.e. a false "unavailable" on exactly the tokens this step exists for. The
Notes now carry a per-command shape table instead of the old blanket claim.

**The fresh-launch gap is dropped as a target, with reasons.** After Step 5B the
highest fresh launch is GROKCHAIN at 89.4 with `rug_ratio: 0.00` and no failed
check beyond thin liquidity, a small holder count and an 8% drawdown, so the gap
stays about −1. That is not a mis-score: the original calibration treated "young"
as a proxy for "risky". Youth is conveyed by the `len(kline.list)` deduction and
the confidence line, not by a depressed composite. The metric that matters is
whether a labelled rug can read as clean, and that is now closed by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:21:04 +08:00
chenjunfengandClaude Opus 5 f5114f703c fix(contract-dd): the calibration gap does not survive re-measurement — record it
Verification of the five open items. Four came back clean; the first one did not,
and it is the most important thing in this file.

**The +15.3 bluechip-vs-fresh-launch gap is gone.** Re-run against six current
fresh launches (three pump.fun near_completion on sol, three flap on bsc, all
over $20K 24h volume and $10K liquidity): lowest bluechip 88.4, highest fresh
launch 89.6, **gap −1.2**, and three of six read "relatively clean". The original
+15.3 came from four launches captured at one moment.

**Worse: the composite does not separate tokens GMGN itself labels as rugs.**
Ten tokens with `rug_ratio >= 0.5` and over $20K of 24h volume, from
`market trending`: three read "relatively clean" — ANTSEM at `rug_ratio: 1.00`
scored 92.8, GASSPAS at 0.54 scored 96.5, Pistacio at 0.96 scored 86.8 — seven
read "mixed", and none reached "high risk". Median 73.8, nine of ten at 100%
coverage / "evidence sufficient".

This is not a regression from the review commits; if anything the new
`vol_ratio` row helped, firing on 2 of the 10. Two causes, both now recorded:
`rug_ratio` lives on `market trending` / `market trenches`, outside the three
endpoints this skill calls, so the most predictive field GMGN publishes is
outside the design; and the rows that fire hardest on labelled rugs are
`drawdown` (8/10) and `worst_candle` (5/10), which detect a token that has
already dumped rather than one about to.

The grade line and the appendix now say so, and Step 6 states that "relatively
clean" means "no measured red flag among the fields this skill reads" rather than
"not a rug". Fixing it properly is a threshold-and-scope decision — either bring
`rug_ratio` in as a fourth endpoint or tighten the `stat` tiers materially, then
re-calibrate against a labelled sample far larger than ten.

**Per-row firing rates, over the 25 tokens scored end to end this session.**
Eleven of twenty-seven rows never fired once, including every permission field in
Step 2 across all seven chains — so the whole chain-mode branch contributed
nothing, and the 0.45 contract weight is carried by liquidity,
`creator_created_count`, `pool_shrink`, `image_dup_count` and `len(kline.list)`.
Also never fired: the tax row, `creator_hold_rate`, `fresh_wallet_rate`,
`private_vault_hold_rate`, and `price / price_24h`. The full table replaces the
placeholder that said firing rates should be measured someday.

**The other four items came back clean.**
- arc, stable and robinhood score end to end, inventory closing at 25 on each
  (24/1/0, 23/2/0, 23/2/0). All three populate `stat`.
- Executing the prose by hand, step by step, on FEFER/stable lands on exactly
  92.3 — the same number the script produced. The rules are executable as
  written.
- No live `is_honeypot === true` token surfaced in 300 trending rows across bsc,
  base and eth, so the Step 7 change could not be re-tested against one directly.
  It is monotone in the safe direction: requiring `privileges` present-and-empty
  can only make the exemption harder to open, never easier, so it cannot turn a
  honeypot clean. `privileges` was `null` on every token measured across all
  seven chains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:01:21 +08:00
chenjunfengandClaude Opus 5 de83ae7012 chore(contract-dd): move the CLAUDE.md, marketplace and docs edits to their own PR
This PR is back to what it was scoped as: one new file, skills/gmgn-contract-dd/SKILL.md.
The routing-table registration and the token-security field-shape corrections in
docs/workflow-token-due-diligence.md are on feat/contract-dd-bookkeeping instead,
to be merged after this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:40:02 +08:00
chenjunfengandClaude Opus 5 35966e4095 feat(contract-dd): the volume row ships, with the noise tier measured out of it
The lift test the previous commit left open is done, and it split the row in two.

`market trenches` cannot label this measurement at all: all 32 sampled trenches
tokens returned 1 to 17 candles against the 40 `vol_ratio` needs. Re-sourced from
`market trending --interval 24h --limit 100` on sol, bsc and base — 299 tokens
over $2K of 24h volume, which are old enough to have candles — then three
sequential batches, 47 scorable `rug_ratio > 0` against 47 scorable
`rug_ratio = 0`:

  < 0.20    -> -18   risky 10/47 (21.3%)  clean 2/47 (4.3%)   lift 5.00x  p=0.013
  0.20-0.40 -> -8    risky  9/47 (19.1%)  clean 9/47 (19.1%)  lift 1.00x  p=1.000
  both as one rule   risky 19/47 (40.4%)  clean 11/47 (23.4%) lift 1.73x  p=0.077

The two tiers are not the same rule. A collapse below 20% separates the
populations at 5x and survives significance; the 20-40% band is 9 against 9, z of
exactly 0.00 — and it is the band that was doing all the damage to bluechips.
Measured `vol_ratio`: USDC 0.28, RAY 0.384, CAKE 0.587, WETH 0.380, eth USDT
1.593. USDC and WETH sit squarely in the deleted band, so shipping both tiers
would have cost each 8 section points for a quiet day; the deep tier alone fires
on 0 of 5. Read as one rule the row looked anti-correlated, and the three batches
read 0.80x, 3.00x, 2.00x — the instability was the tier mix, not the market.

So `vol_ratio < 0.20 -> -18` ships as a scored check and the shallow tier is
deleted, with the measurement recorded so it is not re-litigated from one batch.

Consequences, all re-verified live: the price section is 4 checks, so the Step 6
inventory is 23 on sol and 25 on the EVM chains. It closes exactly on all six
bluechips — 21/2/0, 17/6/0, 22/1/0, 14/2/9, 13/3/9, 13/3/9. **All six composites
are still unchanged** (100.0, 93.2, 88.4, 100.0, 97.3, 97.3) and so is every
confidence label; coverage becomes 91.3 / 73.9 / 95.7 / 87.5 / 81.2 / 81.2. WETH
and eth USDT sit one check above the 80% boundary, now stated as such.

Also corrected a claim from the previous commit: `sniper_count` is not "no such
field" — it exists on `market trending` / `market trenches` rows, just not on
`token security`. And the Step 5 guard list now names `vol_ratio`'s own guards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:39:39 +08:00
chenjunfengandClaude Opus 5 9d0c7c2bdc fix(contract-dd): resolve the sub-command contradiction and make coverage auditable
Third review pass, over the sections the first two did not grill: Step 2, Step 8,
Notes, and the two files outside the skill.

**A contradiction introduced by the previous commit.** Step 0 gained a
`portfolio stats` probe to tell a wallet apart from an address GMGN has no record
of, while Sub-commands still read "runs exactly these three ... Nothing else" and
Notes still said "three GET endpoints". The probe is now documented where it
belongs: a conditional fourth call that runs only when `info.symbol` is empty,
feeds no threshold, and sits on the CLI's API-key-only auth path — verified
against `src/client/OpenApiClient.ts`, where `getWalletStats` uses
`authExistRequest`, so the "never needs GMGN_PRIVATE_KEY" prerequisite still
holds. The `--raw` envelope note now covers all four shapes.

**Coverage was stated in a form that cannot be checked.** Step 8 asked for
`executed N / skipped M` against an inventory total, but never for the held-out
count — so `N + M + K = T`, the identity the fixed inventory exists to make
verifiable, was unprintable. The report line is now
`executed N / skipped M / held-out K of T` with all four numbers mandatory.

**The disclosure line named the wrong set.** "chain-analysis metrics (9 checks)"
covered eight holder metrics plus `creator_created_count`, which is a contract
check. It now says so explicitly in both Step 4 and Step 8.

**CLAUDE.md was left self-contradictory by the previous commit**, which routed a
bare wallet address to `gmgn-wallet-analysis` — a skill CLAUDE.md names nowhere
and has no table row for, while a separate row sent "wallet address provided" to
`gmgn-portfolio`. Added the missing row and split that route the same way the
token route was split: dossier verdict to `gmgn-wallet-analysis`, raw holdings
and P&L to `gmgn-portfolio`.

**The DD workflow doc read a struct-default zero as a pass.** Its thresholds
call `top_10_holder_rate < 0.20` safe, but the field arrives as the string `"0"`
on Solana and on some EVM tokens — 0% top-ten concentration does not exist. Noted
the `stat` fallback, and the `"0"` versus `""` distinction for the taxes.

No scoring rule changed in this commit, so no composite or coverage figure moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:26:43 +08:00
chenjunfengandClaude Opus 5 a8e7ad3a7e fix(contract-dd): six more scoring defects found on a second review pass
All six were found by re-reading the scoring tables against live responses
rather than against the prose, and four of them can change a score.

1. The LP row demanded "neither locked nor burned" — a two-part claim — but
   accepted `burn_status: ""` as the burn half. Measured: `burn_status` is `""`
   on every EVM token sampled (10 of 10 bsc, one each arc/stable/robinhood), so
   any EVM token with `is_locked: false` was taking −12 on a burn status GMGN
   never reported. The row now needs both halves present, which makes it
   decidable on Solana and usually unavailable on EVM. RAY is the case that
   shows the row still works: `is_locked: false` with `burn_status: "none"` —
   an explicit negative — and it takes the −12.

2. `buy_tax` / `sell_tax` did not distinguish `"0"` from `""`. On a populated
   block `"0"` is a real 0% tax; on an unpopulated one `""` is exactly what Step
   1A keys on and is unavailable. Coercing `""` to 0.0 scores a passing check on
   a field that was never reported.

3. The liquidity row "0 and `volume_24h` also 0 → −15" was dead text: under the
   worst-row rule a liquidity of 0 already matches `< $10K` at the same −15, so
   it could never change an outcome — while implying a `0` is always a
   measurement. Removed, and the ambiguity it papered over is now resolved
   explicitly: 0 liquidity with 0 volume is a dead pool and scores; 0 liquidity
   with non-zero volume is unavailable, because a pool cannot turn over volume
   it does not have.

4. Step 6's combine order named only Step 2's caps, leaving Step 7's cap of 59
   with no place to be applied — a downgraded honeypot kept its raw composite.
   All caps are now collected together, lowest wins.

5. `info.image_dup_count` had no stated condition ("logo duplicates another
   token"); it is `> 0`.

6. My own claim that arc/stable/robinhood matched bsc "byte for byte" was an
   overstatement — `top_10_holder_rate` differs. Corrected, as was the
   "Three field traps" heading over five bullets.

Re-verified live: **all six bluechip composites are still unchanged** (100.0,
93.2, 88.4, 100.0, 97.3, 97.3) and every confidence label holds. The Step 6
inventory closes exactly on all six — executed + skipped + held-out came to 22
on each sol token and 24 on each EVM token, nothing left over or double-counted.
Coverage corrected where the LP fix moved a check from executed to unavailable:
CAKE 93.3% → 86.7%, WETH and eth USDT → 80.0% each, which is exactly on the
"evidence sufficient" boundary and is now flagged as such.

Also recorded the measured `vol_ratio` values (0.28 / 0.42 / 0.616 / 0.381 /
1.427 on the five scorable bluechips — a deduction on 2 of 5 at the drafted
tiers) and the fact that its lift test against risky tokens was cut short by a
rate-limit ban, so the row stays reported-not-scored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:20:19 +08:00
chenjunfengandClaude Opus 5 849d709579 docs: route the DD verdict to gmgn-contract-dd and fix stale security field shapes
CLAUDE.md's Quick Decision Guide sent "is this token safe" and any bare token
address to `gmgn-token`, which collides head-on with the new skill's trigger set
— and with `gmgn-holder-analysis`, whose description also claims "whether a
token is safe to buy". Split the row by what the user actually wants: one number
goes to `gmgn-contract-dd`, the fields go to `gmgn-token`, and a bare address
gets the verdict first with the fields offered after. Added the missing skill
table row, and corrected the `skills/` count and the marketplace plugin
description, both of which still said 9.

docs/workflow-token-due-diligence.md carried the same field-shape bug PR #211
fixes in the skills: it documented `is_honeypot` as `"yes"` / `"no"` when the
field is `true` / `false` / `null`, and named `open_source`, `owner_renounced`,
`rug_ratio`, `creator_token_status` and `sniper_count` as `token security`
fields. Verified against live responses on 2026-08-28: `owner_renounced` and
`sniper_count` do not exist at all, `rug_ratio` is a `market trenches` row field,
and `creator_token_status` lives at `token info` → `dev`. Also noted that the
integer mirrors (`honeypot`, `open_source`) are not a safe substitute, since
USDC returns `honeypot: 0` with `is_honeypot: null` — `0` conflates "no" with
"unknown".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:07:23 +08:00
chenjunfengandClaude Opus 5 6cff8ba323 fix(contract-dd): close four field-reading holes found in review
Step 1B — the `stat` population test keyed on `stat.holder_count`, which is
populated independently of the chain-analysis fields. Measured: WETH on base
returns `stat.holder_count: 4818570` mirroring `info.holder_count` while all ten
scored `stat.*` fields are zero, so the block was declared populated and nine
checks were scored as nine *passing* checks. Added a second test (all ten zero →
unpopulated). Deliberately all-ten rather than per-field: USDC reads zero on
eight of ten but carries a real `top70_sniper_hold_rate`, and 0% bundlers on
USDC is the truth. `stat.creator_created_count` is now gated with the eight, so
the held-out group is nine, not eight.

Step 7 — the exemption required "no privileged functions", but
`security.privileges` came back `null` on every response measured across all six
chains sampled. Reading `null` as a pass would let silence lift a honeypot
verdict from 0 to 59. The gate now opens only on a present-and-empty value; same
rule for the tax condition.

Step 6 — coverage had no check inventory, so the denominator was whatever each
executor counted and the published percentages were not reproducible. Added the
fixed inventory: 22 checks on sol, 24 on the EVM chains, with the held-out and
not-applicable groups named.

Step 5 — the three kline measurements had no operational definition. Named the
window `W` and its arrays, stated that every kline field is a string and `time`
is milliseconds, and added divide-by-zero guards for `max(H)`, `O[i]` and
`price_24h`. The volume row is defined but **reported, not scored**: defining it
makes it executable for the first time and it immediately costs USDC 8 section
points while its lift against risky tokens is unmeasured — the same shape as the
sell/buy row this skill already removed. Records the test to run before it scores.

Also: Step 0 probes `portfolio stats` before reporting "no record", because a
bare address may be a wallet and the empty `info` block is identical either way;
the untrusted-input clause now says the tell is gmgn-cli's own `[filtered]`
substitution; the ">1 means already a percent" heuristic is replaced with an
anomaly report, since no measured rate ever exceeded 1; description trimmed from
1006 to 968 chars; section order matches the CLAUDE.md rule.

Re-verified by reimplementing the procedure as a script: all six bluechip
composites and all six confidence labels are unchanged. Only coverage moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:06:13 +08:00
chenjunfengandClaude Opus 5 9cb023889b fix(contract-dd): correct Step 5 field paths, drop an anti-correlated scoring row, make the tier rule explicit
Full critical review of this skill turned up five defects. Fixes, in priority
order, with what each was measured against.

Step 5 named `price / price_24h` and `sell_volume_24h / buy_volume_24h` as if
they were top-level `token info` keys. They are not: `info.price` is an object
and all of those names live inside it as strings, so both rows were arithmetic
on the wrong object and could never execute. Same omission in Step 3's "no 24h
volume" clause and Step 7's sells/sell-volume gate; all now carry the
`info.price.` prefix.

Correcting the paths made `sell_volume_24h / buy_volume_24h > 1.3 -> -10`
execute for the first time, and it turned out to be anti-correlated with risk,
so the row is removed rather than fixed. Because net_buy_24h = buy - sell and
volume_24h = buy + sell, the rule is exactly computable from `market trenches`
rows, which also carry GMGN's own rug_ratio label. Over 415 live tokens on sol,
bsc and base above $2K 24h volume it fired on 0.0% of the 114 with rug_ratio > 0
and 10.0% of the 301 with rug_ratio = 0 — lift 0.00x. Reversing the direction
does not rescue it (best variant lift 1.16x) and neither does the sells/buys
count ratio (0.52x); median net_buy/volume is +0.0166 on risky against +0.0196
on clean. Its only measured effect was costing USDC on sol 2.0 composite points
off a stablecoin's redemption flow while penalising 0 of 12 fresh launches. The
price section keeps full separation without it: the three kline measurements
fire on 6 of 6 scorable tokens in a rug-weighted sample and 0 of 6 bluechips.
The ratio survives in Step 7 as a two-sidedness band, which is a different
question and is now labelled as one.

Tier exclusivity was never stated, leaving six multi-row groups ambiguous — a
creator with 1971 launches matches all five creator_created_count rows, worst-row
-18 against summed -51. Scoring 16 live tokens both ways puts the difference at a
median of 22.2 composite points and up to 33.3, more than the 20-point grade
band: one token reads 71.3 "mixed" or 38.0 "very high risk" depending only on the
reading. Worst-row is now stated in Step 3 and referenced from Steps 4 and 5.

Step 2's honeypot hard stop said "stop scoring" and Step 6 said "0 regardless of
everything else", so Step 7's tokenized-equity exemption was unreachable — which
matters because robinhood, arc and stable are three of the seven supported chains.
Both now point at Step 7 first. Checked against three live is_honeypot === true
tokens on base: none comes near Step 7's gate (sells_24h 243/0/0 against 500,
sell_volume_24h $63.6K/$0/$0 against $100K), so all three still resolve to 0 and
the reordering opens no hole.

Section scores had no lower bound while the eight stat checks alone total -117,
so both section and composite are now clamped to 0-100. Measured on the 14 worst
rug_ratio tokens, no section goes negative under the worst-row rule, so this
changes no published number.

Finally, three places claimed missing data never moves the score, which the
len(kline.list) deduction has always contradicted. Step 6 now names all three
exceptions explicitly and the frontmatter matches.

No published score changes: all six bluechips come back identical after the
path correction and the row removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:34:59 +08:00
chenjunfengandClaude Opus 5 414ed35548 fix(contract-dd): pin the scoring window, drop the nonstandard frontmatter key, add the hand-off table
Three review items, none of which change a single deduction or weight:

- Step 5 said "score the last 96 15m candles, roughly 24h" while the skill passes no
  --from/--to. Measured: the default window is exactly 100 candles on sol, bsc and eth,
  so 96 is satisfiable today with only four candles of headroom. Read len(kline.list)
  and score min(96, len) instead of assuming, and state the real window when it is
  shorter — the measurements are ratios and survive a short window, the "24h" label
  does not.
- metadata.catalogSlug was the only occurrence of that key in the repo: nothing reads
  it and none of the ten existing skills carry one. Removed for consistency; a one-line
  revert if the skill catalog consumes it downstream.
- The holder (0.35) and price (0.20) sections are coarse by design, which makes them
  easy to mistake for gmgn-holder-analysis and gmgn-kline-pattern. Added a hand-off
  table and said outright that those skills use different thresholds and weights, so
  their numbers will not match this composite and neither corrects the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:47:45 +08:00
chenjunfengandClaude Opus 5 4d43962289 feat(contract-dd): add contract due-diligence scoring skill
Turns three read-only GMGN calls (token info, token security, market
kline) into one auditable 0-100 composite: contract safety 0.45, holder
structure 0.35, price action 0.20, renormalised over only the sections
that returned data. Every deduction names the field it came from.

The skill exists because scoring GMGN's security fields naively
condemns clean tokens. An unpopulated response object still carries
false on its booleans, "" on its strings and 0 on its numbers, and
reading those as measurements is the failure mode the whole design is
built around. Missing data is therefore neither a deduction nor a
bonus: it lowers reported coverage, and coverage bounds how strong a
claim the score may make rather than moving the score itself.

Validated by running the procedure end to end over 14 addresses on
2026-08-27 — five bluechips, four fresh launches, three well-formed
addresses with no GMGN record, one malformed address. What that
measured, and what the rules had to say as a result:

- token existence is decided by info.symbol, not by security.address.
  GMGN echoes the requested address back into security.address for
  addresses it holds no record for; two fabricated addresses came back
  echoed with renounced_mint=false, and one also returned a full
  100-candle kline series, while their info blocks were empty. Step 0
  refuses to score those instead of reporting a risk grade for a token
  that is not there.
- every rate and tax field is a decimal fraction, including the four
  named *_percentage. top_10_holder_rate "0.1783" is 17.83% and
  buy_tax "0.01" is a 1% tax, so a threshold written in percent needs
  a x100 before the comparison or the deduction silently never fires.
- on sol the four is_* fields are null by design and the numeric ones
  read "0", so an emptiness test declares every Solana block empty.
  Step 1A is scoped to the EVM fields; renounced_mint and
  renounced_freeze_account are read unconditionally once the token is
  known to exist, or Solana loses its only two contract signals.
- info.stat is populated per token, not per chain: full block on a
  four-hour-old bsc meme, zeros on CAKE.
- the two dev X fields are aggregates over the linked X account across
  every token it has been attached to. USDC's rename history reads
  circlepay -> circle -> arc and CAKE reports 44 deleted launch posts,
  so deducting on them penalises projects for having a past. Both are
  reported and neither scores.
- under --raw these three commands print the unwrapped payload, so
  there is no code field to read.

Read-only by construction: three GET endpoints, no signing, no private
key, and the skill explicitly forbids calling swap, order or cooking
commands.

One new file, nothing else touched. CLAUDE.md and marketplace.json
bookkeeping is left to maintainers; see the PR body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:47:45 +08:00