Installs into .claude/skills of the current project.
Are you the author of Ethskills Indexing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cyotee-ethskills-indexing)
---
name: ethskills-indexing
description: How to read and query onchain data — events, The Graph, indexing patterns. Why you cannot just loop through blocks, and what to use instead.
---
# Onchain Data & Indexing
## What You Probably Got Wrong
**You try to query historical state via RPC calls.** You can't cheaply read past state. `eth_call` reads current state. Reading state at a historical block requires an archive node (expensive, slow). For historical data, you need an indexer.
**You loop through blocks looking for events.** Scanning millions of blocks with `eth_getLogs` is O(n) — it will timeout, get rate-limited, or cost a fortune in RPC credits. Use an indexer that has already processed every block.
**You store query results onchain.** Leaderboards, activity feeds, analytics — these belong offchain. Compute offchain, index events offchain. If you need an onchain commitment, store a hash.
**You don't know about The Graph.** The Graph turns your contract's events into a queryable GraphQL API. It's how every serious dApp reads historical data. Etherscan uses indexers. Uniswap uses indexers. So should you.
**You treat events as optional.** Events are THE primary way to read historical onchain activity. If your contract doesn't emit events, nobody can build a frontend, dashboard, or analytics on top of it. Design contracts event-first.
---
## Events Are Your API
Solidity events are cheap to emit (~375 gas base + 375 per indexed topic + 8 gas per byte of data) and free to read offchain. They're stored in transaction receipts, not in contract storage, so they don't cost storage gas.
### Design Contracts Event-First
Every state change should emit an event. This isn't just good practice — it's how your frontend, indexer, and block explorer know what happened.
```solidity
// ✅ Good — every action emits a queryable event
contract Marketplace {
event Listed(
uint256 indexed listingId,
address indexed seller,
address indexed tokenContract,
uint256 tokenId,
uint256 price
);
event Sold(uint256 indexed listingId, address indexed buyer, uint256 price);
event Cancelled(uint256 indexed listingId);
function list(address token, uint256 tokenId, uint256 price) external {
uint256 id = nextListingId++;
listings[id] = Listing(msg.sender, token, tokenId, price, true);
emit Listed(id, msg.sender, token, tokenId, price);
}
function buy(uint256 listingId) external payable {
// ... transfer logic ...
emit Sold(listingId, msg.sender, msg.value);
}
}
```
**Index the fields you'll filter by.** You get 3 indexed topics per event. Use them for addresses and IDs that you'll query — `seller`, `buyer`, `tokenContract`, `listingId`. Don't index large values or values you won't filter on.
### Reading Events Directly (Small Scale)
For recent events or low-volume contracts, you can read events directly via RPC:
```typescript
import { createPublicClient, http, parseAbiItem } from 'viem';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
// Get recent events (last 1000 blocks)
const logs = await client.getLogs({
address: '0xYourContract',
event: parseAbiItem('event Sold(uint256 indexed listingId, address indexed buyer, uint256 price)'),
fromBlock: currentBlock - 1000n,
toBlock: 'latest',
});
```
**This works for:** Last few thousand blocks, low-volume contracts, real-time monitoring.
**This breaks for:** Historical queries, high-volume contracts, anything scanning more than ~10K blocks.
---
## The Graph (Subgraphs)
The Graph is a decentralized indexing protocol. You define how to process events, deploy a subgraph, and get a GraphQL API that serves historical data instantly.
### When to Use The Graph
- Any dApp that needs historical data (activity feeds, transaction history)
- Leaderboards, rankings, analytics dashboards
- NFT collection browsers (who owns what, transfer history)
- DeFi dashboards (position history, PnL tracking)
- Any query that would require scanning more than ~10K blocks
### How It Works
1. **Define a schema** — what entities you want to query
2. **Write mappings** — TypeScript handlers that process events into entities
3. **Deploy** — subgraph indexes all historical events and stays synced
### Example: NFT Collection Subgraph
**schema.graphql:**
```graphql
type Token @entity {
id: ID!
tokenId: BigInt!
owner: Bytes!
mintedAt: BigInt!
transfers: [Transfer!]! @derivedFrom(field: "token")
}
type Transfer @entity {
id: ID!
token: Token!
from: Bytes!
to: Bytes!
timestamp: BigInt!
blockNumber: BigInt!
}
```
**mapping.ts:**
```typescript
import { Transfer as TransferEvent } from './generated/MyNFT/MyNFT';
import { Token, Transfer } from './generated/schema';
export function handleTransfer(event: TransferEvent): void {
let tokenId = event.params.tokenId.toString();
// Create or update token entity
let token = Token.load(tokenId);
if (token == null) {
token = new Token(tokenId);
token.tokenId = event.params.tokenId;
token.mintedAt = event.block.timestamp;
}
token.owner = event.params.to;
token.save();
// Create transfer record
let transfer = new Transfer(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
);
transfer.token = tokenId;
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.timestamp = event.block.timestamp;
transfer.blockNumber = event.block.number;
transfer.save();
}
```
**Query the subgraph:**
```graphql
{
tokens(where: { owner: "0xAlice..." }, first: 100) {
tokenId
mintedAt
transfers(orderBy: timestamp, orderDirection: desc, first: 5) {
from
to
timestamp
}
}
}
```
### Deploying a Subgraph
```bash
# Install
npm install -g @graphprotocol/graph-cli
# Initialize from contract ABI
graph init --studio my-subgraph
# Generate types from schema
graph codegen
# Build
graph build
# Deploy to Subgraph Studio
graph deploy --studio my-subgraph
```
**Subgraph Studio** (studio.thegraph.com) — development and testing environment. Free during development. Publish to the decentralized network for production.
---
## Alternative Indexing Solutions
| Solution | Best for | Tradeoffs |
|----------|----------|-----------|
| **The Graph** | Production dApp backends, decentralized | GraphQL API, requires subgraph development |
| **Dune Analytics** | Dashboards, analytics, ad-hoc queries | SQL interface, great visualization, not for app backends |
| **Alchemy/QuickNode APIs** | Quick token/NFT queries | `getTokenBalances`, `getNFTs`, `getAssetTransfers` — fast but centralized |
| **Etherscan/Blockscout APIs** | Simple event log queries | Rate-limited, not for high-volume |
| **Ponder** | TypeScript-first indexing | Local-first, simpler than The Graph for single-app use |
| **Direct RPC** | Real-time current state only | Only for current state reads, not historical |
### Dune Analytics
Write SQL queries over decoded onchain data. Best for analytics and dashboards, not for app backends.
```sql
-- Top 10 buyers on your marketplace (last 30 days)
SELECT
buyer,
COUNT(*) as purchases,
SUM(price / 1e18) as total_eth_spent
FROM mycontract_ethereum.Marketplace_evt_Sold
WHERE evt_block_time > NOW() - INTERVAL '30' DAY
GROUP BY buyer
ORDER BY total_eth_spent DESC
LIMIT 10
```
### Enhanced Provider APIs
For common queries, provider APIs are faster than building a subgraph:
```typescript
// Alchemy: get all tokens held by an address
const balances = await alchemy.core.getTokenBalances(address);
// Alchemy: get all NFTs owned by an address
const nfts = await alchemy.nft.getNftsForOwner(address);
// Alchemy: get transfer history
const transfers = await alchemy.core.getAssetTransfers({
fromAddress: address,
category: ['erc20', 'erc721'],
});
```
---
## Reading Current State (Not Historical)
For current balances, allowances, and contract state, direct RPC reads are fine. No indexer needed.
### Single Reads
```typescript
import { createPublicClient, http } from 'viem';
const client = createPublicClient({ chain: mainnet, transport: http() });
// Read current balance
const balance = await client.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
});
```
### Batch Reads with Multicall
For multiple reads in one RPC call, use Multicall3 (deployed at the same address on every chain):
```typescript
// Multicall3: 0xcA11bde05977b3631167028862bE2a173976CA11
// Same address on Ethereum, Arbitrum, Optimism, Base, Polygon, and 50+ chains
const results = await client.multicall({
contracts: [
{ address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: tokenC, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: vault, abi: vaultAbi, functionName: 'totalAssets' },
],
});
// One RPC call instead of four
```
### Real-Time Updates
For live updates, subscribe to new events via WebSocket:
```typescript
import { createPublicClient, webSocket } from 'viem';
const client = createPublicClient({
chain: mainnet,
transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
});
// Watch for new sales in real-time
const unwatch = client.watchContractEvent({
address: marketplaceAddress,
abi: marketplaceAbi,
eventName: 'Sold',
onLogs: (logs) => {
for (const log of logs) {
console.log(`Sale: listing ${log.args.listingId} for ${log.args.price}`);
}
},
});
```
---
## Common Patterns
| What you need | How to get it |
|---------------|---------------|
| Activity feed for a dApp | Emit events → index with The Graph → query via GraphQL |
| Token balances for a user | Alchemy `getTokenBalances` or Multicall |
| NFT collection browser | The Graph subgraph or Alchemy `getNftsForContract` |
| Price history | Dune Analytics or DEX subgraphs |
| Real-time new events | WebSocket subscription via viem |
| Historical transaction list | The Graph or Alchemy `getAssetTransfers` |
| Dashboard / analytics | Dune Analytics (SQL + charts) |
| Protocol TVL tracking | DeFiLlama API or custom subgraph |