token-movers
SkillCommerce & financeLets your agent scan crypto market movers and produce deep reports on individual tokens with pump-risk warnings.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the token-movers skill
About this capability
Crypto market scanner and single-token analyst - movers scans top winners/losers/trending or on-chain runners with pump-risk flags; single-token produces a verdict-first deep report for one token.
What this skill tells your AI
The instructions your AI receives, as published by aeonfun/aeon in skills/token-movers/SKILL.md and read by ahel’s review.
${var} — selects the scan. Two behavioral modes (
movers,single-token) over two sources (coingecko,geckoterminal):
- empty → movers scan on the default source (CoinGecko): top winners, losers, trending.
coingecko→ same movers scan, CoinGecko source (explicit).geckoterminal→ movers scan on GeckoTerminal: on-chain "runners" across major chains.geckoterminal:<chain>or a bare chain slug (solana,eth,ethereum,base,bsc,arbitrum,polygon,optimism,avalanche,avax) → GeckoTerminal runners scoped to that one chain.category:<name>(e.g.category:layer-2,category:meme) — or a bare hyphen/space value likelayer-2/meme coins→ CoinGecko movers scoped to that category.<contract>or<contract>:<chain>(e.g.0xabc…,0xabc…:base) → single-token deep report on that contract.<SYMBOL>(e.g.SOL,WIF) → single-token deep report; resolve the symbol to its top contract first.token/single-token→ single-token deep report on the token configured inmemory/token-report.md.deep-dive:<symbol|contract>(e.g.deep-dive:WIF,deep-dive:0xabc…:base) → single-token deep report — the shape the Telegram force-reply sends. Strips thedeep-dive:prefix and resolves the remainder exactly like a bare symbol/contract.Examples:
""(global movers),geckoterminal:base(Base runners),category:layer-2(L2 movers),0x4ed…:baseorWIF(single-token report).
Preamble (every run)
-
Read
memory/MEMORY.mdfor context. -
Read the last 2 days of
memory/logs/to avoid repeating the same movers/trending/runner names unless the move is materially different — repeat runners across days are the real signal. (The single-token branch reads the last 30 days for itsTOKEN_REPORT_STATE:delta lines — see that branch.) -
Parse
${var}→source+mode(+ optionaltoken/chain/category). Trim whitespace; evaluate the rules top-to-bottom, first match wins (fully deterministic):- Force-reply intercept (Telegram deep-dive). starts with
deep-dive:→ strip the prefix (${var#deep-dive:}) and treat the remainder EXACTLY as a single-token target, resolving it contract-or-symbol just like rule 8 (token:) does → single-token. Single-token branch. This is the shape the Telegram force-reply sends; it reuses all existing single-token logic (no separate handler, no confirmation — the single-token report IS the response). - empty → mode=movers, source=coingecko (global). Go to Movers branch.
coingecko(case-insensitive) → movers / coingecko (global). Movers branch.geckoterminal→ movers / geckoterminal (all major networks). Movers branch.- starts with
geckoterminal:orchain:→ movers / geckoterminal,chain= remainder. Movers branch. - a known chain slug (
solana|eth|ethereum|base|bsc|arbitrum|polygon|optimism|avalanche|avax) → movers / geckoterminal,chain= value. Movers branch. (Preserves monitor-runners'var=chain behaviour. To report on a token that shares a chain name, e.g. the ETH token, usetoken:ethor its contract.) - starts with
category:→ movers / coingecko,category= remainder. Movers branch. - equals
tokenorsingle-token→ single-token, token frommemory/token-report.mdconfig. Single-token branch. - starts with
token:→ single-token, resolve remainder as contract-or-symbol. Single-token branch. - matches a contract address — EVM
0x[0-9a-fA-F]{40}or a Solana base58 address — optionally:chain→ single-token, that contract. Single-token branch. - contains a space or a hyphen and is not a contract (e.g.
layer-2,meme coins) → movers / coingecko,category= value. Movers branch. - otherwise (a bare word, a plausible ticker) → single-token / geckoterminal, resolve
symbol= value to its top contract. Single-token branch.
- Force-reply intercept (Telegram deep-dive). starts with
Mode: movers (default)
Produce an actionable movers report. Plain % change lists are noise — the value is in distinguishing real signal (on volume, from a credible cap tier / deep liquidity) from pump-and-dump noise. Run exactly one source path below, chosen in the preamble.
Source: coingecko — market movers (winners / losers / trending)
1. Fetch data
Fetch market data and trending coins in parallel. Request multi-timeframe changes for context:
# CoinGecko auth: a free/Demo key (the common case) authenticates on
# api.coingecko.com with the `x-cg-demo-api-key` header — send it only when a key
# is set; without one the same public endpoint still works at a lower rate limit.
# (A paid Pro key instead uses pro-api.coingecko.com with `x-cg-pro-api-key`.)
# Pass the key through ./secretcurl's {ENV_NAME} placeholder so no `$SECRET` ever
# lands on the command line (a bare $COINGECKO_API_KEY is refused by the Bash
# permission analyzer). Build the header array only when the key is set, so the
# call stays keyless-public when it isn't.
CG_HDR=(); [ -n "${COINGECKO_API_KEY:+x}" ] && CG_HDR=(-H "x-cg-demo-api-key: {COINGECKO_API_KEY}")
# Top 250 coins by market cap with 1h, 24h, and 7d % change
./secretcurl -s "${CG_HDR[@]}" "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250&page=1&sparkline=false&price_change_percentage=1h,24h,7d"
# Trending searches (top coins people are searching for)
./secretcurl -s "${CG_HDR[@]}" "https://api.coingecko.com/api/v3/search/trending"
If curl fails or returns empty JSON, retry once with WebFetch against the same URL.
2. Filter before ranking
Before picking winners/losers, drop noise. All numeric thresholds below are starting points — tune as needed if the output consistently feels too loose or too strict:
- Stablecoins: exclude symbols/ids that peg to fiat —
tether,usd-coin,dai,first-digital-usd,usde,tusd,usdd,pyusd,fdusd,paxg(gold-pegged), and anything whose symbol starts withUSD/EUR/GBPor name contains "stablecoin". - Illiquid tokens: drop coins with 24h
total_volume< $1,000,000 (tune as needed). Sub-$1M volume on a top-250 coin is a pump/wash-trading target and generates misleading % moves. - Wrapped dupes (optional): if a wrapped version (e.g.
wbtc,weth,steth) would otherwise dominate a list, keep only one representative.
3. Pick the lists
From the filtered market data, sort by price_change_percentage_24h:
- Top 10 winners (highest 24h %)
- Top 10 losers (lowest 24h %)
For each item, capture: name, symbol, market cap rank, current price (USD), 24h %, 7d %, 1h %, 24h volume (USD), market cap (USD).
From the trending endpoint, take the top 7 trending coins with: name, symbol, rank, price, 24h %.
4. Enrich with signal + risk tags
For every entry in the three lists, compute tags. Attach at most 2 tags per coin to keep the output clean. All numeric thresholds below are starting heuristics — tune as needed:
- [TRENDING+UP] — appears in trending AND is a top winner. Strong positive signal.
- [TRENDING+DOWN] — appears in trending AND is a top loser. Capitulation / bad-news signal.
- [BREAKOUT] — 24h change > +15% AND 7d change > +25%. Sustained move, not a flash pump.
- [FADE] — 24h change > +20% BUT 7d change is negative. Likely relief bounce in a downtrend.
- [CAPITULATION] — 24h change < −10% AND 24h volume > 3× the coin's typical daily volume (approximate: use
total_volumevsmarket_capratio > 0.25 as a rough proxy if no historical data). - [PUMP-RISK] — market cap rank > 150 AND 24h change > +30%. Low-cap, big spike — high manipulation probability. Warn the reader.
- [MICROCAP] — market cap < $50M. Disclose; these moves rarely predict direction.
- [MAJOR] — market cap rank ≤ 20. Large-cap moves are more informative per unit % change.
5. Market commentary (one sentence, calibrated)
Compute a quick market pulse: among the top 100 by mcap (after filters), what fraction had positive 24h change? What was the median 24h change of the top 50?
Write one sentence characterizing the tape. Examples:
- "Broad risk-off — 78/100 top coins are red, median −3.2%; losers dominate across L1s and DeFi."
- "Mixed tape with alt rotation — BTC flat but 62% of top-100 alts green, meme and AI-coin names leading."
- "Quiet — median move under 1% either way; trending is dominated by new listings rather than price action."
Don't editorialize beyond what the numbers show. No predictions.
6. Category scoping (when category is set)
When the preamble resolved a category (rules 6 / 10), scope the whole pipeline to it: use /coins/categories/list to resolve the category id, then /coins/markets?vs_currency=usd&category=X&order=market_cap_desc&per_page=250&price_change_percentage=1h,24h,7d in place of the plain markets call, and run steps 2–5 over that subset. Keep the trending call as-is (trending is global) and tag any trending coin that also falls in the category.
7. Send notification
Via ./notify, under 4000 chars:
*Token Movers — ${today}*
_[one-sentence market pulse from step 5]_
*Top Winners (24h)*
1. SYMBOL (Name) — $price +24.1% / 7d +18% / 1h +2.3% • $vol / #rank [TAGS]
2. ...
*Top Losers (24h)*
1. SYMBOL (Name) — $price −18.4% / 7d −22% / 1h −3.1% • $vol / #rank [TAGS]
2. ...
*Trending*
1. NAME (SYMBOL) — #rank, $price, 24h ±X.X% [TAGS]
2. ...
*Notable*
• SYMBOL: trending + up 42% on 6× volume — strong signal
• SYMBOL: #212 rank up 85% — PUMP-RISK, low liquidity
• [1–4 bullets, skip section if none worth calling out]
Formatting rules:
- Round prices sensibly (4 sig figs, or 6 decimals for sub-$0.01 tokens).
- Round % to one decimal. Volume and mcap abbreviated (e.g.
$4.2B,$380M). - Only include the
Notablesection if at least one signal earned[TRENDING+UP],[BREAKOUT],[CAPITULATION], or[PUMP-RISK]. - If a coin appeared in the last 2 days of logs with the same direction and similar magnitude, skip it unless it now has a new tag (e.g. yesterday's winner is now [CAPITULATION]).
- If
categoryis set, title the message*Token Movers — <category> — ${today}*.
8. Log (coingecko movers)
Append to memory/logs/${today}.md:
### token-movers
- Mode: movers | Source: coingecko | Scope: ${category:-global}
- Var: ${var:-<none>}
- Pulse: [one-sentence market pulse]
- Winners: SYM (+X%), SYM (+X%), …
- Losers: SYM (−X%), SYM (−X%), …
- Trending: SYM, SYM, …
- Notable: [any PUMP-RISK / BREAKOUT / CAPITULATION signals]
Then go to Send the digest and stop (do not run the GeckoTerminal path).
Source: geckoterminal — on-chain runners
A flat "top 5 by 24h %" list is dominated by micro-cap meme coins with <$50k liquidity. That output trains the operator to ignore it. The lever is ranking by a composite Runner Score and tagging each pick with an actionable category — so the operator can tell at a glance which picks are serious (deep-liq, sustained momentum) vs speculative (micro-cap, brand-new pool).
Data source: GeckoTerminal API (free, no API key). Docs: https://apiguide.geckoterminal.com
Endpoints used:
GET /networks/trending_pools?page=1— trending pools across all networks (the % movers)GET /networks/{network}/trending_pools?page=1— per-network trendingGET /networks/{network}/pools?page=1&sort=h24_volume_usd_desc— volume leaders (catches runners that aren't on the trending list yet)GET /networks/new_pools?page=1— newly created pools (brand-new breakouts)
Each pool object includes:
attributes.name— pool name (e.g. "TOKEN / SOL")attributes.price_change_percentage.{m5,m15,m30,h1,h6,h24}— price changesattributes.volume_usd.{m5,m15,m30,h1,h6,h24}— volumeattributes.market_cap_usd/attributes.fdv_usd— market capattributes.transactions.h24.{buys,sells,buyers,sellers}— activityattributes.pool_created_at— pool creation timestampattributes.reserve_in_usd— liquidityrelationships.network.data.id— chain namerelationships.base_token.data.id— base token address (for dedup)
1. Fetch data (sequential, rate-limit aware)
TMPDIR=$(mktemp -d)
TODAY=$(date -u +%Y-%m-%d)
# Networks to scan. If ${chain} was resolved in the preamble, restrict to that one.
if [ -n "${chain}" ]; then
NETWORKS="${chain}"
else
NETWORKS="solana eth base bsc arbitrum"
fi
fetch_with_backoff() {
local url="$1" out="$2"
for delay in 0 2 4; do
[ $delay -gt 0 ] && sleep $delay
curl -s --max-time 15 "$url" > "$out"
if ! grep -q '"status":"429"' "$out" 2>/dev/null && [ -s "$out" ]; then
return 0
fi
done
return 1
}
# Global trending
fetch_with_backoff "https://api.geckoterminal.com/api/v2/networks/trending_pools?page=1" "$TMPDIR/global.json" \
&& GLOBAL_OK=1 || GLOBAL_OK=0
sleep 1
# Per-network trending + volume leaders
for N in $NETWORKS; do
fetch_with_backoff "https://api.geckoterminal.com/api/v2/networks/${N}/trending_pools?page=1" "$TMPDIR/${N}-trend.json" \
&& eval "${N}_TREND_OK=1" || eval "${N}_TREND_OK=0"
sleep 1
fetch_with_backoff "https://api.geckoterminal.com/api/v2/networks/${N}/pools?page=1&sort=h24_volume_usd_desc" "$TMPDIR/${N}-vol.json" \
&& eval "${N}_VOL_OK=1" || eval "${N}_VOL_OK=0"
sleep 1
done
# New pools (for BREAKOUT tagging)
fetch_with_backoff "https://api.geckoterminal.com/api/v2/networks/new_pools?page=1" "$TMPDIR/new.json" \
&& NEW_OK=1 || NEW_OK=0
Fetch fallback: if curl fails for any URL (file is empty or has "status":"429" after retries), retry that URL with WebFetch using the same URL. Parse the JSON response body.
2. Merge, dedupe, gate
From every fetched file, extract all pool objects. Then:
- Dedupe by
relationships.base_token.data.id— keep the highest-volume pool per token (same token may have multiple pools across DEXes). - Gate on quality — drop a pool if ANY of:
volume_usd.h24 < 50000(too thin to be a real runner)price_change_percentage.h24 <= 0(we want runners, not dumps)reserve_in_usd < 10000(liquidity floor)transactions.h24.sells / transactions.h24.buys > 10(dumping pattern)transactions.h24.buys / transactions.h24.sells > 50(honeypot pattern — nobody can sell)- pool_created < 1h ago AND
volume_usd.h24 < 100000(too new to judge) price_change_percentage.h24 > 10000(>100x — almost certainly a rug-in-progress)
Record the count of pre-gate and post-gate pools for the log.
3. Score each surviving pool
Compute a Runner Score (0-100) per pool. Use simple normalized components so the math is transparent:
pct_pts = clamp(price_change_percentage.h24 / 500, 0, 1) # 500% maps to full
vol_pts = clamp(log10(volume_usd.h24 + 1) / 7, 0, 1) # $10m vol = full
liq_pts = clamp(log10(reserve_in_usd + 1) / 6, 0, 1) # $1m liq = full
mom_pts = clamp((price_change_percentage.h1 + 50) / 100, 0, 1) # +50% h1 = full, -50% = 0
skew_pts = clamp(buys / (buys + sells), 0, 1) # 0.5 = neutral
runner_score = 40*pct_pts + 25*vol_pts + 15*liq_pts + 10*mom_pts + 10*skew_pts
This weights absolute move (40%) + liquidity-adjusted volume (25%) + liquidity depth (15%) + live momentum (10%) + buy pressure (10%). Pct_pts is clamped to avoid meme-coin moonshots flooding the ranking.
4. Tag each pool (exactly one tag)
Apply tags in priority order — first match wins:
| Tag | Condition |
|---|---|
| DEEP-LIQ | reserve_in_usd >= 1_000_000 AND volume_usd.h24 >= 1_000_000 |
| BREAKOUT | pool_created_at within last 48h AND volume_usd.h24 >= 250_000 |
| CONTINUATION | price_change_percentage.h1 > 2 AND price_change_percentage.h24 > 50 |
| REVERSAL | price_change_percentage.h1 < -5 AND price_change_percentage.h24 > 0 (fading) |
| MICRO-SPEC | everything else (default — small-cap speculation) |
5. Select the top 5 + session verdict
Rank by Runner Score descending, take top 5.
Compute a session verdict from the tag distribution among the top 5:
- STRONG — ≥2 DEEP-LIQ picks (real money moving)
- MIXED — 1 DEEP-LIQ OR ≥2 CONTINUATION (signal but speculative)
- SPECULATIVE — majority MICRO-SPEC/BREAKOUT (retail casino)
- SLEEPY — fewer than 5 pools survived the quality gate
6. Cross-reference prior days
From the last 2 days of memory/logs/, extract any runner token names previously flagged in a ### token-movers runner entry (i.e. under the Top 5 runner list). For each of today's top 5, mark ★ repeat if the token name appears in either prior day's log. Sustained runners across multiple days deserve extra attention.
7. Notify (runners)
Send via ./notify. Format:
*runners — ${TODAY}* — verdict: STRONG
1. [TAG] TOKEN (chain) +X% 24h ★ repeat
vol $X.Xm | liq $X.Xm | fdv $Xm | h1 +X% | buys:sells X:Y
— [one-line actionable take: e.g. "sustained multi-day momentum with deep liquidity — watch for continuation"]
2. [TAG] TOKEN (chain) +X% 24h
vol $Xm | liq $Xk | fdv $Xm | h1 -X% | buys:sells X:Y
— [one-line take]
3. ...
4. ...
5. ...
sources: gt-global=ok gt-{networks}=ok/fail
vibe: [one-line read on overall tape mood]
Formatting rules:
- Format dollar values human-readable:
$2.3m,$450k,$75k. Never show raw dollar amounts with comma separators. - Format percentages:
+347%(no decimals unless <10%, then+4.2%). - If
market_cap_usdis null, showfdv $Xm (no mcap). - Include the ★ repeat marker only for tokens appearing in prior days' logs.
- The one-line take MUST say something the operator can act on — not a restatement of the numbers. Good: "clean breakout, pool <24h old but already $500k liq locked". Bad: "price went up a lot with high volume".
Edge cases:
- If verdict is SLEEPY (<5 pools passed): send a short note instead —
*runners — ${TODAY}* — sleepy session, only N pools cleared quality gate. Skipping top-5.Include the 1-2 survivors if any. - If ALL sources failed (every
*_OK=0): send*runners — ${TODAY}* — MONITOR_RUNNERS_ERROR, all GeckoTerminal endpoints failed. Check rate-limits/network.and skip the rest.
8. Log (runners)
Append to memory/logs/${TODAY}.md:
### token-movers
- Mode: movers | Source: geckoterminal | Scope: ${chain:-all-networks}
- Networks scanned: N (list)
- Source status: gt-global=ok|fail, per-network: ...
- Pools pre-gate: N / post-gate: N
- Verdict: STRONG|MIXED|SPECULATIVE|SLEEPY
- Top 5:
1. [TAG] TOKEN (chain) +X% — score XX, vol $Xm, liq $Xk — [one-line take]
2. ...
- Repeat runners (seen in prior 2 days): [list or "none"]
- Gate rejections breakdown: thin-vol=N, dumping=N, honeypot=N, too-new=N, rug-like=N
- Notification sent: yes|no (reason if no)
If a token appears as a runner on 3 days in a row, flag it in memory/MEMORY.md under "Active topics" — sustained multi-day runners are worth a deeper look.
Then go to Send the digest and stop.
Send the digest
Write the movers digest to /tmp/token-movers-report.md (keep it out of the repo root), send it with ./notify -f /tmp/token-movers-report.md, then make the deep-dive offer below.
Deep-dive offer (force-reply — movers runs only)
On a movers run that surfaced notable movers, follow the buttoned digest with a
one-tap offer to get the single-token deep report on any name the operator names. "Notable"
means: coingecko → at least one winner/loser or Notable signal was published; geckoterminal
→ verdict is not SLEEPY and ≥1 pick cleared the gate. Skip the offer on a SLEEPY / all-sources-failed
run, and never on a single-token run (that branch never reaches this section).
Because force_reply and inline buttons can't share one Telegram message, this is a SEPARATE
./notify sent AFTER the buttoned digest:
./notify "Want a deep-dive report on a mover? Reply with a ticker or contract." \
--force-reply --placeholder "e.g. WIF" \
--context "token-movers::deep-dive"
The operator's reply comes back as var="deep-dive:<their text>" and re-dispatches this skill,
which rule 0 routes into the single-token branch.
Dedup — once per day. Before offering, scan the last ~2 days of memory/logs/ for a
FORCE_REPLY_OFFERED: deep-dive line dated ${today}; if present, skip the offer. When you do
send it, append the marker to memory/logs/${today}.md under the run's ### token-movers entry:
- FORCE_REPLY_OFFERED: deep-dive
Mode: single-token (deep report)
A verdict-first report on one token. Snapshots of price, volume, and liquidity are table-stakes; the value is in the verdict — what changed and whether it matters. Every section either sharpens the verdict or is dropped. No filler, no "N/A", no "no specific context" sentences.
Config — resolve the target token
Resolve the target token in this order:
- From
${var}as parsed in the preamble:- a contract (
contractorcontract:chain, chain defaults tobasewhen omitted) → use it directly; - a
symbol(rule 11 /token:SYMBOL) → resolve symbol → contract first: call CoinGeckoGET /coins/{id}(or/search?query=SYMBOL→ top id) and readplatformsfor a contract + chain, preferring the deepest-liquidity chain; or GeckoTerminalGET /search/pools?query=SYMBOLand take the highest-reserve_in_usdpool's base-token address + network. If resolution fails, fall back to a CoinGecko/coins/{id}snapshot block (price, 24h volume, market cap, 7d & 30d change, ATH distance) and skip the on-chain pipeline below — do not abort.
- a contract (
- From
memory/token-report.mdwhen${var}istoken/single-token(or the resolver above found nothing):
# Token Report Config
## Tracked Token
| Contract | Chain |
|------------|-------|
| 0x… | base |
## Treasury Wallets (optional — omit the whole section if not used)
| Address | Role | RPC URL (optional) |
|---------|----------|--------------------------|
| 0x… | treasury | https://mainnet.base.org |
| 0x… | deployer | |
No-op when unconfigured. If the single-token branch is entered via token/single-token AND memory/token-report.md has no tracked token (file missing, or the Tracked Token table is empty), abort silently — log TOKEN_REPORT_NO_CONFIG, send no notification and write no article. An unconfigured token is not an error.
chain is mapped to a GeckoTerminal network slug for the API calls below. Common mappings: ethereum→eth, base→base, solana→solana, bsc→bsc, arbitrum→arbitrum, polygon→polygon_pos, optimism→optimism, avalanche→avax. Use the configured chain as-is if it already matches a GeckoTerminal slug. Below, ${network} is the resolved GeckoTerminal slug, ${chain} the human chain name, and CONTRACT_ADDRESS the resolved contract.
Read the last 30 days of memory/logs/*.md for prior TOKEN_REPORT_STATE: lines (written in step 8). These are the authoritative source of 1d / 7d / 30d deltas, because a stored price yesterday beats an API window that shifts under you.
1. Fetch core market data (GeckoTerminal — primary)
curl -s "https://api.geckoterminal.com/api/v2/networks/${network}/tokens/CONTRACT_ADDRESS"
curl -s "https://api.geckoterminal.com/api/v2/networks/${network}/tokens/CONTRACT_ADDRESS/pools?page=1"
# Top pool address from the pools response:
curl -s "https://api.geckoterminal.com/api/v2/networks/${network}/pools/POOL_ADDRESS/ohlcv/day?aggregate=1&limit=30"
curl -s "https://api.geckoterminal.com/api/v2/networks/${network}/pools/POOL_ADDRESS/ohlcv/hour?aggregate=1&limit=24"
curl -s "https://api.geckoterminal.com/api/v2/networks/${network}/pools/POOL_ADDRESS/trades"
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 750
- Forks
- 264
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
token-movers- Source
- github.com/aeonfun/aeon