Picking a blockchain indexer is the first architectural decision most Ethereum developers get wrong. The Graph and its alternatives are genuinely excellent tools. They are also the wrong tool for roughly half the reads developers use them for, and that mismatch costs days of setup that should take minutes.
The skill is classifying the question before you choose the tool.
TL;DR
A blockchain indexer like The Graph processes past events and writes them to a queryable database: you need it for history and aggregations. For current state (balances, positions, pool prices), a direct contract query is faster to ship, cheaper to run, and always up-to-date. Match the tool to the question.
What a blockchain indexer actually does
An indexer is a background process that watches the chain, extracts data from transactions and events as they land, and writes it into a queryable store. Every Transfer that moves an ERC-20, every Swap that flows through a DEX, every LiquidationCall on Aave: if you define a handler for it, the indexer records it.
The Graph Protocol is the canonical open standard for this pattern. You write a subgraph: a manifest declaring which contracts to watch, a GraphQL schema defining the entities you want, and AssemblyScript mapping functions that transform raw event bytes into entity rows. Deploy it, and The Graph’s indexer network starts processing historical blocks and backfilling data. By the time you query, you’re hitting a pre-joined database at sub-100ms latency, not the chain directly.
The Graph is the dominant decentralized indexer, but it’s not the only option in 2026. Goldsky, SubQuery, and Ponder (now under the Monad Foundation) are managed and self-hosted alternatives with varying chain coverage and developer-experience trade-offs. All share the same architectural premise: process events once, query many times.
Two categories of blockchain read
Before choosing a data layer, classify the question:
Historical reads — “what happened over time”
- “All ERC-20 transfers for wallet X in the past 30 days”
- “Total swap volume through this pool since launch”
- “Every governance vote cast by address Y”
- “Which wallets held this token at block 19,000,000”
State reads — “what is true right now”
- “What is wallet X’s current USDC balance?”
- “What is the current tick in this Uniswap v3 pool?”
- “Is this Aave position above the liquidation threshold?”
- “What is the total supply of this token right now?”
Indexers solve the first category. The second category doesn’t need an index at all. The answer is sitting in contract storage, readable with a single eth_call.
This distinction matters because a lot of developers reach for an indexer out of habit, then spend three days writing subgraph mappings for a question that has a one-line answer on the RPC layer. If the phrase “right now” or “current” appears in the requirement, stop before opening the Graph documentation.
Reading current state: no indexer needed
For state reads, the pattern is direct: name the contract, write an expression, get a typed result back. No ABI files to manage, no subgraph schema, no GraphQL boilerplate.
Here are the three most common patterns, each validated against live contracts.
ERC-20 balance (single wallet)
chain: evm_ethereum
contracts: { usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 }
context: { wallet: sol_address }
expression: formatUnits(usdc.balanceOf(wallet), usdc.decimals())
Returns 5567.402493 for a given wallet: the decimal-scaled balance, tagged with block metadata, in one round. No ABI download, no manual decoder, no proxy check.
ERC-20 balances across a list of wallets
chain: evm_ethereum
contracts: { usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 }
context: { wallets: list<sol_address> }
expression: wallets.map(w, formatUnits(usdc.balanceOf(w), usdc.decimals()))
Pass an array of addresses in the wallets context value. Returns a typed list of balances. Under the hood, all balanceOf calls are batched into a single Multicall3 round, so one RPC request covers the whole list.
Uniswap v3 pool state
chain: evm_ethereum
contracts: { pool: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 }
expression: pool.slot0()
Returns the live slot0 struct:
{
"sqrtPriceX96": "1647022250302993738808583493622716",
"tick": "198852",
"observationIndex": "581",
"observationCardinality": "723",
"feeProtocol": "68",
"unlocked": true
}
tick encodes the current price. sqrtPriceX96 is the raw square-root price the AMM uses internally. Neither piece of information is historical. Both are live contract state. No subgraph required.
DeFi position health
chain: evm_base
contracts: { aave: 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 }
context: { wallet: sol_address }
expression: aave.getUserAccountData(wallet)
Returns the full position struct: healthFactor, totalCollateralBase, totalDebtBase, and available borrow capacity. Wire this into a cron script, an n8n node, or a GitHub Action and you have a liquidation monitor without touching an indexer.
Proxy resolution is automatic
Aave v3 on Base is an EIP-1967 proxy. A raw eth_call to the proxy address would need the implementation ABI to decode the return. evmquery resolves the implementation automatically, so you call getUserAccountData by name and get a decoded struct back.
When you actually need an indexer
Indexers are indispensable when the question involves events that have already been processed and are no longer visible in current contract state.
Transfer history. The Transfer event is emitted as tokens move. Once processed, the event lives only in historical logs. The contract itself only stores current balances. “All USDC transfers to my address in the past month” requires something that has indexed those logs. That’s The Graph, Goldsky, or your own eth_getLogs scraper.
Aggregate metrics. “Total volume through the USDC/ETH pool in the past 7 days” requires summing Swap events across thousands of transactions. The pool contract stores only the current liquidity and price. An indexer accumulates as it goes; a direct call cannot aggregate what was never tracked on-chain.
Ownership snapshots at a past block. “Who held this NFT at block 19,000,000” is a historical question. The contract today only knows the current owner. An indexer that recorded every Transfer event can reconstruct the state at any past block.
Event-driven pipelines. If your system reacts to on-chain events in near-real-time (liquidation bots, arbitrage watchers, settlement confirmations), an event-streaming indexer or eth_subscribe is the right surface. A polling eth_call loop is not.
The tell: if the phrase “over the past N days/blocks,” “all instances where,” or “at block N” appears in the requirement, reach for an indexer. If the phrase is “current,” “right now,” or “latest,” start with a direct read.
The real cost of a subgraph
It’s worth naming the setup cost explicitly, because it shapes whether the investment makes sense for your situation.
A subgraph requires:
- A
subgraph.yamlmanifest declaring data sources, start blocks, and event handlers. - A
schema.graphqldefining the entities and their relationships. - AssemblyScript mapping functions that transform each event into entity updates.
- A deploy step to the decentralized Graph Network (GRT billing) or a managed host.
- A sync wait: a new subgraph can take hours to days to backfill historical blocks.
For a well-defined, stable protocol (a subgraph tracking all Uniswap v3 swaps since deployment, for example), this one-time investment pays off quickly. For “I need the current price of a pool I deployed this morning,” it’s a week of overhead for a one-line query.
There’s also an ongoing maintenance tax: contract upgrades that emit new event signatures require subgraph updates and re-syncs. The Graph’s hosted service was deprecated in 2026, so teams now run on the decentralized network or a managed alternative like Goldsky or SubQuery, each with its own billing model to account for.
The decision table
| Question shape | Needs indexer? | Right tool |
|---|---|---|
| Current token balance | No | evmquery / direct eth_call |
| Current pool price or tick | No | evmquery |
| Current DeFi position health | No | evmquery |
| Full transfer history | Yes | The Graph / Goldsky |
| Token holders at a past block | Yes | The Graph / Goldsky |
| Aggregate protocol volume | Yes | The Graph / Goldsky |
| Multi-wallet balance snapshot | No | evmquery (Multicall3 batch) |
| Event stream (real-time) | Yes | eth_subscribe or indexer |
| Contract read without ABI | No | evmquery (auto-resolved) |
One more rule of thumb: if the data changes with every new block (price, balance, health factor), it’s a state read. If it accumulates over time (history, aggregations, event counts), it’s an indexer job.
Using both in the same stack
The choice isn’t binary. Many production stacks use both layers in parallel:
- Indexer for history — a Goldsky or The Graph subgraph tracks all protocol events and exposes them via GraphQL.
- Query layer for current state — evmquery handles live balances, positions, and prices without rebuilding the index every time you add a new contract read.
This split matches tool semantics to query semantics. The indexer handles the “what happened” question reliably. The query layer handles the “what is true now” question without any sync delay.
For AI agent builders, the split also keeps tool budgets lean: MCP tools that read current state cost 1-3 credits each; tools that query indexed history against a GraphQL API can be expensive depending on data volume. Keeping current-state reads in evmquery and historical reads in a subgraph means each tool does what it was designed for.
If you’re building something that only needs current state (a monitoring script, a DeFi dashboard, a Claude tool that checks your Aave health factor), skip the indexer entirely. You’ll ship in an afternoon instead of a week.
Next steps
- Not sure which contracts to read? The developer overview has the full expression language reference and chain list.
- Building a multi-wallet balance dashboard? The Multicall3 guide covers the batching primitive evmquery uses under the hood.
- Want to turn any of these reads into an automated alert? The n8n integration post shows how to wire up contract reads to Slack, Discord, or any webhook. No indexer required.