src/config.ts loaded ~/.config/gmgn/.env with override:true and then the
cwd .env without override, so the global file always won — the opposite
of what Readme, Readme.zh, CLAUDE.md and the gmgn-cooking skill promise.
A user who updated GMGN_PRIVATE_KEY in a project .env kept signing with
the stale global key and got AUTH_SIGNATURE_INVALID.
Load the cwd .env first and the global file second, both without
override, so precedence is shell env > project .env > global .env.
`config --check` reuses the same loader instead of its own copy.
Co-Authored-By: Claude <noreply@anthropic.com>
gmgn-openapi-service no longer clamps priority_fee / tip_fee / gas_price /
max_fee_per_gas / max_priority_fee_per_gas to per-chain min/max (openapi MR
!114, merged 2026-10-19). Fees are forwarded as-is and the trade backend
enforces its global ceilings; the server only rejects non-numeric or
negative values, plus BSC "0" for the EIP-1559 fields.
Remove the stale "≥ X" / "Clamped per chain minimums" wording from swap.ts
option help, gmgn-swap and gmgn-cooking SKILL.md, and docs/cli-usage.md, and
replace the gmgn-swap "EIP-1559 minimum values" note with the current rules.
Co-Authored-By: Claude <noreply@anthropic.com>
Two further self-review passes: one for internal consistency, one simulating
execution against boundary inputs.
The consequential one is the "at least half" test, which is what separates
回避 from 不可跟 and was left to the reader with a sample size of three. Now
stated as arithmetic: counting x 2 >= sampled, so 2 of 3, 2 of 4, 3 of 5, and
one of three does not fire it.
Five edges that had no defined answer:
- Two handle candidates with equal total_calls: break on the more recent newest
call, then on richer wallet_tags, then lexicographically, and say which.
- Two chains with equal call counts: score the one whose newest call is more
recent, name the other with its count.
- next_page_token expiring part-way through paging: stop and score what was
fetched, never restart or retry, and report the coverage as a floor.
- created-tokens truncation: the tokens array caps at 100 while
inner_count + open_count is the real total, so an empty intersection is only
evidence at full coverage. A caller measured at 1,551 launches with 101 rows
(6.5%) had an empty intersection that proves nothing — report the launch
record and say the self-launch check could not be completed. Same when
created_token_count > 0 but tokens comes back empty.
- Fields read now states its scope: derived quantities are not API fields,
fields named only as empty or excluded are not read, and error-response keys
are read only on 429/403. The common name fields the cleaning rule references
(name / nick_name / twitter_name / twitter_username) are now listed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four findings from a final self-review of the whole file.
Time units, the consequential one: callout record's create_time is Unix
milliseconds while portfolio activity's timestamp is Unix seconds, and every
conduct quantity — lead_seconds, minutes_to_first_sell, the window ages — is a
difference across that boundary. The file never said so. A missed factor of
1000 does not throw; it silently yields either an absurd number or a plausible
wrong one. Also warns against taking .timestamp() of a naive UTC datetime,
which is read as local time and shifts every age by the machine's offset.
Step 5 had no row for "selection axis not issued", which is the common case
rather than the exception — only two of the tested callers could issue a
comparable hit rate. Added, with the asymmetry stated: conduct alone can
condemn, because a measured sell into one's own call needs no comparison, but
it can never endorse, because "did not dump on the three sampled coins" is not
a reason to follow someone whose record cannot be compared.
post_call_net was labelled "the primary quantity" while clauses (a) and (b) do
not use it — the same kind of wording contradiction already caught once in
review. Now stated as what the report reads from and what Credit is decided on,
with the reason it stays out of (a) and (b).
Name-field cleaning now covers twitter_username / twitter_name / common.name /
nick_name explicitly, and the title line is pinned to the handle as the user
typed it or the truncated address, never a name read back from market search —
closing the last position where a forged heading could land.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two review findings.
Injection: the skill told the reader to print `token_symbol` verbatim inside
「」, which is not escaping — the field can carry the closing bracket itself, a
line break that starts a fake 跟单建议 heading, a markdown link, or a `|` that
tears open a table row. That is the hole gmgn-wallet-analysis already closes in
clean_text(), reopened here. The rule now spells out the same cleaning in five
ordered steps: truncate to 8x the cap, map Cc/Cs/Co/Zl/Zp and Cf except U+200D
to spaces, delete [ ] < > * ` plus `|` and the 「」 wrapper characters, break
`://` / `www.` / email `@`, cap at 32 characters. If cleaning leaves only
spaces, no symbol is printed and the contract address stands alone. Also states
explicitly that call_thesis never reaches the reader in any section.
Logic: the Systematic tier defined two clauses, but a trailing sentence listed
`exit_vs_call_price > 1` as a third qualifying condition for the same
"at least half" test, while the Frequent tier used it as a severity marker —
the two readings disagreed on a contract with a non-applicable ratio and
exit above the call price. The tier now enumerates (a) and (b) as its only
clauses, states that a non-applicable ratio can satisfy (b) but never (a) and
stays in the denominator either way, and keeps `exit_vs_call_price > 1` as a
severity marker inside Frequent and never a Systematic trigger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scores one call-out wallet on two axes — selection (do the coins he calls
actually run) and post-call conduct (does he sell into the attention his own
call created) — and returns a follow / don't-follow verdict.
The conduct axis is proved from the caller's own per-trade ledger rather than
from any self-reported field: for each sampled contract it compares the first
buy, the call timestamp and every sell after it, so "he sold into his own call"
is a measured sequence rather than an inference.
gmgn-callout keeps every raw read of the callout endpoints; this skill calls
the same commands and adds a verdict on top, the same split as gmgn-dev-score
has with gmgn-portfolio.
Every threshold was calibrated against eleven call-out wallets measured live on
SOL and BSC on 2026-10-15 and 2026-10-16.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Align with the server-side default (gmgn-openapi-service trenchesDefaults["robinhood"].QuoteAddressType)
that went live in July. Without it, default trenches queries on robinhood miss tokens paired
with quote type 26 (RWA pools).
Co-Authored-By: Claude <noreply@anthropic.com>
gmgn-openapi-service already exposes two read-only call-out routes on
openapi.gmgn.ai behind the normal API-key auth, so the CLI can reach them
without the separate papi AK/SK channel:
- gmgn-cli callout record -> POST /v1/callout/get_record: one wallet's
call-outs over the last 30 days plus the 7d/30d summary card
- gmgn-cli callout global -> POST /v1/callout/global: multi-chain feed with
only-kol / only-declaration / only-safe / holding-range filters
Both are weight 3. Chains are validated against the service's 13-chain
callout list, kept separate from the tradable VALID_CHAINS. Limit is checked
against the service's 0-50 bound; holding bounds are sent as decimal
strings. /v1/callout/token is deliberately left out: the service removed it
(prod returns 404) and publishing call-outs is not part of OpenAPI.
Docs: new skills/gmgn-callout/SKILL.md, CLI reference in docs/cli-usage.md
and both Readmes, routing rows in CLAUDE.md.
Co-Authored-By: Claude <noreply@anthropic.com>
- A wash check blocked by an unreadable launch list is now an open
question ranked below every measured G1 failure (thin sample, one
coin, concentration, count veto), so it can only stop a pass.
- conviction_share uses main's exact arithmetic, non-finite rows
included, so a never-launched wallet reads exactly as on main.
- Stale comments from the removed logic fixed; caps raised so they never
shorten ordinary tags or funding labels; doc wording clarified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narrow the rule after repeated review findings: proving that a
launcher's profit is NOT from its own launches needs the whole book, a
complete launch list and clean timing, and every inconsistency there
became a false pass. The attribution now only adds a veto.
- Remove every path that lifted the launch-count veto (proven / partial
clear states, the "every listed launch seen" proof, verdict row 10c,
the related archetype tags). A launcher by count is vetoed exactly as
on main; a clear reading is printed for reference only.
- conviction_share is computed on the open book exactly as on main. The
wash test uses the lower of that share and the share without own and
suspected launches, so it can never refute a tag main would not.
- Malformed row addresses are left out instead of counted external.
- "Not measured" in G1 names an unreadable launch list when that is the
cause; ZH gap wording fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A page longer than the row cap is a cut-off, never a complete book.
- A token appearing more than once counts once, at its largest gain.
- Launch-list and creator addresses are stripped and must be
well-formed; a junk creator leaves a token a suspect.
- With an incomplete list, unknown buy timing or a buy within 5 s
either side of creation is a suspect.
- A clear reading is proven only when the whole book was read or every
listed launch already appeared; until then paging continues, for any
launcher. Unproven: launcher by count gets G1 ⚪, others pass with a
"not verified" line.
- A blocked wash check never softens a launcher's count veto; an
unproven clear reading never prints the launcher headline over
another G1 failure.
- No paging without an own-token set, no suspect lookup on an
unmeasured sample, a falsy cursor ends paging.
- Malformed or deeply nested JSON on the new routes becomes a gap.
- Gap lines keep "[gmgn-cli]" but never form a link; e-mail addresses
in third-party text are broken.
- ZH wording aligned with the existing 先别动 / 发 terms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A clear reading on a truncated book no longer lifts a launcher's
count veto: unread older positions can hold any amount of own-token
gains offset by losses the all-time net cannot reveal, so no sample
ratio can bound them. Such a launcher now gets G1 ⚪ and
"HOLD OFF · its launches look minor, but its older record was not
read"; a clear reading keeps paging toward the book's natural end.
- Tag keys are stripped again, so " wash_trader " still matches.
- A wash check blocked by an unreadable launch list advises re-running,
not configuring a key.
- An empty own-token set while the wallet launched is "not measured".
- stats_30d catches a CLI timeout as a gap.
- Docs: verdict row 10c, sixteen outcomes, paging and timeout wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Sample floor: the share needs at least 3 winning positions.
- An unconfirmed range never softens a measured G1 failure; a launcher
by count keeps its count veto unless attribution clears it.
- The launch list counts as complete only when it names
max(inner+open, created_token_count) tokens; a missing total is never
complete. A clear reading on a truncated sample only lifts the count
veto once it covers half the all-time realized profit.
- Paging cut-offs (failed call, timeout, malformed or empty-but-continuing
page, repeated cursor) are never read as the end of the book.
- Own and suspected launch tokens are left out of conviction_share, and
an unreadable launch list can no longer let it refute a wash tag.
- EVM addresses compared case-insensitively, base58 exactly.
- stats_30d moves ahead of the self-launch tier (G2 reads it).
- Third-party text (symbols, X name/handle, funding label, tags, gap
lines) is cleaned of line breaks, control/format characters and
markup before printing; ordinary text prints unchanged.
- Malformed API shapes and non-finite numbers no longer crash the report.
- Docs: holdings weight 2, verdict table order and rows, call order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAPI service now returns two new rate-limit codes:
- RATE_LIMIT_BANNED_MAX (429): the ban has reached its 5-minute ceiling
because requests kept arriving during the cooldown. The CLI now says to
stop all requests until reset_at instead of the generic retry hint.
- PLAN_UPGRADE_REQUIRED (403): the route's weight exceeds the plan's bucket
capacity (e.g. Free on swap / quote / follow-wallet), so it can never
succeed. The CLI surfaces the upgrade message; previously 403s were
printed without it.
Skills and docs/cli-usage.md tell agents not to retry either code, and the
stale rate=10/capacity=10 note is replaced by the plan tiers.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
- Escape every non-visible character in the confirmation block by Unicode
property (\p{C}, \p{Z}, \p{M}, Default_Ignorable_Code_Point, U+2800) instead
of a hand-kept list; soft hyphens, Hangul fillers, tag characters etc. can no
longer make a fee-share username look like another.
- Fold only fields a command declares as blobs (cooking --image); key fields
are always shown in full. multi-swap and strategy create/cancel now validate
their addresses like swap does.
- Judge protections on the values being sent (min_output 0, anti_mev_mode off,
expire_in 0 now warn), and reject NaN / Infinity / negative --slippage,
--percent, --expire-in, --dev-wallet-bps before they reach a signed body.
--gas-price is converted gwei→wei exactly (was float-rounded).
- Total tip / priority fees per container, including sell_param / buy_param and
buy_trade_config / sell_trade_config, so section ① no longer understates.
- Anti-MEV warning only where accurate: none for swap (documented default on),
"server default applies" for multi-swap / strategy on sol / bsc / eth, and
an explicit "off" warning for cooking create on sol.
- Bind confirmations to method + route + query + body, and require one for
every signed non-GET request (deny by default) instead of a path allowlist.
- In interactive mode write the block to /dev/tty, so the process launcher
cannot filter what the human reads before typing yes.
- Display: swap --percent shows the bps line first and marks input_amount as
ignored; strategy amount_in_percent and multi-swap per-wallet bps show "% of
balance"; multi-swap shows the wallet count.
- Group risky leaves in linear time; load credentials before confirmation so a
missing key is reported before the user types yes.
- Sync skills/gmgn-swap, skills/gmgn-cooking and docs/cli-usage.md with the
gated order strategy cancel and its --yes flag.
Co-Authored-By: Claude <noreply@anthropic.com>
The confirmation prompt for swap / multi-swap / order strategy create /
cooking create printed a hand-written summary that omitted fields still sent
and signed: tip / priority fees, gas, min_output_amount, condition_orders
(auto TP/SL), check_price, sell_param / buy_param, buy_wallets,
snip_buy_wallets, dev_wallet_bps and the creator fee-share lists. A
prompt-injected agent could hide any of them behind a normal-looking summary.
Layer 1 - confirmTrade now takes the request object itself, flattens it to
every leaf field and prints four sections: key fields; risky fields, each
with a plain-language consequence and never folded; protections that are NOT
set (no min output, no slippage limit, anti-MEV not requested, no expiry);
and every remaining field. Section 4 is "whatever 1-3 did not print", so new
parameters appear automatically. Long strings (e.g. --image base64) fold to
<string, size, sha256>, gas_price is echoed in gwei, and command-side totals
are added (extra fees, total buy spend across dev / bundle / snipe wallets,
creator-fee recipients). Control, C1, bidi and zero-width characters are
escaped so a value cannot rewrite or hide lines.
Layer 2 - confirmTrade records the canonical-JSON sha256 of the confirmed
body; authSignedRequest refuses to sign swap, multi_swap, strategy/create,
strategy/cancel and create_token unless the exact body being signed was
confirmed, consuming the confirmation so it authorizes one signature. This
blocks a new command that forgets confirmTrade and a body mutated after
confirmation.
Layer 3 - order strategy cancel gets the same gate and --yes flag; cooking
create JSON options fail with a clear error instead of a raw SyntaxError;
Readme / Readme.zh now state the confirmation block is the complete request
body about to be signed.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
`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>
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>
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>
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>
`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>
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>
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>