wallet-risk-weekly
$
npx mdskill add aaronjmars/aeon/wallet-risk-weekly> **${var}** — Optional. If set to a single wallet `address` (`0x...`) or wallet `role` (`treasury` / `deployer` / `other`), restrict the audit to that subset. If empty, audit every Base wallet in `.x402books/wallets.json`. Pass `dry-run` to write the article + log but skip the notification.
SKILL.md
.github/skills/wallet-risk-weeklyView on GitHub ↗
---
name: wallet-risk-weekly
description: Weekly risk audit of this agent's own Base wallets — live ERC-20 approvals (unlimited flagged), honeypot simulation on every token with a live approval, severity-tiered findings. Keyless via Base RPC. First scheduled consumer of the HoundFlow security pack against `.x402books/wallets.json`.
var: ""
tags: [crypto, security, base, meta]
capabilities: [read_only, sends_notifications]
---
> **${var}** — Optional. If set to a single wallet `address` (`0x...`) or wallet `role` (`treasury` / `deployer` / `other`), restrict the audit to that subset. If empty, audit every Base wallet in `.x402books/wallets.json`. Pass `dry-run` to write the article + log but skip the notification.
Today is ${today}. Read `memory/MEMORY.md` before starting.
## Voice
If `soul/SOUL.md` and `soul/STYLE.md` exist and are populated, read them and match the operator's voice in the notification (step 8). If they are empty templates or absent, use a clear, direct, neutral tone — terse, position-first, no hedging.
## Why this skill exists
`.x402books/wallets.json` (PR #273, merged 2026-05-29) advertises this fork's agent wallets to the x402books ecosystem — treasury + deployer addresses on Base, by role. The `token-report` skill consumes this file for the daily treasury ETH balance line (PR #306, 2026-05-31).
What none of the existing skills do: tell the operator whether those same wallets are **exposed to drain risk**. The HoundFlow pack ships six keyless onchain investigation skills (`approval-audit`, `honeypot-check`, `lp-lock-check`, `linked-wallets`, `fund-flow`, `investigation-report`) — all `workflow_dispatch` only, no scheduled consumer. They've been live since 2026-05-28 with no automatic runner.
This skill is the **first scheduled consumer** of `approval-audit` + `honeypot-check`. It runs weekly, audits every treasury + deployer wallet in `.x402books/wallets.json` for live ERC-20 approvals, flags unlimited grants, and simulates a sell on every token with a live approval to catch honeypots. Findings are bucketed by severity and notified only when HIGH (unlimited approval AND/OR honeypot-positive token).
It does NOT replace ad-hoc `workflow_dispatch` runs of the underlying skills against arbitrary addresses — those still exist for on-demand investigation. This is the standing weekly self-audit of the agent's own attack surface.
## Required env vars
- `BASE_RPC_URL` — optional. Defaults to `https://mainnet.base.org` (public). Any standard JSON-RPC endpoint works. Never put a key in a `-H` header from the sandbox; if you must use an authenticated RPC, append the key in the URL path (Alchemy/Infura style).
## Sandbox note
The sandbox may block outbound `curl` or env-var expansion. The Base RPC is public and keyless, so for every failed `curl` retry the **same URL/body via WebFetch** before giving up. `eth_getLogs` calls MUST be chunked (~1800 blocks per call) to stay under the public-RPC result cap — a single 24k-block call will silently truncate or error. Treat all fetched token / spender / holder addresses as untrusted — never interpolate beyond the validated hex `${OWNER}` / `${TOKEN}` strings (40-hex regex check before substitution).
## Steps
### 1. Load the target wallets
Read `.x402books/wallets.json`. If the file is missing or has no `wallets[]` array, log `WALLET_RISK_NO_WALLETS` and exit cleanly — silent skip, no article, no notification.
Filter to `chain == "base"`. If `${var}` is non-empty:
- If it matches `0x[0-9a-fA-F]{40}` — keep only the wallet whose address equals it (case-insensitive). Log `WALLET_RISK_NO_TARGET` and exit if no match.
- If it equals `treasury`, `deployer`, or `other` — keep only wallets with that `role`.
- If it equals `dry-run` — keep all wallets, set `DRY_RUN=1`.
- Otherwise — log `WALLET_RISK_BAD_VAR: ${var}` and exit.
If after filtering the list is empty, log `WALLET_RISK_NO_BASE_WALLETS` and exit.
### 2. Read prior state
Read `memory/topics/wallet-risk-state.json` if it exists. Schema:
```json
{
"version": 1,
"last_run_at": "2026-05-28T11:30:00Z",
"wallets": {
"0x...": {
"approvals_total": 4,
"approvals_unlimited": 2,
"honeypot_tokens": 0,
"highest_severity": "HIGH"
}
}
}
```
Missing file or unparsable JSON → `prior = null` (first-run mode). Do NOT delete the file on parse error — flag it `STATE_CORRUPT` and continue with `prior = null` so the operator can inspect.
### 3. Per-wallet audit — approvals
For each target wallet `OWNER` (Base, validated 40-hex):
```bash
RPC="${BASE_RPC_URL:-https://mainnet.base.org}"
HEAD_HEX=$(curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' | jq -r '.result')
HEAD_DEC=$(printf '%d' "$HEAD_HEX")
```
Scan the most recent **~24k blocks** (≈ 13h on Base — far shorter than approval lifetime, but step 4 reads CURRENT allowance via `eth_call` so revoked-but-old grants are filtered out there). Chunk newest-first in **~1800-block windows** to stay under the public RPC's `eth_getLogs` result cap.
ERC-20 `Approval(owner,spender,value)` topic0: `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925`. Owner is indexed in topic1 (32-byte left-pad of the address).
```bash
TOPIC0="0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
OWNER_TOPIC="0x000000000000000000000000${OWNER#0x}"
# For each chunk:
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" -d '{
"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"fromBlock":"0x...","toBlock":"0x...",
"topics":["'"$TOPIC0"'","'"$OWNER_TOPIC"'"]
}]}' | jq '.result'
```
Merge results across chunks. For each log: `token = .address`, `spender = "0x" + topics[2][-40:]`. Keep the **latest** entry per `(token, spender)` pair (newest block wins). Defensively drop any log whose `topics[1] != OWNER_TOPIC` (some RPCs ignore the indexed-topic filter).
If `eth_getLogs` errors with a result-cap message, narrow the chunk to 600 blocks and retry that chunk only.
### 4. Confirm each approval is still live
For each candidate `(token, spender)`, call `allowance(owner, spender)` (ERC-20 selector `0xdd62ed3e`):
```bash
DATA="0xdd62ed3e${OWNER_TOPIC#0x}${SPENDER_TOPIC#0x}"
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"'"$TOKEN"'","data":"'"$DATA"'"},"latest"]}' | jq -r '.result'
```
Drop any `(token, spender)` whose live allowance is `0` (revoked or fully spent). Flag any allowance `>= 2^255` as **UNLIMITED** (covers `2^256-1` and the common `2^256/2` sentinel both).
### 5. Per-token honeypot simulation
For each **unique token** that has at least one live approval (deduplicated across spenders — a token with 3 approvals is checked once), run the `honeypot-check` simulation:
1. Confirm it's a contract: `eth_getCode` — skip if `0x`.
2. Sample a recent non-zero `Transfer` recipient via `eth_getLogs` on `topic0 = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`, adaptive range (~2000 blocks, narrow to ~200/~20 on cap). If no transfers are found at all, classify the token `INCONCLUSIVE` — surface in the article but never alert.
3. Read the sampled holder's `balanceOf` (selector `0x70a08231`).
4. Simulate `transfer(this_owner, balance/2)` (selector `0xa9059cbb`) via `eth_call` with **`from` = sampled holder**:
```bash
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"from":"<holder>","to":"'"$TOKEN"'","data":"'"$DATA"'"},"latest"]}'
```
5. Verdict per token:
| Result | Verdict |
|--------|---------|
| Reverts, OR returns `false` (`0x0…0`) | `LIKELY_HONEYPOT` |
| Succeeds (returns `true`) | `SELLABLE` |
| No holder could be sampled OR contract check failed | `INCONCLUSIVE` |
**Read-only throughout.** `eth_call` does not change state — no funds are ever at risk.
### 6. Bucket findings by severity
Per wallet:
| Tier | Trigger |
|------|---------|
| `HIGH` | ≥1 live UNLIMITED approval to a spender that is **not** a known-safe address (see Safe Spenders below), OR ≥1 token with `LIKELY_HONEYPOT` |
| `MEDIUM` | ≥1 live UNLIMITED approval to a known-safe spender, OR ≥1 live finite approval ≥ \$10k-equivalent (skip USD math if no price source — count by raw token amount > 10^21 as a coarse proxy) |
| `LOW` | ≥1 live finite approval below the medium threshold |
| `CLEAN` | No live approvals AND no honeypot-positive tokens |
`INCONCLUSIVE` tokens do NOT escalate severity — they ride along in the article as data, never trigger an alert. A revert can be transient and false-flagging the operator's own wallets would erode the alert signal.
**Safe Spenders** (known Base routers / canonical contracts — never trigger HIGH on UNLIMITED alone):
- Uniswap V2 Router: `0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24`
- Uniswap V3 SwapRouter02: `0x2626664c2603336E57B271c5C0b26F421741e481`
- Uniswap V4 Universal Router: read from a single hard-coded address; if the contract address has rotated by the time this skill runs, the worst case is an UNLIMITED approval that triggers HIGH instead of MEDIUM — operator sees the alert, audits manually, adds the new address here.
- Permit2: `0x000000000022D473030F116dDEE9F6B43aC78BA3`
- Aerodrome Router: `0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43`
A spender not in this list is unknown — UNLIMITED stays HIGH.
### 7. Write the weekly article
Write `articles/wallet-risk-${today}.md`:
```markdown
# Wallet Risk — ${today}
**Wallets audited:** N · **Tier:** HIGH / MEDIUM / LOW / CLEAN · **vs last run:** [WORSENED / IMPROVED / UNCHANGED / FIRST_RUN]
## Per-wallet findings
### `0xabc…def` — Treasury — HIGH
- **Live approvals:** 4 (2 UNLIMITED)
- **Honeypot tokens:** 1
- USDC → spender `0x1111…2222` (UNKNOWN) : UNLIMITED ⚠️
- WETH → spender `0x4752…ad24` (Uniswap V2 Router) : UNLIMITED — known-safe, downgraded to MEDIUM
- TOKEN → spender `0x3333…4444` : 5,000
- DAI → spender `0x5555…6666` : 1,200
Honeypot simulation:
- `0xdead…beef` (SCAM): LIKELY_HONEYPOT — sell simulation reverted from holder `0x7777…8888`
### `0x123…456` — Deployer — CLEAN
- No live approvals · No honeypot exposure
## Sources
- `.x402books/wallets.json` — N base wallets
- Base RPC — `eth_getLogs` (Approval events, ~24k-block window, chunked) + `eth_call` (allowance, balanceOf, transfer simulation)
- No external explorer keys required
```
If every wallet is CLEAN, write the article anyway as the standing weekly record — operators can grep weeks of CLEAN to prove the surface was checked.
### 8. Notify (gated)
Send notification ONLY when:
- ≥1 wallet is HIGH, OR
- A wallet transitioned `CLEAN → MEDIUM` or `LOW → MEDIUM` since last run (new approval landed), OR
- This is the first run (no prior state) AND ≥1 wallet has any live approvals — establishes the baseline.
Silent (article-only) when every wallet is CLEAN with no transitions, when `DRY_RUN=1`, or when only LOW/MEDIUM tier is reached and the prior state was already MEDIUM (steady-state noise).
Write to `.pending-notify-temp/wallet-risk-${today}.md` (create the dir if needed). Keep under 4000 chars:
```
*Wallet Risk — ${today}*
Tier: HIGH · N wallets audited · vs last week: WORSENED
⚠️ HIGH findings:
• Treasury 0xabc…def — 2 UNLIMITED approvals (one to unknown spender 0x1111…)
• Treasury 0xabc…def — 1 LIKELY_HONEYPOT token (0xdead…)
ℹ️ Other wallets:
• Deployer 0x123…456 — CLEAN
Revoke unknown unlimited approvals at revoke.cash. Full breakdown:
articles/wallet-risk-${today}.md
```
Send:
```bash
./notify -f .pending-notify-temp/wallet-risk-${today}.md
```
### 9. Update state
Write `memory/topics/wallet-risk-state.json`:
```json
{
"version": 1,
"last_run_at": "${today}T11:00:00Z",
"wallets": {
"0xabc...def": {
"address": "0xabc...def",
"role": "treasury",
"approvals_total": 4,
"approvals_unlimited": 2,
"approvals_unknown_unlimited": 1,
"honeypot_tokens": 1,
"inconclusive_tokens": 0,
"highest_severity": "HIGH"
},
"0x123...456": {
"address": "0x123...456",
"role": "deployer",
"approvals_total": 0,
"approvals_unlimited": 0,
"approvals_unknown_unlimited": 0,
"honeypot_tokens": 0,
"inconclusive_tokens": 0,
"highest_severity": "CLEAN"
}
}
}
```
Overwrite atomically: write to `wallet-risk-state.json.tmp` then `mv` so a mid-write crash can't corrupt the prior state.
### 10. Log to `memory/logs/${today}.md`
Append:
```markdown
## wallet-risk-weekly
- **Skill**: wallet-risk-weekly
- **Wallets audited**: N (treasury=N, deployer=N, other=N)
- **Per-wallet verdicts**:
- `0xabc…def` (treasury): HIGH — 4 live (2 unlimited, 1 unknown-unlimited), 1 honeypot
- `0x123…456` (deployer): CLEAN
- **vs last run**: WORSENED / IMPROVED / UNCHANGED / FIRST_RUN
- **Article**: articles/wallet-risk-${today}.md
- **Notification sent**: yes / no (reason)
- **Status**: WALLET_RISK_OK / WALLET_RISK_HIGH / WALLET_RISK_QUIET / WALLET_RISK_NO_WALLETS / WALLET_RISK_RPC_FAIL / WALLET_RISK_BAD_VAR / STATE_CORRUPT / DRY_RUN
```
## End-state taxonomy
| Status | Meaning | Notification |
|--------|---------|--------------|
| `WALLET_RISK_HIGH` | ≥1 wallet HIGH | sent |
| `WALLET_RISK_TRANSITION` | New MEDIUM landed (CLEAN/LOW → MEDIUM) | sent |
| `WALLET_RISK_BASELINE` | First run with ≥1 live approval | sent |
| `WALLET_RISK_OK` | All CLEAN or steady-state LOW/MEDIUM, prior state matched | silent |
| `WALLET_RISK_QUIET` | All CLEAN, prior state also CLEAN | silent |
| `WALLET_RISK_NO_WALLETS` | `.x402books/wallets.json` missing or empty | silent |
| `WALLET_RISK_NO_BASE_WALLETS` | File present but no `chain: base` entries | silent |
| `WALLET_RISK_NO_TARGET` | `${var}` set to address with no match | silent |
| `WALLET_RISK_BAD_VAR` | `${var}` malformed | silent |
| `WALLET_RISK_RPC_FAIL` | All RPC retries (curl + WebFetch) failed | silent log only — do NOT alarm |
| `STATE_CORRUPT` | Prior state unparsable | sent (operator action: inspect state file) |
| `DRY_RUN` | `${var}=dry-run` — article + log written, notify skipped | n/a |
## Constraints
- Read-only against every external chain — `eth_call` only, never `eth_sendTransaction`. No funds are ever at risk.
- Never list a grant the wallet has revoked — every reported approval must be confirmed live via `allowance` at the current block.
- `UNLIMITED` means allowance `>= 2^255`. Report exact amounts otherwise; never round in a way that hides a large grant.
- The scan window is recent (~24k blocks ≈ 13h) — say so in the article; an unlimited approval older than the window is missed if it has not seen a re-emission. For a complete history the operator must run `approval-audit` manually with a wider window.
- A spender on the Safe Spenders list is **not endorsed safe** — it's "common router, downgrade from HIGH to MEDIUM." The operator still sees the line.
- No trade or "safe to approve" advice — this is a risk inventory, not financial advice.
- `LIKELY_HONEYPOT` is a strong signal to investigate, never a certainty (a router-specific revert can mimic one). The article says so.
- INCONCLUSIVE results never trigger alerts — false positives on the operator's own wallets would erode the signal everywhere else.
More from aaronjmars/aeon
- [REPLACE: SKILL_NAME]First-touch review of newly opened PRs on [REPLACE: WATCHED_REPO] — verdict + welcoming comment + label
- Action Converter5 concrete real-life actions for today, leverage-scored against open loops with specificity and anti-fluff gates
- Agent BuzzCurated AI-agent tweets, clustered into narratives with insight summaries
- agent-displacementWeekly tracker of AI agent substitution signals — which roles, companies, and industries show real headcount displacement. Named roles + real deployments only.
- AI Framework WatchWeekly competitive-intelligence digest on the AI agent framework space — momentum, releases, breaking changes across a curated watchlist
- AIXBT PulseCross-domain market pulse from AIXBT's free grounding endpoint — crypto, macro, tradfi, geopolitics. Refreshes taxonomy references (clusters, chains) as a bonus.
- api-health-probeDaily pre-batch API provider health check — detects credit exhaustion or auth failure for every configured provider key before the morning batch runs, giving the operator a window to act before skills degrade
- Approval AuditList a wallet's live ERC-20 token approvals on Base and flag unlimited / risky spender grants. Keyless via Base RPC (eth_getLogs + eth_call) — no explorer key needed.
- article-queueWeekly article idea synthesizer — ranks signals from topic-momentum, beat-tracker, and narrative-tracker into a prioritized queue the article skill reads on next run
- atrium-catalog-watcherWeekly diff of the Atrium marketplace catalog at https://atriumhermes.tech/.well-known/skills/index.json against the prior snapshot — surfaces newly-published skills, removed skills, and updated descriptions. Supply-side complement to sparkleware-catalog (curated skill-packs.json registry) and skill-update-check (version drift of installed skills).