[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6J4GC7RFRQAC25F7RKVH77M":3,"skill-same:01M6J4GC7RFRQAC25F7RKVH77M":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":32,"linkOnly":33,"markdownContent":34,"markdownBody":35,"frontMatter":36,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":53,"ownerHtmlUrl":54,"ownerBlogUrl":55,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6J4GC7RFRQAC25F7RKVH77M","onchain-monitor",null,"Monitor blockchain addresses and contracts for notable activity","MiroShark\u002Fmiroshark-aeon","MiroShark","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F304594232?v=4","01M6J466B8NNAZ4H6NNK3N3TT5","01M6J466FXNNWH622S3B9PC496",20,"MIT","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fonchain-monitor\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31],"onchain","monitor","onchain monitor","coingecko","api","notifications","etherscan","alchemy","activity","addresses","contracts","blockchain","for","crypto","2026-11-10 10:12:30",false,"---\nname: onchain-monitor\ndescription: Monitor blockchain addresses and contracts for notable activity\nmetadata:\n  title: Onchain Monitor\n  category: crypto\n  var: \"\"\n  tags:\n    - crypto\n  requires:\n    - ALCHEMY_API_KEY?\n    - COINGECKO_API_KEY?\n    - ETHERSCAN_API_KEY?\n  capabilities:\n    - external_api\n    - sends_notifications\n---\n\u003C!-- 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. -->\n\n> **${var}** — Watch label or chain to check. Empty = all watches. `add-address:\u003C0x… [chain]>` is the shape the Telegram force-reply sends — it appends a new watch and exits (see step 0).\n\nIf `${var}` is set, only monitor the watch with that label or watches on that chain.\n\n## Config\n\nReads `memory\u002Fon-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\u002Flogs\u002F` — 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):\n\n```bash\n.\u002Fnotify \"No addresses on watch yet. Paste one to monitor — a 0x… wallet, optionally its chain.\" \\\n  --force-reply --placeholder \"0x… base\" \\\n  --context \"onchain-monitor::add-address\"\n```\n\nThe reply routes back as `var=add-address:\u003C0x… [chain]>`, handled by the config-capture branch in step 0. Record `FORCE_REPLY_OFFERED: add-address` in the log when you send it.\n\n```yaml\n# memory\u002Fon-chain-watches.yml\nwatches:\n  - label: My Wallet\n    address: \"0x1234...abcd\"\n    chain: ethereum          # ethereum | base | arbitrum | optimism | polygon | robinhood\n    type: wallet             # wallet | contract\n    threshold_usd: 1000      # alert on transfers ≥ this USD value (default 1000)\n  - label: Uniswap Pool\n    address: \"0xabcd...5678\"\n    chain: ethereum\n    type: contract\n    event_topics:            # optional — only alert on these topic0 hashes\n      - \"0xddf252ad...\"      # ERC20 Transfer\n```\n\nOptional `memory\u002Fknown-addresses.yml` — counterparty label dictionary used to humanize alerts. Lowercase keys, free-text values:\n```yaml\nlabels:\n  \"0x28c6c06298d514db089934071355e5743bf21d60\": \"Binance 14\"\n  \"0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43\": \"Coinbase 10\"\n  \"0xe592427a0aece92de3edee1f18e0157c05861564\": \"Uniswap V3 Router\"\n  \"0x0000000000000000000000000000000000000000\": \"Zero (mint\u002Fburn)\"\n```\n\n## State\n\n`memory\u002Fon-chain-state.json` — per-watch state, persisted atomically after each successful run:\n\n```json\n{\n  \"My Wallet\": {\n    \"last_block\": 19345678,\n    \"last_run\": \"2026-04-20T12:00:00Z\",\n    \"last_block_hash\": \"0x…\",\n    \"alerted_tx\": [\n      {\"hash\": \"0xabc...\", \"block_number\": 19345690, \"first_seen\": \"2026-04-20T12:00:00Z\"},\n      {\"hash\": \"0xdef...\", \"block_number\": 19345702, \"first_seen\": \"2026-04-20T12:00:00Z\"}\n    ],\n    \"median_usd_30d\": 8500\n  }\n}\n```\n\n- `last_block` — start block for the next run's fetch. Initialise to `current_block − 2400` (≈ 8h ETH) on first run; on `robinhood` (~0.1s blocks) use `current_block − 280000` for the same ≈ 8h window. On `robinhood`, also cap every later run at that span: if `current_block − last_block` is more than 280000 (a delayed or caught-up run; 12h late is ~420k blocks, 15 windows per direction per watch), start from `current_block − 280000` and log `CLAMPED: skipped \u003Cn> blocks` for that watch, so a late run degrades to \"last ~8h\" instead of walking the whole gap.\n- `last_block_hash` — the canonical hash observed for `last_block` on the previous successful run. Before fetching new events, read that block again. If the hash differs, log `REORG: block \u003Cn> changed \u003Cold> → \u003Cnew>`, rewind the watch to `max(0, last_block − 12)` (or the chain's documented finality depth), and replay the range. Do not send an alert until the replay completes. Keep normal transaction-hash dedup during the replay: a reorg can re-mine the same transaction in a new block, and that must not alert twice. Legacy string entries from earlier state are hashes and dedup the same way.\n- `alerted_tx` — objects `{hash, block_number, first_seen}` for txs alerted in the last 7 days, capped at 200. Used for cross-run dedup by `hash`; purge by `first_seen`. `block_number` is the block the alert was seen in, kept for the audit trail.\n- `median_usd_30d` — rolling median USD size of transfers at this watch; powers the `WHALE-TRANSFER` tag.\n\nWrite the file via `mv` from a tempfile so a mid-run failure cannot corrupt state. `mv` is not an allowed Bash command here, so do that move inside one `python3 -c` call (write the tempfile, then `os.replace` it onto the target), or write the whole file in one step with Write.\n\n## Steps\n\nRead `memory\u002FMEMORY.md`, `memory\u002Fon-chain-watches.yml`, `memory\u002Fon-chain-state.json`, and the last 2 days of `memory\u002Flogs\u002F` (for visibility only — state lives in the JSON file).\n\n### 0. Config capture (Telegram force-reply)\n\nBefore 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 `\u003Caddress> [chain]`.\n\n**Shell shape.** Each Bash call is one allowed command (pipes are fine). The run refuses a call that sets a variable (`ADDR=\"$(...)\"`, `KEYQ=\"\"`, `cutoff=$(date ...)`), uses `case` \u002F `if` \u002F `for` or a `[ ... ] &&` test, joins commands with `;`, `&&` or `||`, or appends to a repo file with `>>` \u002F `cat >>`, and `sed`, `awk`, `printf`, `touch`, `rm` and `mv` are not allowed commands; each refusal costs a model turn. So parse `${var}` in your head, edit `memory\u002F` files with Write\u002FEdit, write every network slug, chain id, address, block number and window into each command literally (the `\u003CADDRESS>`, `\u003CFROM_BLOCK>` style placeholders in the snippets), make one call per watch, direction and window, and do file pruning and atomic writes inside one `python3 -c` call.\n\nWork it out in your head, not in a shell script:\n1. `REST` = the text after `add-address:`, leading spaces trimmed. `ADDR` = its first word; `CHAIN` = its second word lowercased, or `ethereum` when there is none. A `CHAIN` that is not one of `ethereum|base|arbitrum|optimism|polygon|robinhood` becomes `ethereum`.\n2. If `ADDR` is not `0x` plus 40 hex characters (case-insensitive), send this (ADDR written in literally) and stop:\n   ```bash\n   .\u002Fnotify \"Couldn't read \\\"\u003CADDR>\\\" as an address. Reply with a 0x… wallet, optionally a chain.\"\n   ```\n3. Read `memory\u002Fon-chain-watches.yml` (Read; create it with Write as `watches:` if it is missing). With Edit, turn a `watches: []` line into `watches:`, and add a `watches:` line if there is none, so block items can be appended.\n4. If the file already contains `ADDR` (case-insensitive), send `.\u002Fnotify \"Already watching \u003CADDR>.\"` and stop.\n5. Otherwise `SHORT` = `ADDR`'s first 6 characters (`0x` plus 4), then `…`, then its last 4. Append this block to the end of the file with Edit:\n   ```yaml\n     - label: \"\u003CSHORT>\"\n       address: \"\u003CADDR>\"\n       chain: \u003CCHAIN>\n       type: wallet\n       threshold_usd: 1000\n   ```\n   then send:\n   ```bash\n   .\u002Fnotify 'Now watching \u003CSHORT> on \u003CCHAIN> (wallet, moves ≥$1000). Edit memory\u002Fon-chain-watches.yml to tune.'\n   ```\n6. Log under `### onchain-monitor`: `- view: add-address (var=\"${var}\") → \u003CADDR> on \u003CCHAIN>`, then **exit**.\n\nDefaults for a captured watch: `type: wallet`, `threshold_usd: 1000`, `label` = the shortened address. The operator refines chain\u002Ftype\u002Fthreshold by editing `memory\u002Fon-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.)\n\nFor each watch (filtered by `${var}`):\n\n### 1. Fetch raw activity from `last_block` → the pinned `to_block`\n\nOnce per chain at the start of the run, resolve `current_block` and its hash. That\nnumber is the chain's `to_block`: every watch on that chain, and every source\n(Alchemy, Etherscan, RPC), queries up to it and no further. Never send `latest`,\n`endblock=99999999` or an open-ended range. Each watch keeps its own `from_block`\n(its `last_block`, after the clamp and any reorg rewind). For a watch with\n`last_block_hash`, perform the reorg check above before constructing any query\nwindows.\n\n**Path A — Alchemy** (preferred, if `ALCHEMY_API_KEY` set).\n\nWallets use `alchemy_getAssetTransfers` - one call returns categorized in\u002Fout history with `value`, `asset`, `category`, `hash`, `from`, `to`, `metadata.blockTimestamp`. Run it twice per watch (once with `toAddress`, once with `fromAddress`) and merge. Write the network slug, the address and both blocks in literally; `\u003CFROM_BLOCK_HEX>` \u002F `\u003CTO_BLOCK_HEX>` are the block numbers as hex digits without the `0x` (convert them yourself):\n\n```bash\n.\u002Fsecretcurl -m 10 -s -X POST \"https:\u002F\u002F\u003CNETWORK>.g.alchemy.com\u002Fv2\u002F{ALCHEMY_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"alchemy_getAssetTransfers\",\"params\":[{\n    \"fromBlock\":\"0x\u003CFROM_BLOCK_HEX>\",\n    \"toBlock\":\"0x\u003CTO_BLOCK_HEX>\",\n    \"toAddress\":\"\u003CADDRESS>\",\n    \"category\":[\"external\",\"internal\",\"erc20\",\"erc721\",\"erc1155\"],\n    \"withMetadata\":true,\"excludeZeroValue\":true,\"maxCount\":\"0x32\"\n  }]}'\n```\n\nContracts use `eth_getLogs` against the same Alchemy URL, with numeric hex `fromBlock` \u002F `toBlock` set to the watch's `from_block` \u002F the chain's `to_block`. Alchemy allows any block range while the response stays under 10,000 logs, and unlimited results for ranges of 5,000 blocks or fewer; on `Log response size exceeded`, re-query in 5,000-block windows (a busy `robinhood` contract hits this within minutes of ~0.1s blocks).\n\nChain → network slug: `ethereum=eth-mainnet`, `base=base-mainnet`, `arbitrum=arb-mainnet`, `optimism=opt-mainnet`, `polygon=polygon-mainnet`, `robinhood=robinhood-mainnet`. On `robinhood`, drop `\"internal\"` from the wallet `category` list: Alchemy rejects it there (\"The 'internal' category is not supported for this network\").\n\n**Path B — Etherscan v2 unified** (fallback, if Alchemy path fails or key unset).\n\nSingle endpoint, all 50+ chains via `chainid`. Works keyless at lower rate limit. Key check: read the \"Keys set for this run\" line in your run notes, never a shell test (those are refused and cost a turn). Append the key only when `ETHERSCAN_API_KEY` is on that line: then `\u003CKEYQ>` below is `&apikey={ETHERSCAN_API_KEY}` (.\u002Fsecretcurl's placeholder; a bare `$ETHERSCAN_API_KEY` is refused by the Bash permission analyzer), otherwise `\u003CKEYQ>` is left out (keyless works). Chain id, address and decimal blocks are written in literally, one call each.\n\nWallet, token transfers:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=account&action=tokentx&address=\u003CADDRESS>&startblock=\u003CFROM_BLOCK>&endblock=\u003CTO_BLOCK>&sort=desc\u003CKEYQ>\"\n```\nWallet, normal transactions:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=account&action=txlist&address=\u003CADDRESS>&startblock=\u003CFROM_BLOCK>&endblock=\u003CTO_BLOCK>&sort=desc\u003CKEYQ>\"\n```\nContract:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=logs&action=getLogs&address=\u003CADDRESS>&fromBlock=\u003CFROM_BLOCK>&toBlock=\u003CTO_BLOCK>\u003CKEYQ>\"\n```\n\nChain → chainid: `ethereum=1`, `base=8453`, `arbitrum=42161`, `optimism=10`, `polygon=137`, `robinhood=4663`. Etherscan's free tier refuses `chainid=4663` (\"Free API access is not supported for this chain\"), so a keyless `robinhood` watch skips Path B and uses Path C.\n\n**Path C — Robinhood Chain public RPC** (`robinhood` only; keyless fallback when Path A is unavailable and Path B is refused).\n\nWallets: ERC-20 `Transfer` logs with the watch address as recipient, then as sender. The public RPC caps a query without an `address` filter at **30,000 blocks**, so walk `from_block → to_block` (the chain's pinned block) in 30,000-block windows (≈ 10 windows per direction per 8h, ≈ 28 per day). Contracts: `eth_getLogs` with `address` set (no 30,000 cap). For **both** wallets and contracts, if a window answers `logs matched by query exceeds limit of 10000`, halve it and query both halves, repeating until each piece fits; a busy address hits this inside one 30,000-block window (the v4 PoolManager returns ~29.7k logs there, which takes 3 splits and 7 calls).\nOne call per window and direction, everything written in literally: topic0 `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` is the ERC-20 Transfer; `\u003CADDR_TOPIC>` is the 32-byte topic `0x000000000000000000000000` followed by the watch address without its `0x`; `\u003CLO_HEX>` \u002F `\u003CHI_HEX>` are the window bounds (`hi - lo \u003C 30000`) as `0x`-hex, converted yourself (the RPC rejects decimal).\n\nIncoming:\n```bash\ncurl -m 15 -s -X POST https:\u002F\u002Frpc.mainnet.chain.robinhood.com -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"fromBlock\":\"\u003CLO_HEX>\",\"toBlock\":\"\u003CHI_HEX>\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",null,\"\u003CADDR_TOPIC>\"]}]}'\n```\nOutgoing:\n```bash\ncurl -m 15 -s -X POST https:\u002F\u002Frpc.mainnet.chain.robinhood.com -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"fromBlock\":\"\u003CLO_HEX>\",\"toBlock\":\"\u003CHI_HEX>\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"\u003CADDR_TOPIC>\"]}]}'\n```\nERC-721 `Transfer` has the same topic0 and also matches these filters. Tell them apart by topic count: **3 topics** = ERC-20 (amount in `data`); **4 topics** = ERC-721 (`data` is empty, the token id is `topics[3]`). Keep 3-topic logs as `erc20`; tag 4-topic logs `erc721` and never call `decimals()` on them (NFT contracts revert). Read `symbol()` \u002F `decimals()` per distinct ERC-20 token with `eth_call` (`0x95d89b41` \u002F `0x313ce567`) on the same RPC. Path C sees token transfers only, not native ETH transfers; note `path=rpc (tokens only)` in the source footer. On HTTP 429, back off and retry the window; if it still fails, mark the watch `fail` as below.\n\n**Fetch fallback.** Both Alchemy and Etherscan carry their key in the URL via `.\u002Fsecretcurl` (`{ALCHEMY_API_KEY}` \u002F `{ETHERSCAN_API_KEY}` placeholders), so if a call fails, retry the exact same URL via **WebFetch**. For POSTs, WebFetch accepts the JSON body. Path C needs no key; on a non-429 failure, retry the same RPC POST through WebFetch the same way before marking the watch `fail`.\n\nIf every path for a watch fails, mark the watch `fail` in the source footer and continue to the next — never abort the whole run.\n\n### 2. Decode every transfer\n\nRequired fields per event (normalised across Alchemy \u002F Etherscan payloads):\n\n| Field | Source |\n|-------|--------|\n| `tx_hash` | `hash` \u002F `txHash` |\n| `block_number`, `timestamp` | `blockNum` \u002F `metadata.blockTimestamp` |\n| `category` | `external_eth` \\| `erc20` \\| `erc721` \\| `erc1155` \\| `internal` \\| `log` |\n| `direction` | `in` if `to == watch`, else `out` |\n| `token.symbol`, `token.decimals` | Alchemy returns inline; for Etherscan, use `tokenSymbol`\u002F`tokenDecimal` fields |\n| `value_token` | human amount, e.g. `1,234,567.89 USDC` |\n| `value_usd` | `value_token × price_usd` (see step 3) |\n| `counterparty` | the non-watch address on the transfer |\n| `counterparty_label` | lookup in `known-addresses.yml`, else `null` |\n\n### 3. USD-enrich in one bulk call\n\nCollect distinct `(chain, token_contract)` pairs from decoded transfers. Bulk price via CoinGecko, one call per chain with the chain and the comma-joined contracts written in literally. `\u003CCGQ>` is `&x_cg_demo_api_key={COINGECKO_API_KEY}` when `COINGECKO_API_KEY` is on the \"Keys set for this run\" line in your run notes (never a shell test: those are refused and cost a turn), otherwise it is left out:\n\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.coingecko.com\u002Fapi\u002Fv3\u002Fsimple\u002Ftoken_price\u002F\u003CCHAIN>?contract_addresses=\u003CJOINED>&vs_currencies=usd\u003CCGQ>\"\n```\n\nNative ETH\u002FMATIC\u002Fetc. use `simple\u002Fprice?ids=ethereum,matic-network,...`. If CoinGecko is unreachable or returns no price for a token, set `value_usd = null` and tag the event `UNPRICED` — keep it in the log, drop it from the notification (can't meaningfully threshold without USD).\n\n### 4. Filter\n\nDrop an individual event if any of:\n- `value_usd \u003C threshold_usd` (watch config, default $1000)\n- `value_usd \u003C $0.10` (hard dust floor — prevents airdrop \u002F phishing spam)\n- `tx_hash` matches the `hash` field (or, for legacy entries, the string itself) of an entry in this watch's `alerted_tx` (cross-run dedup, also during a reorg replay)\n- `category == \"log\"` and watch has `event_topics:` and the topic0 is not in the list\n\n### 5. Categorize surviving events\n\nTag each event with one short label so the alert says *what kind of move it was*:\n\n| Tag | Condition |\n|-----|-----------|\n| `CEX-IN` \u002F `CEX-OUT` | counterparty label contains an exchange name (`Binance`, `Coinbase`, `Kraken`, `OKX`, `Bybit`, `Bitfinex`) |\n| `DEX-SWAP` | counterparty label contains a router (`Uniswap`, `1inch`, `Curve`, `Sushi`, `Aerodrome`, `CoWSwap`) |\n| `BRIDGE` | counterparty label contains a bridge (`Across`, `Stargate`, `Hop`, `Synapse`, `Wormhole`, `Celer`) |\n| `MINT` \u002F `BURN` | counterparty is `0x000…000` or the token contract itself |\n| `WHALE-TRANSFER` | `value_usd > 10 × median_usd_30d` for this watch |\n| `UNKNOWN-IN` \u002F `UNKNOWN-OUT` | fallback — based on direction |\n\nA single event can only carry one tag; pick by priority CEX > DEX > BRIDGE > MINT\u002FBURN > WHALE > UNKNOWN.\n\n### 6. Format the alert\n\nOne notification per run. Sort all surviving events globally by `value_usd` desc; group the output by watch label (watches with zero surviving events are omitted entirely). Lead with a one-sentence TL;DR naming the single biggest move.\n\n```\n*On-Chain Alert - ${today}*\nTL;DR: My Wallet sent $1.2M USDC to Binance 14 (biggest move on any watch in 30d).\n\n*My Wallet* (ethereum)\n• CEX-OUT $1.2M USDC → Binance 14 - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n• DEX-SWAP $42k WETH → USDC via Uniswap V3 Router - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n\n*Uniswap Pool* (ethereum)\n• WHALE-TRANSFER $850k WETH out → 0x9f...a1 - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n\n3 events on 2 watches | sources: alchemy=ok, coingecko=ok, etherscan=skipped | last_block→${block}\n```\n\nCap the notification body at 10 events; if more survived, append `+N more — see memory\u002Flogs\u002F${today}.md`. The `.\u002Fnotify` call should use the explorer URL for each chain (`etherscan.io`, `basescan.org`, `arbiscan.io`, `optimistic.etherscan.io`, `polygonscan.com`, `robinhoodchain.blockscout.com`).\n\nSend the alert with `.\u002Fnotify -f alert.md`.\n\n### 6.1 Evidence mode — make every alert replayable\n\nWhen at least one watch completes, write a compact evidence receipt to\n`memory\u002Fevidence\u002F${today}-${run_id}.json` before sending the notification. This is\nan audit trail, not a safety verdict: it records what was queried and which source\nanswered, but it does not prove that an external RPC is honest.\n\n`run_id` is the UTC start timestamp with `:` replaced by `-`. The receipt must contain:\n\n```json\n{\n  \"schema\": \"aeon.onchain-monitor.evidence.v1\",\n  \"run_id\": \"2026-04-20T12-00-00Z\",\n  \"chains\": [{\n    \"chain\": \"robinhood\",\n    \"chain_id\": 4663,\n    \"from_block\": 92300000,\n    \"to_block\": 92347891,\n    \"block_hash\": \"0x…\",\n    \"sources\": {\"rpc\": \"ok\", \"alchemy\": \"skipped\", \"coingecko\": \"ok\"},\n    \"query_windows\": 12,\n    \"watches\": 2,\n    \"raw_events\": 16,\n    \"kept_events\": 2,\n    \"dropped_events\": 14,\n    \"unpriced_events\": 0,\n    \"tx_hashes\": [\"0x…\"]\n  }],\n  \"sha256\": \"\u003Cdigest of this object without sha256>\"\n}\n```\n\nRules:\n\n- Emit one entry in `chains` for every chain represented by the completed watches;\n  never merge ranges, block hashes, source statuses, counters, or tx hashes across\n  chains. A run may span multiple chains.\n- Pin each chain entry's `from_block` and `to_block` to its actual request range. Do\n  not use `latest` in the receipt; resolve it to a number first.\n- Fetch each chain entry's `block_hash` for `to_block` from the same source when\n  possible. If the source cannot provide it, use `null` and mark that source\n  `degraded`.\n- Record every split query in each chain entry's `query_windows`; this makes the\n  Robinhood 10,000-log fallback visible instead of silently changing the sample.\n- Lowercase each chain entry's `tx_hashes` and `block_hash`, sort the hashes\n  lexicographically and remove duplicates. Never\n  include API keys, full RPC URLs containing credentials, or raw response bodies.\n- Canonicalise the JSON with sorted keys, compact separators and UTF-8 text with\n  no `\\u` escapes (in Python: `json.dumps(obj, sort_keys=True,\n  separators=(\",\", \":\"), ensure_ascii=False).encode()`), compute SHA-256 using a\n  standard-library implementation, then add the `sha256` field and write\n  atomically via a temporary file and `mv` (`os.replace` in the same\n  `python3 -c` call, since `mv` is not an allowed Bash command). Python's default `ensure_ascii=True`\n  produces a different digest as soon as a label contains non-ASCII text.\n- Before writing, create `memory\u002Fevidence\u002F` if needed and prune receipts older than\n  30 days by the date in the filename, not by mtime (every Actions run is a fresh\n  checkout, so mtime is always today). Files that do not start with a\n  `YYYY-MM-DD-` date are left alone:\n  First `date -u -d '30 days ago' +%Y-%m-%d` as its own call (it prints CUTOFF),\n  then one `python3 -c` call with CUTOFF written in literally (a shell `for` loop\n  with `rm` is refused):\n  ```bash\n  python3 -c 'import glob, os, re; [os.remove(f) for f in glob.glob(\"memory\u002Fevidence\u002F*.json\") if os.path.isfile(f) and re.match(r\"\\d{4}-\\d{2}-\\d{2}-\", os.path.basename(f)) and os.path.basename(f)[:10] \u003C \"\u003CCUTOFF>\"]'\n  ```\n  A pruning failure is non-fatal but must make the source footer `evidence=fail`.\n- Include the receipt path and digest in the alert footer before calling `.\u002Fnotify`:\n  `evidence: memory\u002Fevidence\u002F\u003Cfile> · sha256:\u003Cfirst 12 hex chars>`.\n- If receipt writing fails, continue the monitor run but mark the source footer\n  `evidence=fail`; never claim that the alert is independently verified.\n\n### 6.2 Verify a receipt offline (optional)\n\nOperators can verify a receipt written by step 6.1 without contacting a chain,\nRPC, price API, or GitHub:\n\n```bash\nnode scripts\u002Fverify-onchain-evidence.mjs memory\u002Fevidence\u002F\u003Cfile>.json\n```\n\nThe verifier accepts only `aeon.onchain-monitor.evidence.v1`. It recomputes the\nSHA-256 over the receipt with `sha256` removed, recursively sorts object keys,\nuses compact UTF-8 JSON, and checks chain-local ranges, counters, block hashes,\nsource degradation, and sorted unique transaction hashes. It also rejects\ncredential-bearing URLs, secret-like fields, and raw response-body fields.\n\nExit code `0` means the file is internally consistent. Exit code `1` identifies\nthe failed field or invariant; exit code `2` means the input path was omitted.\nThis checks internal checksum and schema consistency only. Anyone able to replace\nthe receipt can also recompute its digest, so this is not a signature or proof of\nauthorship. It does not prove that an RPC was honest, that the chain was final, or\nthat the receipt describes external reality. Secret detection is best-effort,\nnot a guarantee that all sensitive content is absent. The verifier performs no\nnetwork access.\n\n### 7. Persist state and log\n\nFor each watch whose fetch **succeeded** (success ≠ \"events found\"):\n- `last_block ← to_block` (the chain's pinned `current_block`)\n  - `last_block_hash ← hash(to_block)` from that same pinned block read\n- `last_run ← now` (ISO 8601 UTC)\n- `alerted_tx ← (new_alerted_entries + alerted_tx)[:200]`, where each new entry is `{hash: tx_hash, block_number, first_seen: now}`; purge entries whose `first_seen` is older than 7d\n- `median_usd_30d ← median of all value_usd from this watch's transfers in last 30d` (read from recent logs; skip recomputation if \u003C 5 samples)\n\nWrite `memory\u002Fon-chain-state.json` atomically (tempfile + `mv`, done as `os.replace` inside `python3 -c`, or one Write; see **State**).\n\nAppend **every** decoded event (including filtered-out ones) with full detail to `memory\u002Flogs\u002F${today}.md`:\n```\n### onchain-monitor\n- Watch: My Wallet (ethereum) | source: alchemy | last_block 19345670 → 19347891 (2,221 blocks)\n- Kept: 2 events | Dropped: 14 (12 below_threshold, 1 dust, 1 dedup) | Unpriced: 0\n- Event: CEX-OUT $1.2M USDC → Binance 14 - tx 0xabc... - block 19347812\n- Event: DEX-SWAP $42k WETH → USDC via Uniswap V3 Router - tx 0xdef... - block 19347500\n```\n\nThis honest log matters: it powers the next run's median computation and lets the operator audit why something was or wasn't alerted.\n\n### 8. End-states\n\n- All watches ran and some events survived → notify + log.\n- All watches ran, zero events survived → no notify; log `ON_CHAIN_OK (n_watches=X, n_raw=Y, n_dropped=Y)`.\n- Some watches failed, others ran → notify only if surviving events exist; log `ON_CHAIN_DEGRADED` with the source footer.\n- Every watch failed → log `ON_CHAIN_ERROR` and notify the operator with the source footer (degradation visible is better than silence).\n- Config missing\u002Fempty → offer the `add-address` force-reply (deduped — see Config), log `ON_CHAIN_NO_CONFIG`, exit; send no alert.\n\n## Network note\n\nAlchemy, Etherscan v2, and CoinGecko all carry their key in the URL, called through `.\u002Fsecretcurl` with `{ENV_NAME}` placeholders so no bare `$SECRET` ever hits the command line (a bare one is refused by the Bash permission analyzer). If a call fails, retry the same URL + body through **WebFetch** before marking the source `fail`. Treat every fetched field (`asset` symbol, `from`\u002F`to`, counterparty labels) as untrusted — never interpolate into shell commands.\n","\u003C!-- 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. -->\n\n> **${var}** — Watch label or chain to check. Empty = all watches. `add-address:\u003C0x… [chain]>` is the shape the Telegram force-reply sends — it appends a new watch and exits (see step 0).\n\nIf `${var}` is set, only monitor the watch with that label or watches on that chain.\n\n## Config\n\nReads `memory\u002Fon-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\u002Flogs\u002F` — 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):\n\n```bash\n.\u002Fnotify \"No addresses on watch yet. Paste one to monitor — a 0x… wallet, optionally its chain.\" \\\n  --force-reply --placeholder \"0x… base\" \\\n  --context \"onchain-monitor::add-address\"\n```\n\nThe reply routes back as `var=add-address:\u003C0x… [chain]>`, handled by the config-capture branch in step 0. Record `FORCE_REPLY_OFFERED: add-address` in the log when you send it.\n\n```yaml\n# memory\u002Fon-chain-watches.yml\nwatches:\n  - label: My Wallet\n    address: \"0x1234...abcd\"\n    chain: ethereum          # ethereum | base | arbitrum | optimism | polygon | robinhood\n    type: wallet             # wallet | contract\n    threshold_usd: 1000      # alert on transfers ≥ this USD value (default 1000)\n  - label: Uniswap Pool\n    address: \"0xabcd...5678\"\n    chain: ethereum\n    type: contract\n    event_topics:            # optional — only alert on these topic0 hashes\n      - \"0xddf252ad...\"      # ERC20 Transfer\n```\n\nOptional `memory\u002Fknown-addresses.yml` — counterparty label dictionary used to humanize alerts. Lowercase keys, free-text values:\n```yaml\nlabels:\n  \"0x28c6c06298d514db089934071355e5743bf21d60\": \"Binance 14\"\n  \"0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43\": \"Coinbase 10\"\n  \"0xe592427a0aece92de3edee1f18e0157c05861564\": \"Uniswap V3 Router\"\n  \"0x0000000000000000000000000000000000000000\": \"Zero (mint\u002Fburn)\"\n```\n\n## State\n\n`memory\u002Fon-chain-state.json` — per-watch state, persisted atomically after each successful run:\n\n```json\n{\n  \"My Wallet\": {\n    \"last_block\": 19345678,\n    \"last_run\": \"2026-04-20T12:00:00Z\",\n    \"last_block_hash\": \"0x…\",\n    \"alerted_tx\": [\n      {\"hash\": \"0xabc...\", \"block_number\": 19345690, \"first_seen\": \"2026-04-20T12:00:00Z\"},\n      {\"hash\": \"0xdef...\", \"block_number\": 19345702, \"first_seen\": \"2026-04-20T12:00:00Z\"}\n    ],\n    \"median_usd_30d\": 8500\n  }\n}\n```\n\n- `last_block` — start block for the next run's fetch. Initialise to `current_block − 2400` (≈ 8h ETH) on first run; on `robinhood` (~0.1s blocks) use `current_block − 280000` for the same ≈ 8h window. On `robinhood`, also cap every later run at that span: if `current_block − last_block` is more than 280000 (a delayed or caught-up run; 12h late is ~420k blocks, 15 windows per direction per watch), start from `current_block − 280000` and log `CLAMPED: skipped \u003Cn> blocks` for that watch, so a late run degrades to \"last ~8h\" instead of walking the whole gap.\n- `last_block_hash` — the canonical hash observed for `last_block` on the previous successful run. Before fetching new events, read that block again. If the hash differs, log `REORG: block \u003Cn> changed \u003Cold> → \u003Cnew>`, rewind the watch to `max(0, last_block − 12)` (or the chain's documented finality depth), and replay the range. Do not send an alert until the replay completes. Keep normal transaction-hash dedup during the replay: a reorg can re-mine the same transaction in a new block, and that must not alert twice. Legacy string entries from earlier state are hashes and dedup the same way.\n- `alerted_tx` — objects `{hash, block_number, first_seen}` for txs alerted in the last 7 days, capped at 200. Used for cross-run dedup by `hash`; purge by `first_seen`. `block_number` is the block the alert was seen in, kept for the audit trail.\n- `median_usd_30d` — rolling median USD size of transfers at this watch; powers the `WHALE-TRANSFER` tag.\n\nWrite the file via `mv` from a tempfile so a mid-run failure cannot corrupt state. `mv` is not an allowed Bash command here, so do that move inside one `python3 -c` call (write the tempfile, then `os.replace` it onto the target), or write the whole file in one step with Write.\n\n## Steps\n\nRead `memory\u002FMEMORY.md`, `memory\u002Fon-chain-watches.yml`, `memory\u002Fon-chain-state.json`, and the last 2 days of `memory\u002Flogs\u002F` (for visibility only — state lives in the JSON file).\n\n### 0. Config capture (Telegram force-reply)\n\nBefore 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 `\u003Caddress> [chain]`.\n\n**Shell shape.** Each Bash call is one allowed command (pipes are fine). The run refuses a call that sets a variable (`ADDR=\"$(...)\"`, `KEYQ=\"\"`, `cutoff=$(date ...)`), uses `case` \u002F `if` \u002F `for` or a `[ ... ] &&` test, joins commands with `;`, `&&` or `||`, or appends to a repo file with `>>` \u002F `cat >>`, and `sed`, `awk`, `printf`, `touch`, `rm` and `mv` are not allowed commands; each refusal costs a model turn. So parse `${var}` in your head, edit `memory\u002F` files with Write\u002FEdit, write every network slug, chain id, address, block number and window into each command literally (the `\u003CADDRESS>`, `\u003CFROM_BLOCK>` style placeholders in the snippets), make one call per watch, direction and window, and do file pruning and atomic writes inside one `python3 -c` call.\n\nWork it out in your head, not in a shell script:\n1. `REST` = the text after `add-address:`, leading spaces trimmed. `ADDR` = its first word; `CHAIN` = its second word lowercased, or `ethereum` when there is none. A `CHAIN` that is not one of `ethereum|base|arbitrum|optimism|polygon|robinhood` becomes `ethereum`.\n2. If `ADDR` is not `0x` plus 40 hex characters (case-insensitive), send this (ADDR written in literally) and stop:\n   ```bash\n   .\u002Fnotify \"Couldn't read \\\"\u003CADDR>\\\" as an address. Reply with a 0x… wallet, optionally a chain.\"\n   ```\n3. Read `memory\u002Fon-chain-watches.yml` (Read; create it with Write as `watches:` if it is missing). With Edit, turn a `watches: []` line into `watches:`, and add a `watches:` line if there is none, so block items can be appended.\n4. If the file already contains `ADDR` (case-insensitive), send `.\u002Fnotify \"Already watching \u003CADDR>.\"` and stop.\n5. Otherwise `SHORT` = `ADDR`'s first 6 characters (`0x` plus 4), then `…`, then its last 4. Append this block to the end of the file with Edit:\n   ```yaml\n     - label: \"\u003CSHORT>\"\n       address: \"\u003CADDR>\"\n       chain: \u003CCHAIN>\n       type: wallet\n       threshold_usd: 1000\n   ```\n   then send:\n   ```bash\n   .\u002Fnotify 'Now watching \u003CSHORT> on \u003CCHAIN> (wallet, moves ≥$1000). Edit memory\u002Fon-chain-watches.yml to tune.'\n   ```\n6. Log under `### onchain-monitor`: `- view: add-address (var=\"${var}\") → \u003CADDR> on \u003CCHAIN>`, then **exit**.\n\nDefaults for a captured watch: `type: wallet`, `threshold_usd: 1000`, `label` = the shortened address. The operator refines chain\u002Ftype\u002Fthreshold by editing `memory\u002Fon-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.)\n\nFor each watch (filtered by `${var}`):\n\n### 1. Fetch raw activity from `last_block` → the pinned `to_block`\n\nOnce per chain at the start of the run, resolve `current_block` and its hash. That\nnumber is the chain's `to_block`: every watch on that chain, and every source\n(Alchemy, Etherscan, RPC), queries up to it and no further. Never send `latest`,\n`endblock=99999999` or an open-ended range. Each watch keeps its own `from_block`\n(its `last_block`, after the clamp and any reorg rewind). For a watch with\n`last_block_hash`, perform the reorg check above before constructing any query\nwindows.\n\n**Path A — Alchemy** (preferred, if `ALCHEMY_API_KEY` set).\n\nWallets use `alchemy_getAssetTransfers` - one call returns categorized in\u002Fout history with `value`, `asset`, `category`, `hash`, `from`, `to`, `metadata.blockTimestamp`. Run it twice per watch (once with `toAddress`, once with `fromAddress`) and merge. Write the network slug, the address and both blocks in literally; `\u003CFROM_BLOCK_HEX>` \u002F `\u003CTO_BLOCK_HEX>` are the block numbers as hex digits without the `0x` (convert them yourself):\n\n```bash\n.\u002Fsecretcurl -m 10 -s -X POST \"https:\u002F\u002F\u003CNETWORK>.g.alchemy.com\u002Fv2\u002F{ALCHEMY_API_KEY}\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"alchemy_getAssetTransfers\",\"params\":[{\n    \"fromBlock\":\"0x\u003CFROM_BLOCK_HEX>\",\n    \"toBlock\":\"0x\u003CTO_BLOCK_HEX>\",\n    \"toAddress\":\"\u003CADDRESS>\",\n    \"category\":[\"external\",\"internal\",\"erc20\",\"erc721\",\"erc1155\"],\n    \"withMetadata\":true,\"excludeZeroValue\":true,\"maxCount\":\"0x32\"\n  }]}'\n```\n\nContracts use `eth_getLogs` against the same Alchemy URL, with numeric hex `fromBlock` \u002F `toBlock` set to the watch's `from_block` \u002F the chain's `to_block`. Alchemy allows any block range while the response stays under 10,000 logs, and unlimited results for ranges of 5,000 blocks or fewer; on `Log response size exceeded`, re-query in 5,000-block windows (a busy `robinhood` contract hits this within minutes of ~0.1s blocks).\n\nChain → network slug: `ethereum=eth-mainnet`, `base=base-mainnet`, `arbitrum=arb-mainnet`, `optimism=opt-mainnet`, `polygon=polygon-mainnet`, `robinhood=robinhood-mainnet`. On `robinhood`, drop `\"internal\"` from the wallet `category` list: Alchemy rejects it there (\"The 'internal' category is not supported for this network\").\n\n**Path B — Etherscan v2 unified** (fallback, if Alchemy path fails or key unset).\n\nSingle endpoint, all 50+ chains via `chainid`. Works keyless at lower rate limit. Key check: read the \"Keys set for this run\" line in your run notes, never a shell test (those are refused and cost a turn). Append the key only when `ETHERSCAN_API_KEY` is on that line: then `\u003CKEYQ>` below is `&apikey={ETHERSCAN_API_KEY}` (.\u002Fsecretcurl's placeholder; a bare `$ETHERSCAN_API_KEY` is refused by the Bash permission analyzer), otherwise `\u003CKEYQ>` is left out (keyless works). Chain id, address and decimal blocks are written in literally, one call each.\n\nWallet, token transfers:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=account&action=tokentx&address=\u003CADDRESS>&startblock=\u003CFROM_BLOCK>&endblock=\u003CTO_BLOCK>&sort=desc\u003CKEYQ>\"\n```\nWallet, normal transactions:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=account&action=txlist&address=\u003CADDRESS>&startblock=\u003CFROM_BLOCK>&endblock=\u003CTO_BLOCK>&sort=desc\u003CKEYQ>\"\n```\nContract:\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.etherscan.io\u002Fv2\u002Fapi?chainid=\u003CCHAINID>&module=logs&action=getLogs&address=\u003CADDRESS>&fromBlock=\u003CFROM_BLOCK>&toBlock=\u003CTO_BLOCK>\u003CKEYQ>\"\n```\n\nChain → chainid: `ethereum=1`, `base=8453`, `arbitrum=42161`, `optimism=10`, `polygon=137`, `robinhood=4663`. Etherscan's free tier refuses `chainid=4663` (\"Free API access is not supported for this chain\"), so a keyless `robinhood` watch skips Path B and uses Path C.\n\n**Path C — Robinhood Chain public RPC** (`robinhood` only; keyless fallback when Path A is unavailable and Path B is refused).\n\nWallets: ERC-20 `Transfer` logs with the watch address as recipient, then as sender. The public RPC caps a query without an `address` filter at **30,000 blocks**, so walk `from_block → to_block` (the chain's pinned block) in 30,000-block windows (≈ 10 windows per direction per 8h, ≈ 28 per day). Contracts: `eth_getLogs` with `address` set (no 30,000 cap). For **both** wallets and contracts, if a window answers `logs matched by query exceeds limit of 10000`, halve it and query both halves, repeating until each piece fits; a busy address hits this inside one 30,000-block window (the v4 PoolManager returns ~29.7k logs there, which takes 3 splits and 7 calls).\nOne call per window and direction, everything written in literally: topic0 `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` is the ERC-20 Transfer; `\u003CADDR_TOPIC>` is the 32-byte topic `0x000000000000000000000000` followed by the watch address without its `0x`; `\u003CLO_HEX>` \u002F `\u003CHI_HEX>` are the window bounds (`hi - lo \u003C 30000`) as `0x`-hex, converted yourself (the RPC rejects decimal).\n\nIncoming:\n```bash\ncurl -m 15 -s -X POST https:\u002F\u002Frpc.mainnet.chain.robinhood.com -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"fromBlock\":\"\u003CLO_HEX>\",\"toBlock\":\"\u003CHI_HEX>\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",null,\"\u003CADDR_TOPIC>\"]}]}'\n```\nOutgoing:\n```bash\ncurl -m 15 -s -X POST https:\u002F\u002Frpc.mainnet.chain.robinhood.com -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"fromBlock\":\"\u003CLO_HEX>\",\"toBlock\":\"\u003CHI_HEX>\",\"topics\":[\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\",\"\u003CADDR_TOPIC>\"]}]}'\n```\nERC-721 `Transfer` has the same topic0 and also matches these filters. Tell them apart by topic count: **3 topics** = ERC-20 (amount in `data`); **4 topics** = ERC-721 (`data` is empty, the token id is `topics[3]`). Keep 3-topic logs as `erc20`; tag 4-topic logs `erc721` and never call `decimals()` on them (NFT contracts revert). Read `symbol()` \u002F `decimals()` per distinct ERC-20 token with `eth_call` (`0x95d89b41` \u002F `0x313ce567`) on the same RPC. Path C sees token transfers only, not native ETH transfers; note `path=rpc (tokens only)` in the source footer. On HTTP 429, back off and retry the window; if it still fails, mark the watch `fail` as below.\n\n**Fetch fallback.** Both Alchemy and Etherscan carry their key in the URL via `.\u002Fsecretcurl` (`{ALCHEMY_API_KEY}` \u002F `{ETHERSCAN_API_KEY}` placeholders), so if a call fails, retry the exact same URL via **WebFetch**. For POSTs, WebFetch accepts the JSON body. Path C needs no key; on a non-429 failure, retry the same RPC POST through WebFetch the same way before marking the watch `fail`.\n\nIf every path for a watch fails, mark the watch `fail` in the source footer and continue to the next — never abort the whole run.\n\n### 2. Decode every transfer\n\nRequired fields per event (normalised across Alchemy \u002F Etherscan payloads):\n\n| Field | Source |\n|-------|--------|\n| `tx_hash` | `hash` \u002F `txHash` |\n| `block_number`, `timestamp` | `blockNum` \u002F `metadata.blockTimestamp` |\n| `category` | `external_eth` \\| `erc20` \\| `erc721` \\| `erc1155` \\| `internal` \\| `log` |\n| `direction` | `in` if `to == watch`, else `out` |\n| `token.symbol`, `token.decimals` | Alchemy returns inline; for Etherscan, use `tokenSymbol`\u002F`tokenDecimal` fields |\n| `value_token` | human amount, e.g. `1,234,567.89 USDC` |\n| `value_usd` | `value_token × price_usd` (see step 3) |\n| `counterparty` | the non-watch address on the transfer |\n| `counterparty_label` | lookup in `known-addresses.yml`, else `null` |\n\n### 3. USD-enrich in one bulk call\n\nCollect distinct `(chain, token_contract)` pairs from decoded transfers. Bulk price via CoinGecko, one call per chain with the chain and the comma-joined contracts written in literally. `\u003CCGQ>` is `&x_cg_demo_api_key={COINGECKO_API_KEY}` when `COINGECKO_API_KEY` is on the \"Keys set for this run\" line in your run notes (never a shell test: those are refused and cost a turn), otherwise it is left out:\n\n```bash\n.\u002Fsecretcurl -m 10 -s \"https:\u002F\u002Fapi.coingecko.com\u002Fapi\u002Fv3\u002Fsimple\u002Ftoken_price\u002F\u003CCHAIN>?contract_addresses=\u003CJOINED>&vs_currencies=usd\u003CCGQ>\"\n```\n\nNative ETH\u002FMATIC\u002Fetc. use `simple\u002Fprice?ids=ethereum,matic-network,...`. If CoinGecko is unreachable or returns no price for a token, set `value_usd = null` and tag the event `UNPRICED` — keep it in the log, drop it from the notification (can't meaningfully threshold without USD).\n\n### 4. Filter\n\nDrop an individual event if any of:\n- `value_usd \u003C threshold_usd` (watch config, default $1000)\n- `value_usd \u003C $0.10` (hard dust floor — prevents airdrop \u002F phishing spam)\n- `tx_hash` matches the `hash` field (or, for legacy entries, the string itself) of an entry in this watch's `alerted_tx` (cross-run dedup, also during a reorg replay)\n- `category == \"log\"` and watch has `event_topics:` and the topic0 is not in the list\n\n### 5. Categorize surviving events\n\nTag each event with one short label so the alert says *what kind of move it was*:\n\n| Tag | Condition |\n|-----|-----------|\n| `CEX-IN` \u002F `CEX-OUT` | counterparty label contains an exchange name (`Binance`, `Coinbase`, `Kraken`, `OKX`, `Bybit`, `Bitfinex`) |\n| `DEX-SWAP` | counterparty label contains a router (`Uniswap`, `1inch`, `Curve`, `Sushi`, `Aerodrome`, `CoWSwap`) |\n| `BRIDGE` | counterparty label contains a bridge (`Across`, `Stargate`, `Hop`, `Synapse`, `Wormhole`, `Celer`) |\n| `MINT` \u002F `BURN` | counterparty is `0x000…000` or the token contract itself |\n| `WHALE-TRANSFER` | `value_usd > 10 × median_usd_30d` for this watch |\n| `UNKNOWN-IN` \u002F `UNKNOWN-OUT` | fallback — based on direction |\n\nA single event can only carry one tag; pick by priority CEX > DEX > BRIDGE > MINT\u002FBURN > WHALE > UNKNOWN.\n\n### 6. Format the alert\n\nOne notification per run. Sort all surviving events globally by `value_usd` desc; group the output by watch label (watches with zero surviving events are omitted entirely). Lead with a one-sentence TL;DR naming the single biggest move.\n\n```\n*On-Chain Alert - ${today}*\nTL;DR: My Wallet sent $1.2M USDC to Binance 14 (biggest move on any watch in 30d).\n\n*My Wallet* (ethereum)\n• CEX-OUT $1.2M USDC → Binance 14 - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n• DEX-SWAP $42k WETH → USDC via Uniswap V3 Router - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n\n*Uniswap Pool* (ethereum)\n• WHALE-TRANSFER $850k WETH out → 0x9f...a1 - [tx](https:\u002F\u002Fetherscan.io\u002Ftx\u002F0x...)\n\n3 events on 2 watches | sources: alchemy=ok, coingecko=ok, etherscan=skipped | last_block→${block}\n```\n\nCap the notification body at 10 events; if more survived, append `+N more — see memory\u002Flogs\u002F${today}.md`. The `.\u002Fnotify` call should use the explorer URL for each chain (`etherscan.io`, `basescan.org`, `arbiscan.io`, `optimistic.etherscan.io`, `polygonscan.com`, `robinhoodchain.blockscout.com`).\n\nSend the alert with `.\u002Fnotify -f alert.md`.\n\n### 6.1 Evidence mode — make every alert replayable\n\nWhen at least one watch completes, write a compact evidence receipt to\n`memory\u002Fevidence\u002F${today}-${run_id}.json` before sending the notification. This is\nan audit trail, not a safety verdict: it records what was queried and which source\nanswered, but it does not prove that an external RPC is honest.\n\n`run_id` is the UTC start timestamp with `:` replaced by `-`. The receipt must contain:\n\n```json\n{\n  \"schema\": \"aeon.onchain-monitor.evidence.v1\",\n  \"run_id\": \"2026-04-20T12-00-00Z\",\n  \"chains\": [{\n    \"chain\": \"robinhood\",\n    \"chain_id\": 4663,\n    \"from_block\": 92300000,\n    \"to_block\": 92347891,\n    \"block_hash\": \"0x…\",\n    \"sources\": {\"rpc\": \"ok\", \"alchemy\": \"skipped\", \"coingecko\": \"ok\"},\n    \"query_windows\": 12,\n    \"watches\": 2,\n    \"raw_events\": 16,\n    \"kept_events\": 2,\n    \"dropped_events\": 14,\n    \"unpriced_events\": 0,\n    \"tx_hashes\": [\"0x…\"]\n  }],\n  \"sha256\": \"\u003Cdigest of this object without sha256>\"\n}\n```\n\nRules:\n\n- Emit one entry in `chains` for every chain represented by the completed watches;\n  never merge ranges, block hashes, source statuses, counters, or tx hashes across\n  chains. A run may span multiple chains.\n- Pin each chain entry's `from_block` and `to_block` to its actual request range. Do\n  not use `latest` in the receipt; resolve it to a number first.\n- Fetch each chain entry's `block_hash` for `to_block` from the same source when\n  possible. If the source cannot provide it, use `null` and mark that source\n  `degraded`.\n- Record every split query in each chain entry's `query_windows`; this makes the\n  Robinhood 10,000-log fallback visible instead of silently changing the sample.\n- Lowercase each chain entry's `tx_hashes` and `block_hash`, sort the hashes\n  lexicographically and remove duplicates. Never\n  include API keys, full RPC URLs containing credentials, or raw response bodies.\n- Canonicalise the JSON with sorted keys, compact separators and UTF-8 text with\n  no `\\u` escapes (in Python: `json.dumps(obj, sort_keys=True,\n  separators=(\",\", \":\"), ensure_ascii=False).encode()`), compute SHA-256 using a\n  standard-library implementation, then add the `sha256` field and write\n  atomically via a temporary file and `mv` (`os.replace` in the same\n  `python3 -c` call, since `mv` is not an allowed Bash command). Python's default `ensure_ascii=True`\n  produces a different digest as soon as a label contains non-ASCII text.\n- Before writing, create `memory\u002Fevidence\u002F` if needed and prune receipts older than\n  30 days by the date in the filename, not by mtime (every Actions run is a fresh\n  checkout, so mtime is always today). Files that do not start with a\n  `YYYY-MM-DD-` date are left alone:\n  First `date -u -d '30 days ago' +%Y-%m-%d` as its own call (it prints CUTOFF),\n  then one `python3 -c` call with CUTOFF written in literally (a shell `for` loop\n  with `rm` is refused):\n  ```bash\n  python3 -c 'import glob, os, re; [os.remove(f) for f in glob.glob(\"memory\u002Fevidence\u002F*.json\") if os.path.isfile(f) and re.match(r\"\\d{4}-\\d{2}-\\d{2}-\", os.path.basename(f)) and os.path.basename(f)[:10] \u003C \"\u003CCUTOFF>\"]'\n  ```\n  A pruning failure is non-fatal but must make the source footer `evidence=fail`.\n- Include the receipt path and digest in the alert footer before calling `.\u002Fnotify`:\n  `evidence: memory\u002Fevidence\u002F\u003Cfile> · sha256:\u003Cfirst 12 hex chars>`.\n- If receipt writing fails, continue the monitor run but mark the source footer\n  `evidence=fail`; never claim that the alert is independently verified.\n\n### 6.2 Verify a receipt offline (optional)\n\nOperators can verify a receipt written by step 6.1 without contacting a chain,\nRPC, price API, or GitHub:\n\n```bash\nnode scripts\u002Fverify-onchain-evidence.mjs memory\u002Fevidence\u002F\u003Cfile>.json\n```\n\nThe verifier accepts only `aeon.onchain-monitor.evidence.v1`. It recomputes the\nSHA-256 over the receipt with `sha256` removed, recursively sorts object keys,\nuses compact UTF-8 JSON, and checks chain-local ranges, counters, block hashes,\nsource degradation, and sorted unique transaction hashes. It also rejects\ncredential-bearing URLs, secret-like fields, and raw response-body fields.\n\nExit code `0` means the file is internally consistent. Exit code `1` identifies\nthe failed field or invariant; exit code `2` means the input path was omitted.\nThis checks internal checksum and schema consistency only. Anyone able to replace\nthe receipt can also recompute its digest, so this is not a signature or proof of\nauthorship. It does not prove that an RPC was honest, that the chain was final, or\nthat the receipt describes external reality. Secret detection is best-effort,\nnot a guarantee that all sensitive content is absent. The verifier performs no\nnetwork access.\n\n### 7. Persist state and log\n\nFor each watch whose fetch **succeeded** (success ≠ \"events found\"):\n- `last_block ← to_block` (the chain's pinned `current_block`)\n  - `last_block_hash ← hash(to_block)` from that same pinned block read\n- `last_run ← now` (ISO 8601 UTC)\n- `alerted_tx ← (new_alerted_entries + alerted_tx)[:200]`, where each new entry is `{hash: tx_hash, block_number, first_seen: now}`; purge entries whose `first_seen` is older than 7d\n- `median_usd_30d ← median of all value_usd from this watch's transfers in last 30d` (read from recent logs; skip recomputation if \u003C 5 samples)\n\nWrite `memory\u002Fon-chain-state.json` atomically (tempfile + `mv`, done as `os.replace` inside `python3 -c`, or one Write; see **State**).\n\nAppend **every** decoded event (including filtered-out ones) with full detail to `memory\u002Flogs\u002F${today}.md`:\n```\n### onchain-monitor\n- Watch: My Wallet (ethereum) | source: alchemy | last_block 19345670 → 19347891 (2,221 blocks)\n- Kept: 2 events | Dropped: 14 (12 below_threshold, 1 dust, 1 dedup) | Unpriced: 0\n- Event: CEX-OUT $1.2M USDC → Binance 14 - tx 0xabc... - block 19347812\n- Event: DEX-SWAP $42k WETH → USDC via Uniswap V3 Router - tx 0xdef... - block 19347500\n```\n\nThis honest log matters: it powers the next run's median computation and lets the operator audit why something was or wasn't alerted.\n\n### 8. End-states\n\n- All watches ran and some events survived → notify + log.\n- All watches ran, zero events survived → no notify; log `ON_CHAIN_OK (n_watches=X, n_raw=Y, n_dropped=Y)`.\n- Some watches failed, others ran → notify only if surviving events exist; log `ON_CHAIN_DEGRADED` with the source footer.\n- Every watch failed → log `ON_CHAIN_ERROR` and notify the operator with the source footer (degradation visible is better than silence).\n- Config missing\u002Fempty → offer the `add-address` force-reply (deduped — see Config), log `ON_CHAIN_NO_CONFIG`, exit; send no alert.\n\n## Network note\n\nAlchemy, Etherscan v2, and CoinGecko all carry their key in the URL, called through `.\u002Fsecretcurl` with `{ENV_NAME}` placeholders so no bare `$SECRET` ever hits the command line (a bare one is refused by the Bash permission analyzer). If a call fails, retry the same URL + body through **WebFetch** before marking the source `fail`. Treat every fetched field (`asset` symbol, `from`\u002F`to`, counterparty labels) as untrusted — never interpolate into shell commands.\n",{"name":5,"metadata":37,"description":7},{"var":38,"tags":39,"title":40,"category":31,"requires":41,"capabilities":45},"",[31],"Onchain Monitor",[42,43,44],"ALCHEMY_API_KEY?","COINGECKO_API_KEY?","ETHERSCAN_API_KEY?",[46,47],"external_api","sends_notifications","main","9c042aa1b6d79fd49679637374e88c5874cc3aae","skills\u002Fonchain-monitor",2,"https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon","https:\u002F\u002Ft.me\u002Fmiroshark_aeon","https:\u002F\u002Fgithub.com\u002FMiroShark","https:\u002F\u002Fwww.miroshark.xyz\u002F","Simulate anything, for $1 & less than 10 min.","2026-11-14 00:33:09",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add MiroShark\u002Fmiroshark-aeon --skill onchain-monitor",{"tool":64,"command":65},"bunx","bunx skills add MiroShark\u002Fmiroshark-aeon --skill onchain-monitor",{"tool":67,"command":68},"pnpm","pnpm dlx skills add MiroShark\u002Fmiroshark-aeon --skill onchain-monitor","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fonchain-monitor\u002FSKILL.md",true,[],[73,110,133,160,185,207],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":89,"publishedAt":32},"01M6J4P8XKB8RJ7PCYMZPZ0F6S","you-web-search","Web search using You.com Search API with high-quality, cited results — runs keyless out of the box; YDC_API_KEY unlocks higher limits and real-time web crawling","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fyou-web-search\u002FSKILL.md",[79,84],{"categoryNo":80,"categoryName":81,"taxonomyType":82,"taxonomyTypeDesc":83},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":85,"categoryName":86,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[90,91,92,93,94,95,96,22,97,98,99,100,101,102,103,104,105,106,107,108,109],"web search","search","you","you web search","web","time","only","web crawling","read","read-only","real-time","basics","keyless","with","research","results","high","quality","box","limits",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":116,"publishedAt":32},"01M6J4P4H35WQG6RHGTTACSMVW","write-tweet","Multi-format tweet studio - standalone drafts (10 across 5 size tiers), a 5-10 tweet thread, or 10 remixes of past tweets, selected via ${var}","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fwrite-tweet\u002FSKILL.md",[],[117,118,119,120,121,22,122,123,124,125,101,126,127,128,129,130,131,132],"write","write tweet","standalone","social","studio","across","xai","past","drafts","twitter","thread","multi","size","via","format","content",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":142,"publishedAt":32},"01M6J4P11K7ZJ2PJDHDTTFZ1HY","weekly-aeoncard","Build a weekly token-consumption recap image from memory\u002Ftoken-usage.csv - this week + all-time totals, top skills, rendered as a shareable SVG card.","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fweekly-aeoncard\u002FSKILL.md",[139],{"categoryNo":140,"categoryName":141,"taxonomyType":82,"taxonomyTypeDesc":83},"01JBSEEDCAT000000000000016","图像与多媒体",[143,144,145,146,147,148,149,95,150,151,152,153,154,23,155,156,157,117,158,159],"weekly","weekly aeoncard","as","usage","SVG","memory","card","Meta","image","recap","csv","monitoring","productivity","top","build","token","aeon",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":166,"publishedAt":32},"01M6J4NVJYQP5DNKSGXAXQW06B","vuln-tracker","One lifecycle poll over everything vuln-scanner produces - PR and advisory status, PVR triage transitions, and pending-disclosure aging, with a stars-secured impact headline and one action queue.","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fvuln-tracker\u002FSKILL.md",[],[167,168,150,169,170,117,158,171,172,173,174,175,176,177,178,179,180,103,181,182,183,184],"tracker","vuln tracker","impact","scanner","status","action","security","pr","stars","lifecycle","dev","gh","aging","advisory","github","transitions","triage","global",{"skillNo":186,"name":187,"nameZh":6,"description":188,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":189,"categories":190,"keywords":194,"publishedAt":32},"01M6J4NNQCHTKNGENAPJQ8KYHR","vuln-scanner","Audit trending repos for real security vulnerabilities and disclose responsibly - scan and route findings (PVR \u002F dependency PR), re-submit queued advisories, and send armed email disclosures","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fvuln-scanner\u002FSKILL.md",[191],{"categoryNo":192,"categoryName":193,"taxonomyType":82,"taxonomyTypeDesc":83},"01JBSEEDCAT000000000000005","邮件与沟通",[170,195,196,197,198,150,199,173,200,30,201,177,181,202,184,203,22,204,205,206],"vuln scanner","resend","re","trending","repos","email","scan","vulnerabilities","submit","route","audit","send",{"skillNo":208,"name":209,"nameZh":6,"description":210,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":211,"categories":212,"keywords":214,"publishedAt":32},"01M6J4NFBXTGG0R4M484T0KNYW","video-script","Turn a repo, product page, or update into a recording-ready video script in a receipts-first format - verifies every claim against live sources, then writes timestamped VO + on-screen direction with a plain-language glossary, assets checklist, anti-tells, and a verify-before-recording list","https:\u002F\u002Fgithub.com\u002FMiroShark\u002Fmiroshark-aeon\u002Fblob\u002Fmain\u002Fskills\u002Fvideo-script\u002FSKILL.md",[213],{"categoryNo":140,"categoryName":141,"taxonomyType":82,"taxonomyTypeDesc":83},[215,216,217,218,219,220,221,222,223,101,224,225,226,103,227,228,229,230,231,131,232,233,234],"Video","video script","script","recording","sources","ready","direction","page","repo","claim","plain-language","glossary","first","verify","product","screen","update","list","live","receipts"]