onchain-monitor
Monitor blockchain addresses and contracts for notable activity
git clone --depth 1 https://github.com/aeonfun/aeon /tmp/onchain-monitor && cp -r /tmp/onchain-monitor/skills/onchain-monitor ~/.claude/skills/onchain-monitorSKILL.md
<!-- autoresearch: variation B — sharper output (decoded transfers + counterparty labels + ranked USD-denominated one-liners + TL;DR lede); folds in A's Alchemy+Etherscan-v2 input path and C's persistent state + source-status footer + dedup. -->
> **${var}** — Watch label or chain to check. Empty = all watches. `add-address:<0x… [chain]>` is the shape the Telegram force-reply sends — it appends a new watch and exits (see step 0).
If `${var}` is set, only monitor the watch with that label or watches on that chain.
## Config
Reads `memory/on-chain-watches.yml`. If the file is missing or `watches: []`, offer to add the first watch via a Telegram force-reply (only if no `add-address` prompt was offered in the last 2 days of `memory/logs/` — dedup so an unconfigured fork isn't nagged every run), then log `ON_CHAIN_NO_CONFIG` and exit cleanly (do **not** send an alert — empty config is not an error):
```bash
./notify "No addresses on watch yet. Paste one to monitor — a 0x… wallet, optionally its chain." \
--force-reply --placeholder "0x… base" \
--context "onchain-monitor::add-address"
```
The reply routes back as `var=add-address:<0x… [chain]>`, handled by the config-capture branch in step 0. Record `FORCE_REPLY_OFFERED: add-address` in the log when you send it.
```yaml
# memory/on-chain-watches.yml
watches:
- label: My Wallet
address: "0x1234...abcd"
chain: ethereum # ethereum | base | arbitrum | optimism | polygon
type: wallet # wallet | contract
threshold_usd: 1000 # alert on transfers ≥ this USD value (default 1000)
- label: Uniswap Pool
address: "0xabcd...5678"
chain: ethereum
type: contract
event_topics: # optional — only alert on these topic0 hashes
- "0xddf252ad..." # ERC20 Transfer
```
Optional `memory/known-addresses.yml` — counterparty label dictionary used to humanize alerts. Lowercase keys, free-text values:
```yaml
labels:
"0x28c6c06298d514db089934071355e5743bf21d60": "Binance 14"
"0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43": "Coinbase 10"
"0xe592427a0aece92de3edee1f18e0157c05861564": "Uniswap V3 Router"
"0x0000000000000000000000000000000000000000": "Zero (mint/burn)"
```
## State
`memory/on-chain-state.json` — per-watch state, persisted atomically after each successful run:
```json
{
"My Wallet": {
"last_block": 19345678,
"last_run": "2026-04-20T12:00:00Z",
"alerted_tx": ["0xabc...", "0xdef..."],
"median_usd_30d": 8500
}
}
```
- `last_block` — start block for the next run's fetch. Initialise to `current_block − 2400` (≈ 8h ETH) on first run.
- `alerted_tx` — tx hashes alerted in last 7 days, capped at 200. Used for cross-run dedup.
- `median_usd_30d` — rolling median USD size of transfers at this watch; powers the `WHALE-TRANSFER` tag.
Write the file via `mv` from a tempfile so a mid-run failure cannot corrupt state.
## Steps
Read `memory/MEMORY.md`, `memory/on-chain-watches.yml`, `memory/on-chain-state.json`, and the last 2 days of `memory/logs/` (for visibility only — state lives in the JSON file).
### 0. Config capture (Telegram force-reply)
Before the per-watch loop, intercept the add-a-watch reply. When `${var}` starts with `add-address:`, the operator replied to the force-reply prompt (offered in the Config section on an empty config) — append a watch and **exit** (no monitoring this invocation). The remainder is `<address> [chain]`:
```bash
case "${var}" in
add-address:*)
REST="$(printf '%s' "${var#add-address:}" | sed 's/^[[:space:]]*//')"
ADDR="$(printf '%s' "$REST" | awk '{print $1}')"
CHAIN="$(printf '%s' "$REST" | awk '{print tolower($2)}')"; CHAIN="${CHAIN:-ethereum}"
case "$CHAIN" in ethereum|base|arbitrum|optimism|polygon) ;; *) CHAIN=ethereum ;; esac
if ! printf '%s' "$ADDR" | grep -qiE '^0x[0-9a-f]{40}$'; then
./notify "Couldn't read \"$ADDR\" as an address. Reply with a 0x… wallet, optionally a chain."
exit 0
fi
mkdir -p memory; touch memory/on-chain-watches.yml
# Normalize an empty inline list so we can append block items, and ensure a watches: key exists.
sed -i.bak -E 's/^watches:[[:space:]]*\[\][[:space:]]*$/watches:/' memory/on-chain-watches.yml && rm -f memory/on-chain-watches.yml.bak
grep -q '^watches:' memory/on-chain-watches.yml || printf 'watches:\n' >> memory/on-chain-watches.yml
if grep -qi "$ADDR" memory/on-chain-watches.yml; then
./notify "Already watching ${ADDR}."
else
SHORT="$(printf '%s' "$ADDR" | sed -E 's/^(0x.{4}).*(.{4})$/\1…\2/')"
cat >> memory/on-chain-watches.yml <<EOF
- label: "$SHORT"
address: "$ADDR"
chain: $CHAIN
type: wallet
threshold_usd: 1000
EOF
./notify "Now watching ${SHORT} on ${CHAIN} (wallet, moves ≥\$1000). Edit memory/on-chain-watches.yml to tune."
fi
# log under ### onchain-monitor: - view: add-address (var="${var}") → $ADDR on $CHAIN
exit 0 ;;
esac
```
Defaults for a captured watch: `type: wallet`, `threshold_usd: 1000`, `label` = the shortened address. The operator refines chain/type/threshold by editing `memory/on-chain-watches.yml` directly. (This appends to the end of the file, which is correct because `watches:` is the only top-level key — if a future config grows more keys, insert under `watches:` instead of at EOF.)
For each watch (filtered by `${var}`):
### 1. Fetch raw activity from `last_block` → latest
**Path A — Alchemy** (preferred, if `ALCHEMY_API_KEY` set).
Wallets use `alchemy_getAssetTransfers` — one call returns categorized in/out history with `value`, `asset`, `category`, `hash`, `from`, `to`, `metadata.blockTimestamp`. Run it twice per watch (once with `toAddress`, once with `fromAddress`) and merge.
```bash
./secretcurl -m 10 -s -X POST "https://${network}.g.alchemy.com/v2/{ALCHEMY_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"alchemy_getAssetTransfers","params":[{
"fromBlock":"0x'${from_block_hex}Set up and run an Aeon agent instance — get started from scratch, pick which skills to turn on or install more from packs, reschedule or change what runs, edit what an existing skill does, fix a skill that isn't firing, set the STRATEGY.md north star and soul/ voice, turn a coding-agent chat into a scheduled Aeon skill, and mine past coding-agent conversations for recurring work worth automating as a skill. Use when the user mentions Aeon, aeon.yml, an Aeon skill / instance / routine / pack, asks to schedule, enable, edit, or debug an agent that runs on a cron, or asks what of their repeated/manual work Aeon could take over.
Mention/keyword sweep on social platforms for [REPLACE: KEYWORDS] — trends, sentiment, top posts
5 concrete real-life actions, leverage-scored against open loops with specificity and anti-fluff gates
Static config-correctness linter for this instance - catches the silent-failure class (unquoted schedules, duplicate keys, unconfigured skills, mode typos, broken requires/MCP refs) that no run-based health skill can see. Notifies only on problems.
Pull framework updates from the upstream Aeon repo into this instance - 3-way merges canon's new commits into a PR, never clobbering operator config.
Write a publication-ready article in one of three angles - a trending long-form piece, a watched-repo thesis, or a project-through-a-lens essay. Optional Replicate hero image with --visual.
Automatically merge open PRs that have passing CI, no blocking reviews, and no conflicts
Two-mode aeon.yml workflow builder - analyze inspects URLs and emits a tiered, signal-verified skill-enablement plan plus an aeon.yml diff; enable flips slugs to enabled:true and opens a PR.