Introduction
The Graph is the essential decentralized indexing protocol for Web3 dApps in 2026. It lets you query blockchain data via GraphQL, outperforming slow RPCs for complex queries like Uniswap swaps or ERC20 transfers. Why use it? Imagine querying 1M+ events in milliseconds instead of hours of scanning.
This advanced tutorial guides you step-by-step to index Transfer events from an ERC20 token on Sepolia (address: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238). We cover CLI installation, GraphQL schema, AssemblyScript mappings, local build with Docker, playground queries, and deployment to Subgraph Studio. Result: a production-ready queryable GraphQL endpoint. Ideal for DeFi dashboards or NFT analytics. Estimated time: 2 hours for an experienced dev.
Prerequisites
- Node.js 20+ and Yarn 1.22+
- Docker and Docker Compose (for local graph-node)
- Alchemy or Infura API key (Sepolia RPC)
- Subgraph Studio account (free at thegraph.com/studio)
- Advanced knowledge of Solidity, GraphQL, and TypeScript
- Git installed
Install the The Graph CLI
npm install -g @graphprotocol/graph-cli
yarn global add @graphprotocol/graph-cli
graph --versionInstall the official CLI via npm or Yarn to handle init, codegen, and deploy. Verify with graph --version (should show 0.38+ in 2026). Avoid outdated global versions; use npx if there are conflicts.
Initialize the Subgraph Project
Create a new directory and initialize the boilerplate. This generates subgraph.yaml, schema.graphql, and src/mapping.ts. We're targeting Sepolia (chainId 11155111) and our ERC20 at 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238.
Initialize the Subgraph
mkdir erc20-subgraph && cd erc20-subgraph
graph init --studio erc20-subgraph
# Follow the prompts:
# - Subgraph name: erc20-sepolia
# - Directory: .
# - Ethereum network: sepolia
# - Contract address: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
# - Contract ABI: (copy from Etherscan or Alchemy)The init sets up the base manifest and downloads the ABI. Provide the exact address of the ERC20 contract deployed on Sepolia. If ABI is missing, export it from Sepolia Etherscan.
Define the GraphQL Schema
Analogy: The schema is the blueprint for your decentralized database, like a SQL table but queryable with GraphQL. We define Transfer with entities for from/to/value/block.
Complete GraphQL Schema
type Transfer @entity {
id: ID!
from: Bytes! # hex address
to: Bytes!
value: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
type Account @entity {
id: ID!
totalTransfers: BigInt!
totalSent: BigInt!
totalReceived: BigInt!
}
type Token @entity {
id: ID!
totalSupply: BigInt!
totalTransfers: BigInt!
}Defines three entities: Transfer (primary events), Account (aggregates by address), Token (global metadata). Use BigInt for Solidity uint256. @entity enables automatic indexing.
Subgraph Manifest (subgraph.yaml)
specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum/contract
name: ERC20
network: sepolia
source:
address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
abi: ERC20
startBlock: 5000000 # approx deployment block
mapping:
kind: ethereum/events
apiVersion: 0.0.8
language: wasm/assemblyscript
entities:
- Transfer
- Account
- Token
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.tsThe manifest links the schema, ABI, and mappings. startBlock optimizes indexing (find it on Etherscan). apiVersion 0.0.8 supports the latest WASM features. Download ABI to ./abis/.
Write AssemblyScript Mappings
Mappings process blockchain events into entities. handleTransfer creates/updates Transfer, increments Account/Token counters. Use ctx.Token.createOrUpdate for idempotency.
Complete Mappings (mapping.ts)
import { Transfer as TransferEvent } from "../generated/ERC20/ERC20";
import { Transfer, Account, Token } from "../generated/schema";
import { Address, BigInt, log } from "@graphprotocol/graph-ts";
export function handleTransfer(event: TransferEvent): void {
let transferId = event.transaction.hash.toHex() + "-" + event.logIndex.toString();
let transfer = new Transfer(transferId);
transfer.from = event.params.from.toHex();
transfer.to = event.params.to.toHex();
transfer.value = event.params.value;
transfer.blockNumber = event.block.number;
transfer.blockTimestamp = event.block.timestamp;
transfer.transactionHash = event.transaction.hash;
transfer.save();
let tokenId = "current";
let token = Token.load(tokenId);
if (token == null) {
token = new Token(tokenId);
token.totalSupply = BigInt.fromI32(0);
}
token.totalTransfers = token.totalTransfers.plus(BigInt.fromI32(1));
token.save();
let fromId = event.params.from.toHex();
let fromAccount = Account.load(fromId);
if (fromAccount == null) {
fromAccount = new Account(fromId);
fromAccount.totalSent = BigInt.fromI32(0);
fromAccount.totalReceived = BigInt.fromI32(0);
fromAccount.totalTransfers = BigInt.fromI32(0);
}
fromAccount.totalSent = fromAccount.totalSent.plus(event.params.value);
fromAccount.totalTransfers = fromAccount.totalTransfers.plus(BigInt.fromI32(1));
fromAccount.save();
let toId = event.params.to.toHex();
let toAccount = Account.load(toId);
if (toAccount == null) {
toAccount = new Account(toId);
toAccount.totalSent = BigInt.fromI32(0);
toAccount.totalReceived = BigInt.fromI32(0);
toAccount.totalTransfers = BigInt.fromI32(0);
}
toAccount.totalReceived = toAccount.totalReceived.plus(event.params.value);
toAccount.totalTransfers = toAccount.totalTransfers.plus(BigInt.fromI32(1));
toAccount.save();
}Generates a unique ID per log. load/create ensures atomic upserts. Increments aggregates for analytics (e.g., totalSent). Log with log.info for debugging. Test with real events post-5000000.
Generate Types and Build
graph codegen
graph build
ls generated/ # check erc20/ERC20.tsCodegen generates TS types from ABI/schema (e.g., TransferEvent). Build compiles to WASM. Common error: malformed ABI; validate JSON.
Local Deployment with Docker
Local setup: Run graph-node to test without costs. Provide your Alchemy Sepolia RPC in docker-compose.yml.
Docker Compose for graph-node
version: '3.6'
services:
postgres:
image: postgres
environment:
POSTGRES_DB: subgraph
POSTGRES_USER: subgraph
POSTGRES_PASSWORD: subgraph
ports:
- "5432:5432"
graph-node:
image: semaphoreui/graph-node:v0.38.0
depends_on: [postgres]
environment:
postgres_host: postgres
postgres_db: subgraph
postgres_user: subgraph
postgres_pass: subgraph
ethereum: sepolia https://eth-sepolia.g.alchemy.com/v2/VOTRE_CLE_API
GRAPH_LOG: info
ports:
- "8020:8020"
- "8000:8000"
- "8001:8001"
indexer-agent:
image: semaphoreui/indexer-agent:v0.6.0
depends_on: [graph-node]
environment:
GRAPH_NODE: http://graph-node:8020
INDEXER: http://indexer:80Replace VOTRE_CLE_API with your Alchemy key. Ports: 8000/GraphQL, 8020/admin, 8001/query. Use up -d for background.
Deploy and Query Locally
docker-compose up -d
graph create-local --node http://localhost:8020/ erc20-sepolia
graph deploy --version-label v0.1.0 --node http://localhost:8020/ --ipfs http://localhost:5001 erc20-sepolia
# Query playground
curl -X POST -H 'Content-Type: application/json' --data '{"query": "{ transfers(first:5, orderBy: blockTimestamp, orderDirection: desc) { id from to value } }"}' http://localhost:8000/subgraphs/name/erc20-sepoliaCreates the local subgraph, deploys the WASM build. Query via curl or localhost:8001. Wait for sync (check logs with docker logs graph-node).
Deployment to Subgraph Studio
Authenticate and deploy to the hosted service, then migrate to decentralized.
Deploy to Studio
graph auth --studio VOTRE_ACCESS_TOKEN
# Get it from thegraph.com/studio
graph subgraph create --studio erc20-sepolia
graph deploy --studio erc20-sepolia
# Endpoint: https://api.studio.thegraph.com/query/XXXXXX/erc20-sepolia/v0.1.0Token from Studio dashboard. First deploy indexes; subsequent ones update. Endpoint is publicly queryable. For decentralized: graph deploy --network mainnet after indexing.
Best Practices
- Precise StartBlock: Use Etherscan to minimize sync time (e.g., 5000000 for our contract).
- Paginated Indexing: Add
skip/firstin queries; limit to 1000 entities/page. - Denormalized Aggregates: Store totalSent in Account to avoid expensive joins.
- Unit Tests: Use
matchstick-asto mock events. - Monitoring: Watch fatal errors via Studio dashboard; retry with versioning.
Common Errors to Avoid
- Incomplete ABI: Forgetting
Transferevent → mappings crash. Always copy full ABI. - Non-unique ID: tx.hash + logIndex prevents duplicates during reorgs.
- BigInt Overflow: AssemblyScript handles it natively; avoid JS Number.
- Stuck Sync: RPC rate-limit; upgrade to Alchemy Pro or use snapshots.
Next Steps
- Official docs: The Graph Academy
- Advanced example: Index Uniswap V3 here.
- Migrate to L2 (Optimism/Base) by changing
network. - Check our Learni Web3 training courses for The Graph + IPFS masterclasses.