Mapping Wallet Activity with Block Explorer Traces
Why I map wallet activity before touching funds
Block explorers are the fastest way I know to understand how a wallet really behaves. Before I move any funds, I map a wallet’s activity to see which contracts it touches, which tokens it moves, and how much gas it burns per workflow. This post covers the exact explorer tabs I use, how I interpret traces, and a small Python helper for normalizing CSV exports so I can compare activity across chains.
How block explorers actually organize wallet activity
Explorer pages look busy, but they are just views over the same three data sources: transactions, logs, and execution traces.
1) Transactions (EOA-level calls). The normal transactions list shows signed calls from the wallet, including the to/from, value, input data, gas used, and the status. That’s my first pass for “what did this wallet call and when.”
2) Logs (token transfers and events).
Token transfers on explorer tabs are derived from event logs like the ERC-20 Transfer event. If a wallet never appears in the normal transactions list but shows up in token transfers, that means some other contract acted on its behalf (e.g., a protocol pull). The standard event signature is defined in the ERC-20 spec and used by explorers to render those token movements. (ERC-20 standard)
3) Execution traces (internal calls).
Explorer “internal transactions” are derived from a node’s execution trace, which expands a single transaction into every call, delegatecall, and value transfer that happened within it. When I want to know why a swap moved a different token than expected, I look at the trace output. On self-hosted nodes, the trace data comes from debug_traceTransaction in the client API. (Geth debug RPC)
When I’m mapping a wallet, I open each of these tabs and build a simple inventory:
- Contracts touched: the destination contracts for signed transactions plus any contracts called in traces.
- Tokens moved: tokens listed in ERC-20 transfer logs.
- Gas profile: gas used for common actions (swap, approve, deposit).
This triage tells me whether a wallet is interacting with a known protocol or with obscure routers I should vet further.
Trace-first workflow for decoding confusing transactions
If a transaction succeeds but the resulting balances look wrong, I go straight to the trace view and reconstruct the flow:
- Confirm the outer call (the signed transaction). Identify the function selector in the “Input Data” section and verify it matches the action you think you sent. If the selector does not match, the wallet used a proxy or router you may not recognize.
- Walk each internal call in order. I focus on calls that send value or hit token contracts. That often reveals a hidden fee or a token approval I forgot about.
- Cross-check token logs to see the exact Transfer events fired. For ERC-20 tokens, I match the
from/toin the logs to the internal call stack.
When I’m using a lean wallet (sub-$1k), I don’t try to automate this entirely. I’m trying to avoid expensive mistakes, so a quick manual check after any odd transaction is worth the time.
Python helper: normalize explorer CSV exports
Most explorers let you export transactions as CSV. The columns differ slightly, so I normalize the fields I care about: timestamp, hash, action, counterparty, and a rough gas cost in ETH. This example uses a tiny in-memory CSV so you can run it without any external dependencies.
import csv
import io
from dataclasses import dataclass
from decimal import Decimal
RAW_CSV = """Timestamp,Txhash,Method,From,To,Value_IN(ETH),GasUsed,GasPrice
2026-01-01 12:01:00,0xaaa,swap,0xWallet,0xRouter,0.0,142321,2000000000
2026-01-01 12:05:00,0xbbb,approve,0xWallet,0xToken,0.0,48321,1500000000
2026-01-01 12:07:00,0xccc,deposit,0xWallet,0xVault,0.5,210000,2500000000
"""
@dataclass
class TxRow:
timestamp: str
tx_hash: str
method: str
counterparty: str
value_eth: Decimal
gas_cost_eth: Decimal
def normalize_rows(raw_csv: str, wallet: str) -> list[TxRow]:
"""Normalize explorer CSV rows for quick wallet activity summaries."""
rows: list[TxRow] = []
reader = csv.DictReader(io.StringIO(raw_csv))
for row in reader:
gas_used = Decimal(row["GasUsed"])
gas_price = Decimal(row["GasPrice"])
gas_cost_eth = gas_used * gas_price / Decimal("1e18")
value_eth = Decimal(row["Value_IN(ETH)"])
rows.append(
TxRow(
timestamp=row["Timestamp"],
tx_hash=row["Txhash"],
method=row["Method"],
counterparty=row["To"] if row["From"].lower() == wallet.lower() else row["From"],
value_eth=value_eth,
gas_cost_eth=gas_cost_eth,
)
)
return rows
if __name__ == "__main__":
wallet = "0xWallet"
txs = normalize_rows(RAW_CSV, wallet)
for tx in txs:
print(tx)
I use the gas_cost_eth field to flag transactions that look expensive relative to the wallet size. For example, if a $500 wallet burns 0.003 ETH on a failed swap, that transaction is worth investigating before repeating the flow.
Risk analysis: what can go wrong when reading traces
- Technical risk (trace gaps): Some explorers do not expose full traces for older blocks or rollups. That can hide intermediary calls and make a transaction look simpler than it was.
- Technical risk (proxy indirection): A contract might forward calls through a proxy, so the address you see in the outer call is not the implementation that ran.
- Economic risk (hidden fees): Internal calls sometimes reveal fee transfers to a treasury or partner. If you ignore traces, you can miss a meaningful slice of execution cost.
- Operational risk (RPC mismatches): Traces depend on the node implementation. A trace from a third-party node can disagree with a locally re-executed trace if it uses different tracing options. (Geth debug RPC)
Practical takeaways for small wallets
- Use the explorer’s normal transactions, token transfers, and internal trace tabs as a three-layer map of wallet behavior.
- Spot-check every new protocol flow with traces before repeating it at higher size.
- Normalize CSV exports so you can compare gas costs across chains and set a personal “too expensive” threshold for a sub-$1k wallet.
- If a transaction looks off, trust the trace and logs over the UI summary.