investigation-report

SkillCloud & infra

Lets your agent run a security check on any Base token, covering rug risk, contract audit, holders, honeypot and LP lock.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the investigation-report skill

About this capability

One-shot Base-token investigation - runs any subset of six onchain-security checks (rug-scan, contract-audit, deployer-trace, holder-concentration, honeypot, lp-lock) into one verdict. Keyless core.

What this skill tells your AI

The instructions your AI receives, as published by aeonfun/aeon in skills/investigation-report/SKILL.md and read by ahel’s review.

${var} — Base subject to investigate, plus optional flags: <token-address> [--checks=rug,contract,deployer,holders,honeypot,lp] [--depth=quick|deep]. The first token is the subject contract address (0x…, required). --checks= is a comma-list selecting which analyzers to run (default = all six). --depth= is quick (the old rug-scan fast path — minimal reads) or deep (full standalone logic of each selected check; default). If the subject address is empty, log REPORT_NO_TARGET and exit cleanly (no notify).

Examples:

  • 0xToken → all six checks, deep report.
  • 0xToken --checks=honeypot → only the honeypot simulation (reproduces the standalone honeypot-check exactly, incl. its HONEYPOT_* end-states).
  • 0xToken --checks=rug,lp --depth=quick → rug verdict + LP-lock, fast path.
  • 0xToken --checks=contract,deployer,holders --depth=deep → structural audit + deployer entity intel + full concentration.

The "tell me everything about this token" skill. Instead of running six checks by hand, this composes them into one structured report behind a selector: rug risk, contract audit (verification / owner powers / proxy), deployer trace (who shipped it and their history), holder concentration (whale risk), honeypot (can you actually sell?), and LP lock (can the team pull liquidity?) — with a one-line summary on top.

Designed to degrade gracefully: each selected section runs independently, so a section that needs a key (or returns nothing) is marked unavailable without aborting the rest. Selecting a single check makes the composite behave as that one analyzer — same steps, same thresholds, same notify format, same status codes.

