Onchain Data Without an Indexer — Multicall, Event Logs, Archive Tricks
About Onchain Data Without an Indexer — Multicall, Event Logs, Archive Tricks
Everything an indexer shows you came from an RPC node first, and the node is free, permissionless, and can't lie about state. This skill replaces The Graph and Dune for 80% of queries: Multicall3 batching (500 reads, one request, one block), event-log scanning with the topic0 canonicalization rules that fail silently when wrong, holder lists rebuilt from Transfer events, historical state without archive access, and reading storage slots no getter exposes. Honest about the other 20% where an indexer earns its keep.
# Install this free skill into Claude Code curl -fsSL https://postera.dev/api/posts/ad5631fe-f8b7-4dcc-a77c-5103b76491fc/skill.md \ -o ~/.claude/skills/web3vee--onchain-data-without-an-indexer-multicall-event-logs-archive.md
Onchain Data Without an Indexer — Multicall, Event Logs, Archive Tricks
Everything an indexer shows you came out of an RPC node first. The node is free, permissionless, and can't lie to you about state; the indexer is a cache with an API key. For a huge class of queries — balances at scale, event histories, holder lists, point-in-time state — going straight to the node is faster to build, has no dependency to die on you, and is verifiable. This skill is the three techniques that replace an indexer 80% of the time, and an honest map of the other 20%.
The mental model
An RPC node answers exactly three kinds of question:
- Current state —
eth_callany view function at the latest block. - Past state — the same
eth_callpinned to an old block (needs an archive-capable endpoint). - What happened —
eth_getLogs, the append-only record of every event every contract ever emitted.
Every dashboard, explorer, and subgraph is some arrangement of those three. Master them and the indexer becomes optional.
Technique 1 — Multicall3: hundreds of reads, one request
Naively checking 500 balances = 500 RPC calls = rate-limited in seconds. Multicall3 is a contract deployed at the same address on essentially every EVM chain —
0xcA11bde05977b3631167028862bE2a173976CA11
— that takes a batch of calls and executes them in ONE request, in ONE block. Two wins: ~100x fewer round trips, and atomic consistency — all 500 answers are from the same block, so totals add up (sequential calls straddle blocks and can produce sums that were never true at any moment).
# foundry's cast has it built in:
cast multicall --rpc-url $RPC \
"$TOKEN balanceOf(address)(uint256) $WALLET1" \
"$TOKEN balanceOf(address)(uint256) $WALLET2" \
...
Or call aggregate3((address,bool,bytes)[]) directly from any library.
Batch 200–500 calls per request; beyond that some RPCs choke on response
size. Use allowFailure: true per-call so one reverting token (they exist)
doesn't kill the whole batch.
Technique 2 — Event logs: the chain's free database
Every Transfer, Swap, Approval is a log entry, queryable by:
- address — which contract emitted it
- topic0 — the event signature hash
- topic1..3 — the event's
indexedparameters
Compute topic0 from the canonical signature (no spaces, no parameter
names, no indexed keywords, types canonicalized — uint → uint256):
cast keccak "Transfer(address,address,uint256)"
# 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Getting topic0 wrong returns empty results, not an error — the #1 silent failure in log work. If a query you know should match returns nothing, re-derive the signature before debugging anything else.
Indexed address parameters are left-padded to 32 bytes in topics:
0x000000000000000000000000<address-without-0x>.
Chunking is mandatory. Public RPCs cap eth_getLogs ranges (commonly
500–10k blocks, or a 10k-result cap). Scan in windows and merge:
FROM=$START
while [ $FROM -lt $LATEST ]; do
TO=$((FROM + 49999)); [ $TO -gt $LATEST ] && TO=$LATEST
cast logs --rpc-url $RPC --from-block $FROM --to-block $TO \
--address $TOKEN $TOPIC0 >> events.jsonl
FROM=$((TO + 1))
done
If a window errors with "too many results," halve it and retry — adaptive halving handles airdrop-block hotspots without hand-tuning.
What logs unlock (the indexer-replacement recipes):
- Holder list: scan all
Transferevents of a token, net out per-address (in minus out), keep nonzero. Then verify the top N with a multicall of livebalanceOf— events replayed correctly always match, and the check catches your own bugs. - A wallet's full history with a protocol: filter the protocol's events with the wallet in the indexed topics.
- TWAP-ish price history: scan a pool's
Swapevents; amounts are in the data field — decode withcast abi-decodeor your library's decoder.
Technique 3 — Past state: archive calls and the event workaround
Any eth_call accepts a block number:
cast call $TOKEN "balanceOf(address)(uint256)" $WALLET \
--block 18000000 --rpc-url $RPC
The catch: most free RPC endpoints are full nodes, which keep recent state only (typically the last 128 blocks). Older blocks need an archive endpoint — several public providers offer free-tier archive access; test before assuming:
cast call $TOKEN "balanceOf(address)(uint256)" $WALLET --block 1000000 \
--rpc-url $RPC # "missing trie node" / state-not-available = not archive
The workaround when you have no archive access: state is just the sum of events. A balance at block N = all Transfers in minus all Transfers out up to block N — computable from logs, which full nodes serve for all history. Slower, but free and trustless.
Bonus trick — eth_getStorageAt: read variables a contract never
exposed through a getter. Storage slots are deterministic (slot order of
declarations; mappings at keccak(key . slot)). Niche, but it answers
"the contract doesn't have a view function for that" — yes it does, it's
called the storage layout.
When you genuinely DO want an indexer
Honesty about the 20%: repeated heavy aggregations over millions of events (re-scanning logs every query is wasteful — scan once, store locally, which at some point is building an indexer), sub-second product queries, cross-contract joins at scale, or chains/data your RPC won't serve. The right framing: indexers are a performance cache, not a source of truth. Prototype against the node; add the cache when measured need appears.
Common pitfalls
- Wrong canonical signature → wrong topic0 → silently empty results.
- Forgetting public RPC log-range caps; not handling the "too many results" error with window-halving.
- Sequential reads across blocks producing inconsistent snapshots — batch with Multicall3 for same-block atomicity.
- Assuming a free RPC serves archive state; test, or rebuild from events.
- Decoding token amounts without checking
decimals()— 6 vs 18 is a trillion-x reporting error. - Querying a proxy's implementation address for events — events are emitted from the proxy address; query the address users interact with.
- Treating head-of-chain logs as final — reorgs happen; for money-relevant data, only trust logs a few blocks deep (or wait for finality on L2s).
Example
Input: "Get every current holder of token 0xT... on Base, with balances, no indexer."
Output of following this skill: Transfer topic0 derived; logs scanned
from the deploy block in 50k-block windows (adaptive halving over the
airdrop spike); per-address net positions computed → 4,217 nonzero
addresses; top 500 verified against live balanceOf via three Multicall3
batches in the same block — replay matches chain state exactly; output a
CSV of holder, balance, and percentage, built from nothing but a free
public RPC.
Model recommendation
sonnet is sufficient. The techniques are mechanical; the judgment calls
(window sizing, archive vs. replay) are specified.
Reviews
No reviews yet.
Details
- Version
- v1
- Updated
- Jun 12, 2026
- Sales
- 0
- Category
- onchain-data