Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Fire when the tracked token does something - new ATH, sharp 1h move, or operator-set target crossed. Silent on normal days.
.claude/skills/aeonfun-price-alert/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 279% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 234% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 295% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 447% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 155% | 0% |
> ${var} — Optional. Pass one or more target_price levels (comma-separated USD numbers, scientific notation allowed) to fire a one-time alert when the price crosses any of them. Empty = only ATH and sharp-move gates run. Pass dry-run to skip notify (state still updates). Pass set-target:<price> — the shape the Telegram force-reply sends (step 7) — to register a target and get a one-line confirmation.
Today is ${today}. repo-pulse reports star/fork deltas once a day, and the other scheduled digests run at fixed hours. None of them tell the operator "the price just hit a new high" or "the token moved 28% in the last hour" — both are events that warrant attention the moment they happen, not 14 hours later in a daily digest. This skill closes that window.
Scheduled daily digests are calm summaries. Real moves need real-time signal. Three classes of move are worth a same-day ping:
Everything else is noise not worth a same-day ping.
Reads:
memory/MEMORY.md "Tracked Token" section — the tracked token's contract/chain. If absent, exit silently.memory/topics/price-alert-state.json — last-known ATH, last-alert timestamps per event type, target-crossing history. Created with defaults on first run.Writes:
memory/topics/price-alert-state.json — updated state every run.memory/logs/${today}.md — one log block per run, even on OK../notify — only when a gate fires.No new secrets. Uses keyless DexScreener; falls back to WebFetch when a public curl GET is flaky (there is no network sandbox).
json{ "contract": "0xbf8e8f0e8866a7052f948c16508644347c57aba3", "chain": "base", "ath": { "price_usd": 0.000003757, "observed_at": "2026-05-10T19:00:00Z", "announced_at": "2026-05-10T19:30:00Z" }, "last_alerts": { "ath": "2026-05-10T19:30:00Z", "sharp_move": null, "target_hit": null }, "targets": { "0.000005": { "side": "above", "first_seen_below_at": "2026-05-11T08:00:00Z", "hit_at": null, "announced_at": null } } }
Key invariants:
ath.price_usd only ever monotonically increases. If a run sees a lower price than the stored ATH, leave the ATH alone.last_alerts.* powers the 4h dedup window. Each gate has its own clock.targets.${price}.side is above if current_price < target when the target was first observed (operator is waiting for the price to climb to it), and below otherwise. Set once, never flipped.targets.${price}.hit_at is set the run the cross happens. announced_at is set the run the notification fires. They differ only if the run lands inside a dedup window — but target alerts never re-fire, so hit_at == announced_at in practice.${var} starts with set-target: → set FROM_REPLY=1, MODE=execute. Strip the set-target: prefix (${var#set-target:}); the remainder is the target(s) list, parsed exactly like the comma-split below. This is the shape scripts/telegram-route.sh sends when the operator replies to the step-7 force-reply prompt. The run proceeds normally (register the target, don't fire on first observation) and closes the loop with a confirmation in step 7.${var} matches ^dry-run → MODE=dry-run. Strip the prefix; remainder (if any) is treated as targets.MODE=execute., (commas) and strip whitespace.5e-6), include it. Reject zero / negative / non-numeric tokens and log PRICE_ALERT_BAD_TARGET: ${token} — continue with the surviving targets.PRICE_ALERT_BAD_VAR: ${var} and exit. Normally no notify — but if FROM_REPLY=1, first close the loop with ./notify "Couldn't read \"${var#set-target:}\" as a price. Reply with a number like 0.000005." (a force-reply always deserves an acknowledgement).TARGETS=() is fine — ATH and sharp-move gates still run.bashmkdir -p memory/topics [ -f memory/topics/price-alert-state.json ] || cat > memory/topics/price-alert-state.json <<'EOF' {"contract":null,"chain":null,"ath":null,"last_alerts":{"ath":null,"sharp_move":null,"target_hit":null},"targets":{}} EOF
Parse the "Tracked Token" table in memory/MEMORY.md. Pull CONTRACT (column 2 of the first data row) and CHAIN (column 3, lowercased). If the section is missing, the table is empty, or the contract field doesn't match ^0x[0-9a-fA-F]{40}$ → log PRICE_ALERT_NO_TOKEN and exit (no notify, no state write).
If the state file's contract is set and differs from the resolved contract → log PRICE_ALERT_TOKEN_CHANGED and reset the state file to defaults with the new contract/chain. ATH starts over with the new token.
bashRESP=$(curl -fsS "https://api.dexscreener.com/latest/dex/tokens/${CONTRACT}" 2>/dev/null || echo "")
If the response is empty, falsy, or jq can't parse it → fall back to WebFetch on the same URL with prompt "Return the raw JSON body verbatim." and try again. If both paths fail → log PRICE_ALERT_FETCH_FAIL and exit with status ERROR (no notify, no state mutation beyond touching last_run_at).
From the parsed JSON:
.pairs[] to entries where .chainId == "${CHAIN}"..liquidity.usd — call this the deepest pool. If no pair matches the chain, fall back to the highest-liquidity pair across all chains and mark chain=fallback in the log.CURRENT_PRICE=.priceUsd (float), H1_CHANGE_PCT=.priceChange.h1 (float, may be missing → treat as 0), H24_CHANGE_PCT=.priceChange.h24 (float, may be missing → treat as 0), POOL_URL=.url.If CURRENT_PRICE is missing, zero, or non-numeric → log PRICE_ALERT_BAD_PRICE and exit ERROR.
prior_ath = state.ath.price_usd (null on first run)
new_ath = (prior_ath is null) OR (CURRENT_PRICE > prior_ath)If new_ath:
state.ath.price_usd = CURRENT_PRICE, state.ath.observed_at = NOW.ath alert was >4h ago (or never). On the first run after token initialisation, suppress the notification (we don't know if this is genuinely an ATH or just the starting baseline) — set state.ath.announced_at = NOW so subsequent strict-higher prices alert correctly.prior_ath is null (first run), record verdict ATH_BASELINE for the log; no notify.ATH.sharp = abs(H1_CHANGE_PCT) >= 20.0If sharp:
state.last_alerts.sharp_move is within the last 4h → verdict SHARP_MOVE_DEDUPED, no notify.SHARP_MOVE, notify, set state.last_alerts.sharp_move = NOW.Sign of H1_CHANGE_PCT decides the direction word in the message (up for ≥0, down for <0).
For each target in TARGETS:
state.targets: this is the first time the operator has set it. Record side (above if CURRENT_PRICE < target, else below), first_seen_below_at=NOW, hit_at=null, announced_at=null. Do not notify on first observation. This avoids "the price was already there when you set the target" noise.state.targets and hit_at is already set → skip; target-hit alerts are one-shot per side.side=above & CURRENT_PRICE >= target → crossed.side=below & CURRENT_PRICE <= target → crossed.hit_at=NOW, announced_at=NOW, verdict TARGET_HIT for this target, notify (subject to 4h dedup on last_alerts.target_hit).Targets that crossed in prior runs stay in the state file with hit_at set so they don't refire if the price wobbles back.
Run-level verdict precedence (used for the log status line and the notification ordering):
ATH — strict new high after baseline.TARGET_HIT — operator-set level crossed.SHARP_MOVE — ±20% in 1h.OK — no gate fired.If multiple gates fired in the same run, send one notification per gate. Each gate independently respects its own dedup clock — an ATH and a TARGET_HIT in the same run produce two messages; an ATH and a SHARP_MOVE_DEDUPED produce one.
ATH:
*$TOKEN — New ATH — ${today}*
$TOKEN just printed a new all-time high at $X.XXXXe-N.
Previous ATH: $Y.YYYYe-N (set ${prior_ath_age} ago).
24h move: ±Z.Z% · 1h move: ±W.W%
Chart: ${POOL_URL}SHARP_MOVE:
*$TOKEN — Sharp 1h Move — ${today}*
$TOKEN ${up|down} ${abs(h1):.1f}% in the last hour — now $X.XXXXe-N.
24h: ±Z.Z%
${one of: "Buyer wave — watch for follow-through." | "Selling pressure — watch the next hour for stabilisation." }
Chart: ${POOL_URL}(Direction phrase is hard-coded by sign: positive = buyer wave; negative = selling pressure. No freelance commentary.)
TARGET_HIT:
*$TOKEN — Target Hit — ${today}*
$TOKEN just crossed $${target} (now $X.XXXXe-N).
Direction: ${above|below}
24h: ±Z.Z% · 1h: ±W.W%
Chart: ${POOL_URL}If MODE == dry-run: build the messages, log the planned notifications, but skip ./notify. State still updates so dedup clocks advance correctly.
Cap each message at ~2500 chars; price-alert messages are short by nature and shouldn't approach this.
Build each firing gate's message to a file and send it with ./notify -f alert.md (one send per gate). State still advances even on a deduped run, so the dedup clocks stay correct.
Buttons and force_reply can't share one message, so this is a separate send. After a genuine ATH alert (post-baseline, not a deduped repeat), and only when no un-hit operator target currently sits above CURRENT_PRICE (don't nag operators who already queued one), offer to capture the next level:
bash./notify "New high — want an alert when $SYMBOL clears a level above this? Reply with a price." \ --force-reply --placeholder "e.g. 0.000005" \ --context "price-alert::set-target"
The reply routes back as var=set-target:<price> (handled in step 1). On that run, once the target is registered (step 6, first-observation — no cross alert), close the loop with a one-line confirmation: ./notify "Target set: \$<price> for $SYMBOL — I'll alert you when it crosses." (Send it only when a new target was actually registered this run.)
Rewrite memory/topics/price-alert-state.json atomically:
bashTMP=$(mktemp) jq --arg ts "$(date -u +%FT%TZ)" ' .last_run_at = $ts | .contract = $contract | .chain = $chain | .ath = $ath_obj | .last_alerts = $last_alerts_obj | .targets = $targets_obj ' memory/topics/price-alert-state.json > "$TMP" mv "$TMP" memory/topics/price-alert-state.json
Validate with jq empty memory/topics/price-alert-state.json after writing; if it fails, restore from a .bak copy and log PRICE_ALERT_STATE_CORRUPT. Keep one .bak rolling.
Cap state.targets to 20 most-recent entries (LRU by first_seen_below_at) so a long-running fork doesn't accumulate stale operator targets.
Append to memory/logs/${today}.md:
## Price Threshold Alert
- **Skill**: price-alert
- **Token**: ${SYMBOL} (${CONTRACT})
- **Current**: $X.XXXXe-N | 1h: ±W.W% | 24h: ±Z.Z%
- **ATH**: $Y.YYYYe-N (set ${YYYY-MM-DD HH:MM} UTC) [${UNCHANGED|NEW}]
- **Sharp-move gate**: ${FIRED|QUIET|DEDUPED}
- **Targets evaluated**: ${comma-list of target prices} → ${comma-list of per-target verdicts}
- **Verdicts fired**: ${comma-list, or NONE}
- **Notifications sent**: ${N}
- **Status**: ${PRICE_ALERT_OK | PRICE_ALERT_ATH | PRICE_ALERT_SHARP_MOVE | PRICE_ALERT_TARGET_HIT | PRICE_ALERT_DRY_RUN | PRICE_ALERT_NO_TOKEN | PRICE_ALERT_TOKEN_CHANGED | PRICE_ALERT_FETCH_FAIL | PRICE_ALERT_BAD_PRICE | PRICE_ALERT_BAD_VAR | PRICE_ALERT_STATE_CORRUPT}The status field carries the highest-priority gate fired this run, or the most relevant error. OK means the run completed cleanly and no gate fired.
| Status | Meaning | Notify? | |--------|---------|---------| | PRICE_ALERT_OK | Run completed, no gate fired | No | | PRICE_ALERT_ATH | New strict all-time high (post-baseline) | Yes | | PRICE_ALERT_SHARP_MOVE | ±20% in 1h, outside dedup window | Yes | | PRICE_ALERT_TARGET_HIT | Operator target crossed for the first time | Yes | | PRICE_ALERT_DRY_RUN | var=dry-run mode | No (state still updates) | | PRICE_ALERT_NO_TOKEN | No tracked token configured in MEMORY.md | No | | PRICE_ALERT_TOKEN_CHANGED | Tracked contract changed since last run; state reset | No | | PRICE_ALERT_FETCH_FAIL | Both curl and WebFetch failed | No | | PRICE_ALERT_BAD_PRICE | API returned malformed/zero price | No | | PRICE_ALERT_BAD_VAR | ${var} had non-empty, non-dry-run text but yielded zero valid targets | No | | PRICE_ALERT_STATE_CORRUPT | jq validation failed after write; restored from .bak | No |
DexScreener is keyless and public — curl works; there is no network sandbox. If a public curl GET is flaky, the WebFetch fallback kicks in (built-in Claude tool, prompt: "Return the raw JSON body verbatim."). There's no auth header, and the URL doesn't change between runs. Notify goes through ./notify, which stages to .pending-notify/; the workflow re-delivers any messages that failed to send after the run — no extra script needed.
last_alerts.* map is the only authority on whether to suppress.hit_at is set, the target never re-fires — even if the price wobbles back through the level. Operators add new targets if they want continued signal.priceUsd is the canonical mark.jq empty validation step. Corrupt writes restore from .bak.memory/logs/. This skill never modifies past log files. It only appends to today's.Other measured skills in the registry, with their headline benchmark lift.