Config

  • Subject = the first token of ${var} (validate: 0x + 40 hex). Chain = Base (chainid=8453, explorer basescan.org).
  • Etherscan v2 unified API (https://api.etherscan.io/v2/api?chainid=8453&…) — used by the rug, contract, deployer, holders checks. Works keyless at a lower rate limit.
  • Base RPC (${BASE_RPC_URL:-https://mainnet.base.org}) — used by honeypot, lp, and the eth_call/eth_getLogs/eth_getStorageAt/eth_getCode reads inside the other checks. Keyless; any standard JSON-RPC endpoint works.
  • Secrets (all optional):
    • ETHERSCAN_API_KEY (a.k.a. BASESCAN_API_KEY — same Etherscan v2 key) — appended to the Etherscan URL as &apikey=… via ./secretcurl's {ETHERSCAN_API_KEY} placeholder (never a bare $SECRET on the line, never a header). Raises the rate limit and unlocks verified source, full deployer history, and the holder list. Used by rug, contract, deployer, holders.
    • BASE_RPC_URL — overrides the default public Base RPC. Used by every RPC read; primary for honeypot and lp.
  • Preamble (run once, before dispatch): read memory/MEMORY.md and the last ~2–3 days of memory/logs/ so a repeat investigation can note what changed since last time and avoid re-reporting the same signal. Parse ${var} → subject address, --checks (default all six), --depth (default deep).

Steps

Dispatch to each selected check below (default: all six). Each is self-contained — collect its verdict/section; never let one check's failure stop the others. --depth=quick runs the lightweight path noted in each branch (rug-scan-style inline sampling, fewer calls); --depth=deep runs the full standalone logic.

Check rug — Rug Scan

A fast, opinionated rug verdict: does the contract let someone print, freeze, or drain — and is supply/liquidity concentrated enough to pull?

1. Verify contract + pull source

TOKEN="${var}"
# ./secretcurl substitutes {ETHERSCAN_API_KEY} internally, so no `$SECRET` hits the
# command line (a bare one is refused by the Bash permission analyzer). Append the key
# only when set — Etherscan v2 works keyless at a lower rate limit.
KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=contract&action=getsourcecode&address=${TOKEN}${KEYQ}" | jq '.result[0]'

Capture ContractName, Proxy, Implementation, SourceCode. Empty SourceCode = unverified → strong risk signal.

2. Scan source for dangerous powers — grep the returned source (case-insensitive) for these signals and record which fire:

SignalPatternsWeight
Unverified sourceempty SourceCode+3
Mint authorityfunction mint, _mint( callable by owner+2
Blacklist / freezeblacklist, isBlocked, _freeze, addBan+2
Pausable transferswhenNotPaused, function pause+1
Mutable fees/taxsetFee, setTax, updateTaxes+2
Owner not renouncedowner != 0x0 (see step 3)+1
Proxy / upgradeableProxy == "1" or delegatecall + upgrade fn+2
Trading toggleenableTrading, tradingActive, setSwapEnabled+1

3. Check ownership state — call owner() (selector 0x8da5cb5b) via eth_call:

curl -m 10 -s -X POST "${BASE_RPC_URL:-https://mainnet.base.org}" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"'"$TOKEN"'","data":"0x8da5cb5b"},"latest"],"id":1}' | jq -r '.result'

Trailing 40 hex chars = the owner address. All-zero → ownership renounced (lowers risk). A live EOA/multisig → flag the step-2 powers as currently exercisable.

4. Holder concentration (quick read)

KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=token&action=tokenholderlist&contractaddress=${TOKEN}&page=1&offset=10${KEYQ}" | jq '.result'

Compute top-1 and top-10 share of supply. Flag +2 if top-1 > 30% (excluding known LP/lock/burn addresses), +1 if top-10 > 70%. If this endpoint returns empty on the keyless tier, note holders=unavailable and skip this signal rather than failing. Depth: on --depth=deep when the holders check is also selected, take the top-1/top-10 EOA share from that check's full result instead of this 10-row sample.

5. LP / liquidity check — identify the token's main pool (Aerodrome / Uniswap V3 on Base). If LP tokens sit in a known locker or burn address (0x000…dead, Unicrypt, Team Finance) → liquidity locked (lowers risk). If LP is held by the deployer EOA → +2 (pull risk). Depth: on --depth=deep when the lp check is also selected, use that check's LOCKED/PARTIAL/UNLOCKED verdict here.

6. Score + verdict — sum the weights:

ScoreVerdict
0–2LOW
3–5ELEVATED
6–8HIGH
9+CRITICAL

The verdict must come from this table — no freelance labels. Section end-states: RUG_SCAN_OK (LOW), RUG_SCAN_FLAGGED (≥ELEVATED), RUG_SCAN_ERROR (all fetches failed).

Check contract — Contract Audit

Deep structural inspection: what powers exist, who holds them, and whether they're still exercisable.

1. Source + verification

ADDR="${var}"
KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=contract&action=getsourcecode&address=${ADDR}${KEYQ}" | jq '.result[0] | {ContractName, Proxy, Implementation, CompilerVersion, verified: (.SourceCode != "")}'

If unverified, say so plainly: no static analysis is possible and audit confidence is low. Continue with the onchain checks below.

2. Proxy / upgradeability — if Proxy == "1" or the source contains delegatecall, read the EIP-1967 implementation slot:

curl -m 10 -s -X POST "${BASE_RPC_URL:-https://mainnet.base.org}" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["'"$ADDR"'","0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","latest"],"id":1}' | jq -r '.result'

A non-zero slot = upgradeable (Transparent/UUPS). Upgradeable means post-deploy logic can change — flag who controls the upgrade (admin/owner from step 3).

3. Ownership & admin roles — probe common accessors via eth_call and record any that return a non-zero address:

FunctionSelector
owner()0x8da5cb5b
admin()0xf851a440
paused()0x5c975abb
curl -m 10 -s -X POST "${BASE_RPC_URL:-https://mainnet.base.org}" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"'"$ADDR"'","data":"0x8da5cb5b"},"latest"],"id":1}' | jq -r '.result'

Check whether the owner address itself has code (multisig/contract) vs is an EOA via eth_getCode.

4. Dangerous function surface — from verified source, enumerate externally-callable, owner-gated functions and classify:

  • Supply: mint, burnFrom
  • Access: blacklist, setFreeze, pause/unpause
  • Economics: setFee, setTax, setMaxTx, setLimits
  • Control: transferOwnership, upgradeTo, setImplementation
  • Drain: arbitrary call/delegatecall reachable by admin, withdraw/rescueTokens that can move user funds

