- name
- token-movers
- description
- 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.
- metadata
- {"title":"Token Movers","category":"basics","var":"","tags":["crypto"],"mode":"write","requires":["COINGECKO_API_KEY?","ALCHEMY_API_KEY?","XAI_API_KEY?","BASE_RPC_URL?"],"capabilities":["external_api","sends_notifications"]}
<!-- autoresearch: variation B — consolidated hub. Folds monitor-runners (GeckoTerminal on-chain runner scan w/ composite Runner Score) and token-report (verdict-first single-token deep report) behind a source + mode selector. Movers = broad market scan (CoinGecko winners/losers/trending OR GeckoTerminal runners); single-token = deep per-token report. Sharper output everywhere: enrich, score, flag pump risk, lead with the verdict. -->
> **${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 like `layer-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 in `memory/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 the `deep-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…:base` or `WIF` (single-token report).
## Preamble (every run)
1. Read `memory/MEMORY.md` for context.
2. 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 its `TOKEN_REPORT_STATE:` delta lines — see that branch.)
3. **Parse `${var}` → `source` + `mode` (+ optional `token`/`chain`/`category`).** Trim whitespace; evaluate the rules top-to-bottom, first match wins (fully deterministic):
0. **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).
1. empty → **mode=movers, source=coingecko** (global). Go to **Movers branch**.
2. `coingecko` (case-insensitive) → **movers / coingecko** (global). Movers branch.
3. `geckoterminal` → **movers / geckoterminal** (all major networks). Movers branch.
4. starts with `geckoterminal:` or `chain:` → **movers / geckoterminal**, `chain` = remainder. Movers branch.
5. 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, use `token:eth` or its contract.)*
6. starts with `category:` → **movers / coingecko**, `category` = remainder. Movers branch.
7. equals `token` or `single-token` → **single-token**, token from `memory/token-report.md` config. Single-token branch.
8. starts with `token:` → **single-token**, resolve remainder as contract-or-symbol. Single-token branch.
9. 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.
10. contains a space or a hyphen and is not a contract (e.g. `layer-2`, `meme coins`) → **movers / coingecko**, `category` = value. Movers branch.
11. otherwise (a bare word, a plausible ticker) → **single-token / geckoterminal**, resolve `symbol` = value to its top contract. Single-token branch.
---
# 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:
```bash
# 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 with `USD`/`EUR`/`GBP` or 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_volume` vs `market_cap` ratio > 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 `Notable` section 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 `category` is 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 trending
- `GET /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 changes
- `attributes.volume_usd.{m5,m15,m30,h1,h6,h24}` — volume
- `attributes.market_cap_usd` / `attributes.fdv_usd` — market cap
- `attributes.transactions.h24.{buys,sells,buyers,sellers}` — activity
- `attributes.pool_created_at` — pool creation timestamp
- `attributes.reserve_in_usd` — liquidity
- `relationships.network.data.id` — chain name
- `relationships.base_token.data.id` — base token address (for dedup)
### 1. Fetch data (sequential, rate-limit aware)
```bash
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
GitHub에서 보기