Live: https://tracehound.vercel.app
Agentic on-chain hack tracer with executable evals, built for the gap institutional vendors (Chainalysis, TRM Labs, Elliptic) leave open: individual cases too small for those vendors to prioritize, too niche for FBI to move fast on. Built from experience working with FBI and U.S. Secret Service on a hack + tracking/referring $6M of crypto crime.
Give it a seed address (e.g. an exploited contract or a hacker's wallet), and it:
- Walks live outgoing transfers N hops out using the real Etherscan v2 API (works across Ethereum, BNB Chain, Polygon, Arbitrum — anywhere Etherscan's unified API covers), rendered as a radial hop-by-hop graph (seed at center, watchlist hits highlighted) alongside a full table.
- Flags any address on the watchlist (
data/watchlist.json) — populated with real, current OFAC SDN-sanctioned digital currency addresses (105 as of this writing, EVM-format only: ETH/ARB/BSC/USDC/USDT-on-EVM), pulled directly from the U.S. Treasury's official SDN list. Refresh it any time withnpm run update-watchlist. - Has an LLM agent narrate the trace in plain English — this narration step, not the hop-walk, is the actual differentiator over clicking through a block explorer by hand. Every material model claim is required to cite a stable evidence record displayed with the output.
- Looks up each address's verified smart contract name (free, via Etherscan's official
getsourcecodeendpoint) — e.g. flagging that a hop went toUniswapV2Router02, not a person's wallet. This is deliberately different from exchange/mixer attribution: it's an objective fact ("this address is a verified contract named X"), not a guess about ownership. - Can draft a demand/freeze-request letter from the real trace facts (loss amount + optional FBI IC3 complaint number), for victims whose case is too small for a Chainalysis/TRM-tier vendor to prioritize. It never claims a specific exchange relationship, never claims the letter itself can compel a freeze, and always tells the victim to file with IC3 regardless.
Read this before treating any output as authoritative:
- No mixer/exchange attribution database. The watchlist has real OFAC sanctions data (see above), but it does not know which addresses belong to mixers or exchanges — that data set is most of what Chainalysis, TRM Labs, and Elliptic actually sell, built over years from subpoenas and law-enforcement cooperation. Community datasets exist if you want to extend it: https://github.com/OffcierCia/On-Chain-Investigations-Tools-List
- No freezing capability. Only an exchange's compliance team can freeze funds, and generally only with law enforcement legal process behind it. This tool drafts the letter from real trace facts; it never sends it, never claims an exchange relationship, and never guarantees a hold.
- No cross-chain bridge/mixer demixing. Following funds through a bridge or a mixer like Tornado Cash requires dedicated heuristics this MVP doesn't implement.
- Not court-admissible chain of custody. No cryptographic proof of data integrity, no expert witness track record.
What it's honest about being: a real, functional first-hop tracer with an autonomous narration layer, useful for a fast first look at where funds moved — not a regulatory-grade investigation platform.
npm install
cp .env.example .env.local
# fill in ETHERSCAN_API_KEY (free, https://etherscan.io/apis)
# fill in ANTHROPIC_API_KEY (https://console.anthropic.com/)
npm run devOpen http://localhost:3000, paste an address, pick a chain and hop depth, and trace.
src/
app/
page.tsx UI: address form, results, narrative
api/trace/route.ts server-side chain walk (keeps API keys off the client)
api/narrate/route.ts LLM narration of the trace graph
api/draft-letter/ LLM-drafted demand/freeze-request letter from trace facts
mcp/
server.ts MCP stdio server: read-only trace/watchlist/evidence tools
lib/
etherscan.ts Etherscan v2 API client (tx history + verified-contract lookup)
evidence.ts stable evidence records shared by prompts, UI, and evals
trace.ts BFS outward walk, fan-out capping, watchlist + contract enrichment
validate.ts shared trace-input rules, so the HTTP route and MCP can't drift
watchlist.ts watchlist lookup
components/
TraceGraphView.tsx radial SVG hop graph (hand-rolled, no charting library)
scripts/
update-watchlist.js pulls the real OFAC SDN advanced XML and refreshes watchlist.json
mcp-smoke.mjs end-to-end MCP wiring check over a real stdio client
data/
watchlist.json known-address list — burn/zero + real OFAC-sanctioned addresses
evals/
golden-set.json synthetic clean and adversarial model-output cases
lib/evaluator.mjs deterministic policy and evidence checker
judge-prompt.md complementary semantic rubric (not yet calibrated)
At a glance:
| Layer | What it does | Status |
|---|---|---|
Golden set — evals/golden-set.json |
Synthetic clean + adversarial model-output cases | Built |
Deterministic gate — evals/lib/evaluator.mjs |
Catches invented identifiers, false watchlist claims, missing citations, unsafe letter claims — npm run eval |
Built, executable |
Judge rubric — evals/judge-prompt.md |
Semantic failures pattern checks can't reliably catch | Designed |
Calibration plan — evals/validation-plan.md |
Labeled splits, per-violation precision/recall, re-calibration triggers | Planned — not yet validated |
The obvious thing to evaluate here is the OFAC watchlist matching, and that would be a mistake:
it's a deterministic set-membership check against watchlist.json. Correctness there is a unit
test, not an eval, and wrapping an LLM judge around a lookup would measure nothing.
The parts that actually need evaluating are the two LLM-generated outputs — the trace narration and the demand-letter draft — because both make claims a reader will act on, and both have constraints that are already written down in the "What this is not" section above. That makes the eval criteria unusually concrete:
Faithfulness — does the narration describe only what the trace data actually shows? Specific failure modes to catch: invented hops, wrong amounts, asserted ordering the BFS walk didn't establish, or a watchlist hit claimed where none exists.
Constraint compliance — the tool's stated guarantees are testable as pass/fail criteria:
- never claims a specific exchange relationship for an address
- never claims the letter itself can compel a freeze
- always tells the victim to file with IC3 regardless
- never presents a verified-contract name as ownership attribution
A violation of any of these is a real-world problem, not a style issue — the user may act on the output, and overclaiming is the failure mode with actual consequences.
The built evaluation layer has two parts:
- An executable deterministic gate over a synthetic golden set. It catches invented on-chain
identifiers, false watchlist claims, missing/dangling evidence citations, unsupported owner or
exchange attribution, verified-contract/ownership conflation, missing truncation disclosure,
and unsafe letter claims. Run it with
npm testandnpm run eval. - A semantic judge rubric and calibration plan for failures that pattern checks cannot reliably understand. The judge is designed but deliberately not represented as validated. It must be calibrated against independently hand-labeled examples before its scores can gate releases.
Runtime output now uses the same evidence contract: prompts receive numbered evidence records, material claims must cite them, and the UI exposes the full evidence appendix. That does not make the model infallible; it makes claims inspectable and gives the evals a concrete contract to test.
See evals/README.md and
evals/validation-plan.md for scope and calibration requirements.
TraceHound exposes its trace, watchlist, and evidence layers to any MCP client — Claude Desktop, Claude Code, another agent.
npm run mcp:build # compile src/mcp + src/lib to dist/ (separate from the Next build)
npm run mcp # start the server on stdio
npm run mcp:smoke # verify the wiring end-to-end with a real MCP clientTo use it from Claude Desktop or Claude Code, point an MCP client at the built server:
{
"mcpServers": {
"tracehound": {
"command": "node",
"args": ["/absolute/path/to/tracehound/dist/src/mcp/server.js"],
"env": { "ETHERSCAN_API_KEY": "your-key" }
}
}
}| Tool | Returns |
|---|---|
trace_address(address, chain?, depth?) |
Trace summary plus EvidenceRecord[] with stable IDs, and a traceId |
check_watchlist(address) |
Exact-address watchlist match, with the not-evidence-of-legitimacy caveat attached |
get_evidence(traceId) |
The evidence records for a trace already run this session |
Two design decisions worth stating, because they are the point rather than an implementation detail:
It returns evidence records, not prose. The eval layer's whole contract is that every material
claim cites a stable evidence ID. If this interface handed back a paragraph, a consuming agent
would have nothing to cite and the citation discipline would be lost exactly where it matters
most — at the machine-to-machine boundary, where no human is reading. So trace_address returns
the same E1/E2/E3 records the UI and the letter drafter use, and every response repeats the
"What this is not" limitations inline, since an agent may never see this
README.
It is read-only, and letter drafting is deliberately not exposed. Producing a legal demand
letter should not be something an agent can trigger unattended; it stays behind the UI where a
human reads it before it exists. tests/mcp.test.mjs fails if a tool is
ever added to the server without that decision being revisited.
Trace storage for get_evidence is in-memory and scoped to the server process (persistent case
storage is still future scope, below), so traces do not survive a restart.
Ordered by signal, not by effort. The first three are about making the evidence claim hold up; the rest extend reach.
- Calibrate the judge rubric. This is the highest-value gap in the repo, because it is the one
place the project's own standard is not yet met.
evals/judge-prompt.mdis designed but unvalidated, and an uncalibrated judge is a confidence signal with nothing behind it.evals/validation-plan.mdalready specifies the work: labeled splits, per-violation TPR/TNR against human labels, and re-calibration triggers. Until that runs, the deterministic gate is the only layer that has earned trust. - Watchlist staleness as evidence. A watchlist miss is currently reported with the caveat that
absence is not evidence of legitimacy — true, but incomplete. The evidence record should carry
when
data/watchlist.jsonwas last refreshed from the OFAC SDN list and how many entries it holds. A no-match against a six-month-old list is a materially weaker claim than one against a list pulled this morning, and right now nothing downstream can tell those apart. - Evidence integrity and deterministic replay. Record the raw Etherscan responses behind a trace and hash each evidence record over its source payload. Two payoffs: evals become runnable offline against fixtures instead of live chain state, and a trace becomes re-verifiable after the fact. This does not make output court-admissible — that needs chain of custody and an expert witness, neither of which is a software feature — but it closes the gap between "we assert this" and "you can check this."
- MCP resources and prompts, not only tools. The server currently exposes tools. Exposing a completed trace as an MCP resource with a stable URI would let a client re-read a case without a tool round-trip, and shipping an MCP prompt that encodes the citation contract would push the evidence discipline into the consuming agent's context rather than relying on it to read tool descriptions carefully.
- Etherscan rate-limit and backoff handling. At depth 5 with a fan-out cap of 8, a single trace can issue enough calls to hit the free tier's limit mid-walk. There is no retry or backoff today, so a throttled response surfaces as a failed trace rather than a slower one.
- Incoming-transfer tracing (where funds came from), not just outward.
- Bridge-hop following.
- Persistent case storage so a trace can be revisited/exported as a report (PDF). Would also give
the MCP server's
get_evidencea durable backing store instead of per-process memory.