Depth: --depth=quick may stop after the onchain reads (steps 1–3) and report the capability matrix from those; --depth=deep runs the full step-4 source-based surface enumeration. Report a power as a risk only if it's live AND not renounced. Section end-states: AUDIT_OK, AUDIT_FLAGGED (a live, non-renounced power in {upgrade, mint, blacklist, drain}), AUDIT_UNVERIFIED, AUDIT_ERROR.

Check deployer — Deployer Trace

"What else did this person ship, and how did those end?" — entity intel for spotting serial ruggers.

1. Resolve deployer — the subject is a token, so resolve its creator first:

TARGET="${var}"
KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=contract&action=getcontractcreation&contractaddresses=${TARGET}${KEYQ}" | jq -r '.result[0].contractCreator'

Use contractCreator as the deployer for the rest of this check; if the subject is already an EOA, use it directly.

2. Enumerate deployments — pull the deployer's tx list, keep only contract-creation txns (empty to, or a receipt contractAddress):

DEPLOYER="<contractCreator from step 1>"
KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=account&action=txlist&address=${DEPLOYER}&startblock=0&endblock=99999999&sort=asc${KEYQ}" | jq '[.result[] | select(.to == "")]'

For each creation record: contract address, creation date, and cheap current state (has code? verified?).

3. Pattern linkage — group deployments that share signals (same bytecode, same token-name template, identical owner, sequential deploys minutes apart). Repeated identical templates from one deployer is a strong serial-launcher signal.

4. Outcome per contract — for each deployed token, a fate check (reuse rug logic lightly): liquidity pulled? ownership renounced? holders → near-zero? Classify each ALIVE, ABANDONED, or RUGGED (LP removed AND price → 0; never infer RUGGED from a low balance alone).

Depth: --depth=quick resolves the creator + enumerates deployments + reports the count and any obvious template reuse (skip per-contract fate); --depth=deep runs the full step-3 linkage and step-4 per-contract fate classification. If the deployer has only 1 deployment, report it plainly — one contract is not a serial pattern. Section end-states: DEPLOYER_TRACE_OK, DEPLOYER_TRACE_FLAGGED (≥2 deployments classify as RUGGED), DEPLOYER_TRACE_ERROR.

Check holders — Holder Concentration

How concentrated is real circulating supply, once you strip out LP, lockers, and burn.

1. Fetch supply + top holders

TOKEN="${var}"
KEYQ=""; [ -n "${ETHERSCAN_API_KEY:+x}" ] && KEYQ="&apikey={ETHERSCAN_API_KEY}"
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=stats&action=tokensupply&contractaddress=${TOKEN}${KEYQ}" | jq -r '.result'
./secretcurl -m 10 -s "https://api.etherscan.io/v2/api?chainid=8453&module=token&action=tokenholderlist&contractaddress=${TOKEN}&page=1&offset=100${KEYQ}" | jq '.result'

If tokenholderlist returns empty on the keyless tier, reconstruct top holders from Transfer logs via Base RPC eth_getLogs and note reduced confidence.

2. Classify & exclude non-circulating holders — tag each top holder before computing concentration (these are NOT free float):

TagMarker
LPknown DEX pool (Aerodrome / Uniswap pair)
LOCKUnicrypt / Team Finance / known locker
BURN0x000…000 or 0x…dead
CONTRACThas code (staking, vesting, treasury)
EOAplain wallet — the holders that drive concentration

3. Compute metrics over circulating supply (total − burn):

  • Top-1, top-5, top-10, top-50 % share (report EOA-only and raw).
  • HHI (sum of squared % shares) → 0–10000; >2500 = concentrated.
  • Number of holders to reach 50% of supply.

4. Whale-cluster check — flag groups of top EOAs that share a funding source or transact among themselves (cheap heuristic: same first-funder, or one inbound hop apart). Clustered whales effectively act as one holder.

5. Verdict

SignalVerdict
top-1 EOA >30% or HHI >2500CONCENTRATED
top-10 EOA >70%CONCENTRATED
LP unlocked + top-1 >20%FRAGILE
broad distribution, HHI <1000HEALTHY

Depth: --depth=quick fetches the top-10 sample and reports top-1/top-10 EOA share (the rug-scan-style read) without full HHI/cluster analysis; --depth=deep runs the full top-100 fetch, exclusion tagging, HHI, and whale-cluster steps. Always label LP/lock/burn before computing concentration. If holder data can only be RPC-reconstructed, say so and lower confidence. Section end-states: HOLDER_CONC_OK, HOLDER_CONC_FLAGGED (CONCENTRATED or FRAGILE), HOLDER_CONC_ERROR.

