
Agents for Humans: building ChainPilot, an autonomous EVM triage agent with the Strands Agents SDK
Build story for the Agents for Humans hackathon. Repo: https://github.com/cryptoflops/chainpilot
Build story for the Agents for Humans hackathon. Repo: https://github.com/cryptoflops/chainpilot
I hunt smart contracts for bug bounties. Every new program starts the same way: a scope list of 30 to 50 contract addresses, and thirty minutes of clerical work per engagement before any interesting analysis happens. Is this address actually a contract? What does
name() return? Does the supply look sane? Does the proxy point where the docs say it points? The same five reads, forty-seven times, in Etherscan tabs.The boring part is where triage mistakes hide: wrong address pasted, stale docs trusted, copy-paste assumptions. It is also the part a machine should do. So I built ChainPilot: an autonomous agent that runs the clerical loop over live Ethereum mainnet and surfaces a written triage note only when there is something to read. Judgment stays with me.
Why Strands
The whole product is a tool-use loop, and I did not want to own a control loop. Strands Agents SDK gives me the event loop (planning, tool dispatch, retries, streaming) in about ten lines of agent construction, so the code I actually own is the interesting part: the EVM tool layer and the trust discipline.
1
2
3
4
5
6
7
8
from strands import Agent
from strands.models.gemini import GeminiModel
agent = Agent(
model=GeminiModel(...),
tools=[eth_call, eth_get_code, eth_get_storage_at, read_file, write_file],
system_prompt=SYSTEM_PROMPT,
)Five
@tool functions, all plain Python over requests:| Tool | What it does |
|---|---|
eth_get_code | contract liveness and bytecode size |
eth_call | raw mainnet read, publicnode with llamarpc fallback |
eth_get_storage_at | raw storage slot reads (EIP-1967 proxy targets) |
read_file / write_file | artifact handling, sandboxed, 4 KB caps |
No web3.py dependency, no API keys, no RPC account. Public JSON-RPC is permissionless by design, which means the agent's blast radius is zero: it can read the chain and write files inside its own directory. That is the kind of agent people can actually trust first.
The trust model: no fabrication, auditable hex
An LLM describing on-chain data is a liability. One invented address in a triage note and the whole artifact is worthless. So the system prompt bans fabricated chain data, and the tool layer enforces it structurally:
- Every value the agent reports must come from a tool result. The note is written with
write_file, then re-read withread_fileto verify it landed on disk. - Raw hex travels with every decoded value.
0x...5772617070...is "Wrapped Ether" because the ABI says so, and the note keeps the bytes next to the decoding so a human can audit the claim in seconds. - The console shows the actual Strands events. The UI is a Server-Sent Events stream of the real event loop: tool calls, tool results, deltas. Nothing is simulated. If the agent fails, the card says
no data.
The dashboard is FastAPI serving one HTML file. The streaming side was the interesting engineering: Strands'
stream_async emits toolResult events without the tool name, so I correlate toolUseId to the preceding toolCall to label each card correctly.Model portability is the point
The demo runs Gemini through
GeminiModel because that is the model I have a free key for. Swapping to Amazon Bedrock is a one-line change to the model= argument, and the tools, prompt, and loop are untouched. That portability is exactly what I want from an SDK: the model is a config value, the workflow is the product.Three bugs worth writing down
Fresh-clone demo silently ran in mock mode.
load_dotenv() resolves relative to the current working directory, and demo.sh ran Python from the repo root while .env lived there and the code expected it in src/. The deterministic mock path masked the failure: exit code 0, plausible output, wrong product. Caught it only because I cloned the published repo into a clean directory and ran bash demo.sh the way a judge would. Lesson: run the fresh-clone proof against the public URL, not your working copy.localhost worked in curl and failed in the browser. The server bound IPv4-only; the browser resolved
localhost to ::1 and got connection refused. --host :: then broke IPv4. The fix is an explicit dual-stack socket that clears IPV6_V6ONLY, in a small run.py.Test deadlines were time bombs. Fixtures hardcoded future dates that quietly became past dates, and the scorer zeroed every factor for expired opportunities. A green suite turned red by doing nothing. Dates in tests are now computed from
date.today().Reproducibility as a feature
bash demo.sh on a fresh clone installs dependencies, runs the agent, and prints the tool trace plus the written artifact. No key? Deterministic mock mode, exit 0, useful for CI. Any AI Studio key? The real loop against mainnet: bytecode size, decoded name(), supply, proxy slot reads, the note on disk. The agent writes its own proof, which is the claim I most want judges to test.What is next
Cross-check chain reads against an independent oracle so verdicts come from two agreeing sources, disagreements surfaced rather than averaged. Batch mode: feed the scope CSV of a new program, wake up to a triage folder. Spending authority is a later design question, not a demo shortcut.
The code is at https://github.com/cryptoflops/chainpilot (MIT). If you triage contracts yourself, the interesting question to answer is whether you would trust this note without re-checking the hex. I would, but that is the claim I want you to test.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article