Self-hosted, API-key-free pre-sign checks for Ethereum transactions.
Before a transaction is signed, txlens answers what will this actually do?
It decodes the calldata (recursively, through multicalls, Safe wrappers and the
Uniswap Universal Router), simulates the call against any JSON-RPC endpoint,
lists the asset transfers and approval changes it would cause, decodes the
revert reason if it fails, and raises a small set of high-signal risk flags.
It also inspects mined transactions the same way.
$ txlens simulate --rpc $RPC --from 0xF977…aceC --to 0xdAC1…1ec7 --data 0xa9059cbb…
Calldata
transfer(address,uint256) [sourcify]
_to address = 0x28C6c06298d514Db089934071355E5743bf21d60
_value uint256 = 1000000
Result: SUCCESS gas estimate: 46479 trace gas used: 46097
Asset changes (from trace)
0x28C6c06298d514Db089934071355E5743bf21d60 +1.0 USDT
0xF977814e90dA44bFA03b6295A0616a897441aceC -1.0 USDT
Single static binary, Go, one dependency (go-ethereum for ABI/RLP/crypto).
Works against any endpoint; gets richer (call tree, asset changes, state diff)
when the endpoint exposes debug_traceCall.
- Tenderly's simulation API is not on the free tier; Blocknative/Alchemy/etc. need keys and send your unsigned transactions to a third party. A wallet, an agent framework or a CI job should be able to run this check against its own node, or any public one.
- Agents that send transactions need a guardrail that is a process, not a
prompt:
txlens simulate --json --fail-on-riskreturns a structured report and a non-zero exit code when the transaction reverts or grants an unlimited approval. castdecodes and calls, but does not tie calldata → trace → asset changes → approvals → warnings together, and has no selector-database / proxy-aware ABI resolution.
go install github.com/meldrov/txlens/cmd/txlens@latest
# or
git clone https://github.com/meldrov/txlens && cd txlens && make build # → bin/txlensGo 1.26+. Prebuilt static binaries for Linux, macOS and Windows (amd64/arm64)
are on the Releases page, with a
checksums.txt to verify downloads.
All outputs below are real mainnet runs (2026-08-21) against
https://eth.drpc.org, trimmed for width. https://eth.merkle.io also exposes
debug_* without a key; publicnode, 1rpc, cloudflare-eth, flashbots
and nodies do not (txlens then falls back to eth_call only and says so).
export TXLENS_RPC=https://eth.drpc.org # or pass --rpc on every commandUniversal Router input of a real swap (0x9fad0837…), ABI from Sourcify,
commands and per-command inputs decoded, V3 path rendered:
$ txlens decode --to 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af --data 0x3593564c…
contract: 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af (UniversalRouter, verified)
execute(bytes,bytes[],uint256) [sourcify]
commands bytes = 0x0a00
↳ PERMIT2_PERMIT, V3_SWAP_EXACT_IN
inputs bytes[] (2 items)
[0] PERMIT2_PERMIT bytes = 0x0000000…0000000 (352 bytes)
→ PERMIT2_PERMIT(((address token, uint160 amount, uint48 expiration, uint48 nonce) details, address spender, uint256 sigDeadline) permitSingle, bytes signature) [builtin]
permitSingle ((address,uint160,uint48,uint48),address,uint256)
details (address,uint160,uint48,uint48)
token address = 0xdAC17F958D2ee523a2206206994597C13D831ec7
amount uint160 = 1461501637330902918203684832716283019655932542975
expiration uint48 = 1789878407
nonce uint48 = 512
spender address = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af
sigDeadline uint256 = 1787286527
signature bytes = 0x1364b6644f2f…c85c551b (65 bytes)
[1] V3_SWAP_EXACT_IN bytes = 0x0000000…0000000 (256 bytes)
→ V3_SWAP_EXACT_IN(address recipient, uint256 amountIn, uint256 amountOutMin, bytes path, bool payerIsUser) [builtin]
recipient address = 0x0000000000000000000000000000000000000001
↳ MSG_SENDER
amountIn uint256 = 169052246
amountOutMin uint256 = 605290434415167272469
path bytes = 0xdac17f958d2ee523a2206206994597c13d831ec7000bb89d70bae2944ffa477f37bae227fd981e6eb31982
↳ 0xdAC17F958D2ee523a2206206994597C13D831ec7 -(0.3%)-> 0x9d70baE2944Ffa477F37Bae227fd981E6eB31982
payerIsUser bool = true
↳ tokens pulled from msg.sender via Permit2
deadline uint256 = 1787286527
SwapRouter02 exactInputSingle (struct parameter), tx 0x55d9f117…:
$ txlens decode --to 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 --data 0x04e45aaf…
contract: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 (SwapRouter02, verified)
exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) [sourcify]
params (address,address,uint24,address,uint256,uint256,uint160)
tokenIn address = 0x100acD9FcD8E0FF80A6595B66fdABe93184Aa100
tokenOut address = 0xdAC17F958D2ee523a2206206994597C13D831ec7
fee uint24 = 100
recipient address = 0xe0eD57c18f9eEacd41A1113E41175237f7AB7Cb7
amountIn uint256 = 8268337
amountOutMinimum uint256 = 8793016
sqrtPriceLimitX96 uint160 = 0
Offline, no --to: built-in signatures still cover the common standards.
$ txlens decode --offline --data 0xa9059cbb000…0f4240
transfer(address,uint256) [builtin]
to address = 0x28C6c06298d514Db089934071355E5743bf21d60
amount uint256 = 1000000
USDT transfer from the Binance hot wallet:
$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
--to 0xdAC17F958D2ee523a2206206994597C13D831ec7 \
--data 0xa9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d6000000000000000000000000000000000000000000000000000000000000f4240
Request
from: 0xF977814e90dA44bFA03b6295A0616a897441aceC
to: 0xdAC17F958D2ee523a2206206994597C13D831ec7
value: 0 ETH
data: 0xa9059cbb000000…0f4240 (68 bytes)
block: latest (chain 1)
contract: 0xdAC17F958D2ee523a2206206994597C13D831ec7 (TetherToken, verified)
Calldata
transfer(address,uint256) [sourcify]
_to address = 0x28C6c06298d514Db089934071355E5743bf21d60
_value uint256 = 1000000
Result: SUCCESS gas estimate: 46479 trace gas used: 46097
Asset changes (from trace)
0x28C6c06298d514Db089934071355E5743bf21d60 +1.0 USDT
0xF977814e90dA44bFA03b6295A0616a897441aceC -1.0 USDT
transfers:
0xF977…aceC → 0x28C6…1d60 1.0 USDT
Call trace (1 frames)
CALL 0xdAC17F958D2ee523a2206206994597C13D831ec7 transfer(address,uint256) gas=46097
log Transfer(from=0xF977814e90dA44bFA03b6295A0616a897441aceC, to=0x28C6c06298d514Db089934071355E5743bf21d60, value=1000000)
Revert: transferring more USDC than the balance. The proxy (ZeppelinOS slot)
is detected, the implementation ABI is merged, and the Error(string) comes
out of the DELEGATECALL frame. Exit code 1.
$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
--to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --data 0xa9059cbb…0c9f2c9cd04674edea40000000
contract: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (FiatTokenProxy, verified)
proxy (zeppelinos) → 0x43506849D7C04F9138D1A2050bbF3A0c054402dd (FiatTokenV2_2, verified)
Calldata
transfer(address,uint256) [sourcify]
to address = 0x28C6c06298d514Db089934071355E5743bf21d60
value uint256 = 1000000000000000000000000000000
Result: REVERTED trace gas used: 36349
reason: Error("ERC20: transfer amount exceeds balance")
Asset changes (from trace): none
Call trace (2 frames)
CALL 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 transfer(address,uint256) gas=36349 ✗ execution reverted: Error("ERC20: transfer amount exceeds balance")
DELEGATECALL 0x43506849D7C04F9138D1A2050bbF3A0c054402dd transfer(address,uint256) gas=7361 ✗ execution reverted: Error("ERC20: transfer amount exceeds balance")
The same over-balance transfer on USDT (Solidity 0.4, assert-style) fails
with an INVALID opcode and no data; txlens reports that instead of guessing:
Result: REVERTED trace gas used: 600000000
reason: reverted without data (invalid opcode: INVALID)
Custom errors resolve through the target's ABI. Re-running a mined Universal Router transaction from its signed raw bytes after its deadline:
$ txlens simulate --raw 0x02f90413…
Result: REVERTED trace gas used: 36740
reason: TransactionDeadlinePassed()
error TransactionDeadlinePassed() resolved via sourcify
data: 0x5bf6f916
Unlimited approval → warning, and exit code 3 with --fail-on-risk:
$ txlens simulate --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
--to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
--data 0x095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
--fail-on-risk
Calldata
approve(address,uint256) [sourcify]
spender address = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
value uint256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935
Result: SUCCESS gas estimate: 56711 trace gas used: 55906
return: 0x0000000000000000000000000000000000000000000000000000000000000001
Asset changes (from trace)
approvals:
USDC: 0xF977…aceC approves 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 for UNLIMITED (⚠)
Call trace (2 frames)
CALL 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 approve(address,uint256) gas=55906
DELEGATECALL 0x43506849D7C04F9138D1A2050bbF3A0c054402dd approve(address,uint256) gas=26673
log Approval(owner=0xF977…aceC, spender=0x68b3…Fc45, value=1157920892373161954235709850086879078532699846656405640394575840079131296399
35)
WARNINGS
[warning] unlimited-approval: USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48): approve 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 for UNLIMITED — the spender can move the owner's entire balance at any time
$ echo $?
3
State overrides: fund an address that has nothing and wrap ETH.
$ txlens simulate --from 0x000000000000000000000000000000000000dEaD \
--to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --value "1 ether" --data 0xd0e30db0 \
--override-balance 0x000000000000000000000000000000000000dEaD="10 ether"
Result: SUCCESS gas estimate: 28284 trace gas used: 27938
Asset changes (from trace)
0x000000000000000000000000000000000000dEaD -1.0 ETH, +1.0 WETH
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 +1.0 ETH
transfers:
0x0000…dEaD → 0xC02a…6Cc2 1.0 ETH
0x0000…0000 → 0x0000…dEaD 1.0 WETH
--state-diff adds the prestateTracer diff (storage slots, balances, nonces):
State diff (prestateTracer)
0xF977814e90dA44bFA03b6295A0616a897441aceC
nonce: 20302 → 20303
0xdAC17F958D2ee523a2206206994597C13D831ec7
storage[0x0be16d71…405f36]: 0x…0041faf2403f74d7 → 0x…0041faf240303297
storage[0x78b35599…1436e0]: 0x…00041a22d373ed6d → 0x…00041a22d3832fad
Endpoint without the debug namespace (here ethereum-rpc.publicnode.com):
Result: SUCCESS gas estimate: 46479
Call trace: unavailable — provider lacks debug_traceCall (the method debug_traceCall does not exist/is not available); asset changes and the call tree need an endpoint with the debug namespace
$ txlens tx 0x9fad0837a86f1e8c59e0c1fca03b8a03e35705808fbffda6a7bd639721540192
Transaction 0x9fad0837a86f1e8c59e0c1fca03b8a03e35705808fbffda6a7bd639721540192
status: SUCCESS
block: 25801109 (index 339)
from: 0x56d2f51A6e15B6d1CfE89698Dc000c41dbD4F0A9
to: 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af
value: 0 ETH
nonce: 1043 type: 2
gas: 157805 used / 211439 limit @ 0.093064677 gwei fee 0.000014686071353985 ETH
contract: 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af (UniversalRouter, verified)
Calldata
execute(bytes,bytes[],uint256) [sourcify]
commands bytes = 0x0a00
↳ PERMIT2_PERMIT, V3_SWAP_EXACT_IN
… (as in the decode example above)
Logs (4)
[389] 0x0000…8BA3 Permit(owner=0x56d2…F0A9, token=0xdAC1…1ec7, spender=0x66a9…8Af, amount=1461501637330902918203684832716283019655932542975, expiration=1789878407, nonce=512)
[390] 0x9d70…1982 Transfer(from=0xfAf4…8384, to=0x56d2…F0A9, value=608332094889615349216)
[391] 0xdAC1…1ec7 Transfer(from=0x56d2…F0A9, to=0xfAf4…8384, value=169052246)
[392] 0xfAf4…8384 Swap(sender=0x66a9…8Af, recipient=0x56d2…F0A9, amount0=-608332094889615349216, amount1=169052246, sqrtPriceX96=41715199519853159017144, liquidity=547256676836576503, tick=-289154)
Asset changes (from trace)
0x56d2f51A6e15B6d1CfE89698Dc000c41dbD4F0A9 +608.332094889615349216 AYNI, -169.052246 USDT
0xfAf41F3761EB08374639955BDE44CBbF3dcC8384 -608.332094889615349216 AYNI, +169.052246 USDT
transfers:
0xfAf4…8384 → 0x56d2…F0A9 608.332094889615349216 AYNI
0x56d2…F0A9 → 0xfAf4…8384 169.052246 USDT
Call trace (11 frames)
CALL 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af execute(bytes,bytes[],uint256) gas=157805
CALL 0x000000000022D473030F116dDEE9F6B43aC78BA3 permit(address,((address,uint160,uint48,uint48),address,uint256),bytes) gas=13686
log Permit(owner=0x56d2…F0A9, token=0xdAC1…1ec7, spender=0x66a9…8Af, …)
STATICCALL 0x0000000000000000000000000000000000000001 0x72ec0f95? gas=3000
CALL 0xfAf41F3761EB08374639955BDE44CBbF3dcC8384 swap(address,bool,int256,uint160,bytes) gas=105977
log Swap(…)
CALL 0x9d70baE2944Ffa477F37Bae227fd981E6eB31982 transfer(address,uint256) gas=22733
DELEGATECALL 0x4A13A71d29b4e678497efD4D230c5e324CB9e5c7 transfer(address,uint256) gas=17837
log Transfer(from=0xfAf4…8384, to=0x56d2…F0A9, value=608332094889615349216)
STATICCALL 0xdAC17F958D2ee523a2206206994597C13D831ec7 balanceOf(address) gas=5031
CALL 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af uniswapV3SwapCallback(int256,int256,bytes) gas=26253
CALL 0x000000000022D473030F116dDEE9F6B43aC78BA3 transferFrom(address,address,uint160,address) gas=23737
CALL 0xdAC17F958D2ee523a2206206994597C13D831ec7 transferFrom(address,address,uint256) gas=22530
log Transfer(from=0x56d2…F0A9, to=0xfAf4…8384, value=169052246)
STATICCALL 0xdAC17F958D2ee523a2206206994597C13D831ec7 balanceOf(address) gas=1031
WARNINGS
[info] permit-signature: calldata carries permit-style signature(s): PERMIT2_PERMIT — an off-chain signature grants a token allowance; check signer, spender, amount and deadline
txlens simulate --tx-hash 0x9fad08… re-runs the same transaction at its
parent block and produces the same asset changes (with a note that intra-block
ordering is not reproduced).
Common flags (every subcommand): --rpc URL (or TXLENS_RPC), --chain-id N
(when no RPC; default 1), --abi file.json (repeatable; bare ABI arrays and
Foundry/Hardhat artifacts), --json, --no-cache, --cache-dir DIR,
--offline (no Sourcify/selector-DB), --timeout 60s.
txlens decode --data 0x… [--to 0xADDR] [--depth 5]
--to enables Sourcify lookup (and, with --rpc, on-chain proxy detection).
Without it only local ABIs, built-ins and the selector databases are used.
txlens simulate --from 0x… --to 0x… [--value WEI|"0.1 ether"|"5 gwei"] [--data 0x…] [--gas N]
[--block latest|N] [--override-balance ADDR=WEI]… [--override-code ADDR=0x…]…
[--state-diff] [--no-trace] [--fail-on-risk]
txlens simulate --tx-hash 0x… # mined tx, replayed at its parent block (fee fields dropped)
txlens simulate --raw 0x… # signed raw tx (any type); sender recovered from the signature
Steps: eth_estimateGas → eth_call (state overrides attached when given) →
if the endpoint answers debug_traceCall (probed once): callTracer
(withLog) for the call tree and asset changes, prestateTracer (diffMode)
with --state-diff.
Exit codes: 0 success, 1 the call reverts, 2 usage / RPC error,
3 warning-level risk flags with --fail-on-risk.
txlens tx 0xHASH [--no-trace]
Receipt, decoded input, decoded logs, debug_traceTransaction call tree and
asset changes. Without the debug namespace, asset changes come from receipt
logs (native value flows inside the call tree are then invisible) and the
revert reason of a failed transaction is recovered by replaying eth_call at
the parent block. Exit code 1 if the transaction reverted.
--json prints one object. schema is 1 and is bumped on incompatible
changes. Abbreviated shapes (full structs in internal/render/types.go):
Amounts (amount, value, deltas) are decimal strings in base units;
formatted applies token decimals when known.
For each selector, candidates are gathered in this order and the first one whose decoding re-encodes to exactly the calldata wins (falling back to the first that unpacks at all):
- Local
--abifiles. - Sourcify v2 —
GET /v2/contract/{chainId}/{address}?fields=abi,compilation,proxyResolutionfor the target. With--rpc, proxies are resolved on-chain first: EIP-1967 implementation slot, EIP-1967 beacon slot (→implementation()), ZeppelinOS legacy slot, EIP-1167 minimal-proxy bytecode, and EIP-897implementation()on the proxy itself; Sourcify'sproxyResolutionis the fallback. The implementation's ABI is merged into the target's. - Built-in signatures: ERC-20/721/1155, WETH, Multicall/Multicall3, Safe, Universal Router, Uniswap V2/V3 routers, Permit2, common proxy functions, the standard token events, and OpenZeppelin 5 / Permit2 / Universal Router custom errors.
- openchain (
/signature-database/v1/lookup?function=…&filter=true), then 4byte.directory only if openchain has nothing (4byte entries sorted by id, oldest first — far fewer spam collisions). Signatures whose real selector does not match are discarded. Candidates from these sources are markedinexactin text output when the re-encoding check fails, andalternativeslists the other matching signatures.
Events use the same chain (contract ABI → built-ins → openchain/4byte). For
signature-only events the indexed layout is unknown, so txlens tries layouts
(preferring indexed address parameters) until topics and data decode
exactly. Custom errors: contract ABI of the callee and of the deepest
reverting frame → built-ins → openchain (which indexes error selectors in the
function namespace).
Everything network-derived is cached under $XDG_CACHE_HOME/txlens or
~/.cache/txlens (Sourcify hits forever, misses 6 h; selector lookups 30 d,
misses 24 h; token metadata 30 d). --no-cache bypasses it, --offline
disables network lookups.
With a callTracer tree (debug_traceCall / debug_traceTransaction with
withLog: true):
- Native:
valueof everyCALL,CALLCODE,CREATE/CREATE2andSELFDESTRUCTframe (notDELEGATECALL/STATICCALL), fromfromtoto. - ERC-20:
Transfer(address,address,uint256)with 3 topics (value in data),Approvalwith 3 topics; WETH-styleDeposit/Withdrawalas mint/burn. - ERC-721:
Transferwith 4 topics (tokenId indexed),Approvalwith 4 topics,ApprovalForAll. - ERC-1155:
TransferSingle,TransferBatch. - Frames that reverted — and everything beneath them — are skipped, since their effects were rolled back.
Transfers are netted per address and asset; the zero address (mint/burn
counterparty) is omitted from the net view. Tokens are enriched with
symbol()/decimals()/name() via eth_call (bytes32 symbols such as MKR
are handled), cached.
Without a trace (tx on a non-debug endpoint) the same classification runs
on receipt logs, plus the transaction's own value.
| code | severity | raised when | why it matters |
|---|---|---|---|
unlimited-approval |
warning | ERC-20 Approval ≥ 2^96−1 (covers MaxUint256, Permit2's uint160 max, uint96 max) |
spender can drain the whole balance at any later time |
approval-for-all |
warning | ApprovalForAll(…, true) |
operator may move every token of the collection |
approval-to-eoa |
warning | approval whose spender has no code | approvals are granted to contracts; an EOA spender is a classic drainer pattern |
calldata-to-eoa |
warning | to has no code but calldata is non-empty |
the call "succeeds" and does nothing — wrong address or undeployed contract |
value-to-reverting-call |
warning | native value attached and the call reverts | nothing moves, gas is burnt; usually a construction bug |
unverified-contract |
info | to has code but no Sourcify ABI |
decoding relies on selector databases; read the trace, not the names |
permit-signature |
info | permit-style function or a PERMIT2_* Universal Router command in the calldata |
an off-chain signature grants allowance; verify signer/spender/amount/deadline |
delegatecall-unexpected |
info | to delegatecalls an address that is not its detected implementation |
callee runs with the target's storage and balance |
--fail-on-risk turns warning-level flags into exit code 3. The set is
deliberately small; it is meant as a guardrail, not a scanner.
- No prices / USD values, no cross-chain context; one endpoint, one chain.
- Call tree, asset changes and state diff need
debug_traceCall/debug_traceTransaction. Without it you geteth_call, gas estimate, decoded calldata and revert reason;txfalls back to receipt logs. - Selector databases collide and contain junk; txlens ranks by re-encoding
fidelity and source trust but can still name a function wrong. Prefer a
verified ABI (
--to+ Sourcify) or--abi. - Re-simulation (
--tx-hash,--raw) runs at the parent block's end state; transactions earlier in the same block are not replayed, so results can differ from the mined outcome. Fee fields are dropped. - Universal Router command table is the v2 layout (v1.x NFT commands kept where byte values do not collide); V4 actions are named but not decoded further.
- Internal (trace-level) value flows of a
txare only visible with the debug namespace; fee-on-transfer and rebasing tokens are reported by their events, not by balance diff. - Token metadata is fetched at
latest, not at the simulated block. - Public endpoints rate-limit; txlens retries transient errors a few times and caches aggressively, but large traces against free endpoints can still fail.
- MCP server exposing
decode/simulateas tools for agent pre-sign checks. - Library API (
pkg/txlens) with the same report structs. - EIP-712 typed-data explainer (Permit, Permit2, Safe, Seaport, …).
- Batch / CI mode: many transactions from a file, aggregated exit code.
- Safe transaction service integration: fetch a pending multisig tx by hash and simulate it as the Safe.
- Balance-diff based asset changes (prestateTracer) to catch non-standard tokens.
make build # bin/txlens with version ldflags
make test # offline unit tests (httptest fakes for Sourcify/openchain/4byte)
make test-integration # TXLENS_INTEGRATION=1, hits TXLENS_RPC (default https://eth.drpc.org)
make lint # gofmt + go vetLayout: cmd/txlens (stdlib flag dispatcher), internal/abiresolve
(local/Sourcify/selector-DB/proxy, cache), internal/decode (calldata → tree,
wrappers), internal/sim (RPC, overrides, tracers), internal/assets
(events/value flows → asset changes, metadata), internal/revert,
internal/risk, internal/render (report structs, text, JSON).
examples/ has real callTracer fixtures and commands.
Issues and PRs welcome. Keep the dependency list at go-ethereum + stdlib,
run make lint test before pushing, and add a fixture or an httptest-backed
test for anything that talks to the network.
MIT — see LICENSE.