Check honeypot — Honeypot Check

"Can I actually sell this token, or is it a trap?" Simulates a sell with eth_call — no funds, no transaction. Runs keyless on the Base RPC.

1. Confirm it's a contract

TOKEN="${var}"
RPC="${BASE_RPC_URL:-https://mainnet.base.org}"
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["'"$TOKEN"'","latest"]}' | jq -r '.result'

If the result is 0x, it's not a contract — this section is HONEYPOT_NO_TARGET; report that and skip the rest of this check.

2. Sample a real holder — fetch recent Transfer events (topic0 0xddf252ad…) and take a recent non-zero to address (they hold a balance to simulate selling). Use an adaptive range (try ~2000 blocks, then narrow to ~200/~20 if the RPC's result cap is hit on a high-volume token):

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":"latest","address":"'"$TOKEN"'",
    "topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
  }]}' | jq -r '.result[-1].topics[2]'    # -> recent recipient (holder)

If no transfers are found at all, the token is inactive — section is HONEYPOT_INCONCLUSIVE; report that plainly.

3. Read the holder's balancebalanceOf(holder) (selector 0x70a08231), then plan to transfer half of it:

DATA="0x70a08231<holder padded to 32 bytes>"
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'

4. Simulate the selleth_call transfer(recipient, amount) (selector 0xa9059cbb) with from = the sampled holder. Because eth_call doesn't change state, this is a safe dry-run of whether the holder could move the token:

DATA="0xa9059cbb<recipient 32B><amount 32B>"
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

Result of the simulated transferVerdict
Reverts, or returns false (0x0…0)LIKELY_HONEYPOT
Succeeds (returns true)SELLABLE
No holder could be sampledINCONCLUSIVE

Depth: --depth=quick samples one holder and runs one simulation; --depth=deep retries with several sampled holders / narrower block ranges to reduce a false LIKELY_HONEYPOT from a transient/router-specific revert. A SELLABLE verdict does NOT mean the sell tax is low — recommend checking the tax separately. eth_call only — never send a transaction. Section end-states: HONEYPOT_OK (sellable), HONEYPOT_FLAGGED (LIKELY_HONEYPOT), HONEYPOT_INCONCLUSIVE, HONEYPOT_ERROR.

Check lp — LP Lock

"Can the team pull the liquidity?" Resolves the token's main pool and classifies LP custody. Runs keyless on the Base RPC.

1. Locate the main pool — fetch recent Transfer events (topic0 0xddf252ad…); the address that appears most as a counterparty is the dominant venue. Confirm a candidate is a real pair (not a router) by calling token0() (0x0dfe1681) / token1() (0xd21220a7) — a pool returns two addresses, one of which is ${var}:

TOKEN="${var}"; RPC="${BASE_RPC_URL:-https://mainnet.base.org}"
# (1) eth_getLogs Transfer for $TOKEN, tally counterparties
# (2) for the busiest, eth_call token0()/token1() and keep the one whose pair includes $TOKEN
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"<candidate>","data":"0x0dfe1681"},"latest"]}' | jq -r '.result'

Use an adaptive block range (try ~3000, then ~400/~40) so high-volume tokens don't overflow the public-RPC result cap.

2. V2 vs V3 — only V2 LP is lockable this way — call totalSupply() (0x18160ddd) on the pool:

  • Readable, non-zero → a V2-style AMM pair: the pool address is a fungible LP token whose custody we can inspect. Continue to step 3.
  • Reverts / zero → a V3 / V4 concentrated-liquidity pool: liquidity is held as NFT positions, not a fungible LP token. Prefer a V2 pair if one exists among the candidates; otherwise report LPLOCK_UNKNOWN and explain the lock must be checked at the position manager / locker directly.

3. Measure locked supply (V2) — for each burn / known-locker address, read its LP balance via balanceOf (0x70a08231) on the pool and divide by totalSupply:

AddressMeaning
0x…dEaD, 0x0burned (permanent)
Unicrypt 0x71b5…7641, Team.Finance 0xe2fe…35fbtime-locked
curl -m 10 -s -X POST "$RPC" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"<pool>","data":"0x70a08231<addr 32B>"},"latest"]}' | jq -r '.result'

4. Verdict

Locked share of LP supplyVerdict
≥ 90% burned/lockedLOCKED
50–90%PARTIAL
< 50%UNLOCKED (rug risk)
V3/V4 or no fungible LPUNKNOWN
No pool foundINCONCLUSIVE

Depth: --depth=quick resolves the single busiest V2 pool and classifies it; --depth=deep checks multiple candidate pools across venues and prefers a V2 pair over V3 when both exist. Only V2-style pools can be classified by LP custody here; V3/V4 return UNKNOWN — say so plainly. The locker list is not exhaustive — report the pool address so custody can be verified manually. Section end-states: LPLOCK_OK (locked), LPLOCK_FLAGGED (UNLOCKED/PARTIAL), LPLOCK_UNKNOWN (V3/non-fungible), LPLOCK_INCONCLUSIVE, LPLOCK_ERROR.

Compose

If exactly one check was selected, output is that check's standalone result — its section body, its native notify format, and its native end-state (e.g. --checks=honeypot produces the honeypot report and emits HONEYPOT_OK/HONEYPOT_FLAGGED/HONEYPOT_INCONCLUSIVE). Skip the aggregate wrapper.

If two or more checks ran, merge the selected sections into one document with an at-a-glance header:

# Investigation Report — 0xToken (Base)   ·   depth: deep · checks: rug,contract,deployer,holders,honeypot,lp

**At a glance:** Rug risk ELEVATED · Source verified · Owner NOT renounced · Deployer 9/14 rugged · Top holder 42% · Honeypot SELLABLE · LP UNLOCKED ⚠️

## 1. Rug Scan
...
## 2. Contract Audit
...
## 3. Deployer Trace
...
## 4. Holder Concentration
...
## 5. Honeypot Check
...
## 6. LP Lock
...

Only render sections for checks that were selected. An unavailable section means that data source needed a key or returned nothing — not that the token is safe. State that explicitly.

Notify

Send one consolidated alert via ./notify — never one per check (don't double-notify).

  • Single-check run: use that check's own notify trigger and format verbatim:
    • rug → notify if verdict ≥ ELEVATED.
    • contract → notify if a live, non-renounced power in {upgrade, mint, blacklist, drain} exists.
    • deployer → notify if ≥2 deployments classify as RUGGED.
    • holders → notify if verdict is CONCENTRATED or FRAGILE.
    • honeypot → notify only if LIKELY_HONEYPOT.
    • lp → notify only if UNLOCKED or PARTIAL.
  • Multi-check run: notify when the composite is concerning — any of: rug risk HIGH/CRITICAL; rug ELEVATED combined with another red flag (unverified source, live owner powers, top holder > ~30%); honeypot LIKELY_HONEYPOT; LP UNLOCKED/PARTIAL; deployer serial-rug (≥2 RUGGED); a live drain/upgrade/mint/blacklist power; holders CONCENTRATED/FRAGILE.

Keep it under 4000 chars, lead with the verdict, use clickable URLs. Example (multi-check):

*Investigation Report — 0xToken (Base)*
At a glance: Rug HIGH · unverified · top holder 61% · LP UNLOCKED · honeypot LIKELY ⚠️

Multiple red flags across rug, holders, lp and honeypot. Sells appear restricted
and liquidity is removable. Full report saved. Treat with caution.

Token: https://basescan.org/token/0xToken

Example single-check notify formats (use verbatim when only that check ran):

*Rug Scan — TOKEN_NAME (Base)*        *Contract Audit — CONTRACT_NAME (Base)*
Verdict: HIGH (score 7/12)            Verified: yes · Proxy: UUPS · Owner: multisig
Red flags: • Mint authority live      Live powers: • Upgradeable • mint() • rescueTokens()
Token: https://basescan.org/token/0xToken     Contract: https://basescan.org/address/0xAddr
*Honeypot Check — 0xToken (Base)*     *LP Lock Check — 0xToken (Base)*
Verdict: LIKELY_HONEYPOT ⚠️            Verdict: UNLOCKED ⚠️
A transfer from a real holder          Main pool 0xPool — ~0% of LP burned/locked;
reverted in simulation.                liquidity largely removable (rug risk).
Token: https://basescan.org/token/0xToken     Pool: https://basescan.org/address/0xPool

Log

Append to memory/logs/${today}.md under one heading (regardless of verdict — audit trail), with a discriminator line naming the checks + depth that ran:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
750
Forks
264
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
investigation-report
Source
github.com/aeonfun/aeon