Introduction
The Graph is a decentralized protocol that indexes and organizes blockchain data like Ethereum, enabling ultra-fast GraphQL queries. Think of it as Google for the blockchain: instead of scanning millions of blocks, you query an optimized subgraph.
Why use it in 2026? With the rise of DeFi and NFT dApps, on-chain data is exploding. Subgraphs avoid slow, expensive RPC nodes, making your apps scalable. This beginner tutorial walks you through creating a subgraph that indexes ERC20 token transfers (e.g., USDC). You'll end up with 100% functional, deployable code on The Graph's Hosted Service. At the end, you'll query your data in real time. Time: 30 min. Ready to supercharge your Web3 projects?
Prerequisites
- Node.js 18+ and npm/yarn installed
- Free account on The Graph Hosted Service
- Subgraph API key (create one after signing up)
- Basic JavaScript/TypeScript knowledge
- MetaMask or Infura for ABI (optional, we'll use a public example)
Install Graph CLI
npm install -g @graphprotocol/graph-cli
npm install -g @graphprotocol/graph-node
npm install -g @graphprotocol/graph-tsGraph CLI is the official command-line tool for creating, building, and deploying subgraphs. Global installation lets you run graph anywhere. Verify with graph --version: should show 0.38+. Avoid older versions for 2026 compatibility.
Create the Subgraph Project
Create a project folder and initialize the skeleton. We'll target ERC20 transfers from the USDC contract (0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 on Ethereum Mainnet). This will index all Transfer events.
Initialize the Subgraph
mkdir erc20-transfers-subgraph
cd erc20-transfers-subgraph
graph init --studio erc20-transfers
# Suivez les prompts :
# Network: mainnet
# Contract: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
# ABI: Copiez depuis Etherscan ou fournissez un fichier local
# Start Block: 12287507 (premier bloc USDC)graph init generates the structure: subgraph.yaml, schema.graphql, abis/ and src/ folders. Specify the USDC contract to auto-generate the ABI. The start block optimizes indexing by skipping empty blocks. Pitfall: without a valid ABI, the build will fail.
Configure subgraph.yaml
specVersion: 0.0.5
graphqlSchema: ./schema.graphql
schemaFile: ./schema.graphql
packageManager: npm
source:
abi: ERC20
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
startBlock: 12287507
network: mainnet
dataSources:
- kind: ethereum/contract
name: ERC20
source:
abi: ERC20
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
startBlock: 12287507
mapping:
entities:
- Transfer
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/erc20.tsThis YAML file defines the Ethereum contract source, events to listen for (Transfer), and the mapping handler. specVersion: 0.0.5 is standard in 2026. The startBlock speeds up indexing. Pitfall: use checksum address (mixed case) to avoid resolution errors.
Define the GraphQL Schema
The schema describes the entities to index, like a relational DB in GraphQL. We'll store each Transfer with from/to/value/block details.
Write schema.graphql
type Transfer @entity {
id: ID!
from: Bytes! # address
to: Bytes! # address
value: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
type ERC20 @entity {
id: ID!
totalSupply: BigInt!
symbol: String!
name: String!
}Transfer and ERC20 types are persistent entities. Use Bytes for addresses/hex, BigInt for uint256 (values > JS Number). @entity enables indexing. After changes, run graph codegen. Pitfall: forget ! on non-nullable fields, and queries will fail.
Generate Code and ABI
graph codegen
graph buildgraph codegen generates TypeScript types in generated/ from the schema and ABI. graph build compiles mappings to WASM. Check build/subgraph.sol.json for no errors. Essential before deployment.
Implement the Mappings
Mappings in AssemblyScript process blockchain events. Like hooks: on each Transfer, we save the entity.
Write the erc20.ts Mapping
import { Transfer as TransferEvent } from "../generated/ERC20/ERC20";
import { Transfer, ERC20 } from "../generated/schema";
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
export function handleTransfer(event: TransferEvent): void {
let transfer = new Transfer(event.transaction.hash.toHex() + "-" + event.logIndex.toString());
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.value = event.params.value;
transfer.blockNumber = event.block.number;
transfer.blockTimestamp = event.block.timestamp;
transfer.transactionHash = event.transaction.hash;
transfer.save();
// Update ERC20 entity
let token = ERC20.load("current");
if (token == null) {
token = new ERC20("current");
token.symbol = "USDC";
token.name = "USD Coin";
token.totalSupply = BigInt.fromI32(0);
}
token.save();
}On each Transfer event, create a Transfer entity with a unique ID (tx hash + log index). Update the ERC20 entity. save() persists to IPFS/Indexer. AssemblyScript is strict: always use BigInt for large numbers. Pitfall: duplicate IDs cause overwrites.
Download ERC20 ABI
{"abiVersion":2,"version":"erc20:1.0.0","types":{"TransferEvent":{"fields":[{"name":"from","type":"address","indexed":true},{"name":"to","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}]},"ApprovalEvent":{"fields":[{"name":"owner","type":"address","indexed":true},{"name":"spender","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}]},"TransferResult":{"type":"boolean"},"ApprovalResult":{"type":"boolean"},"TransferResultWithReturnData":{"data":{"type":"bytes"},"success":{"type":"boolean"}},"ApprovalResultWithReturnData":{"data":{"type":"bytes"},"success":{"type":"boolean"}}},"functions":[{"name":"allowance","inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"name":"approve","inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"TransferResult"}]},{"name":"balanceOf","inputs":[{"name":"account","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint8"}]},{"name":"name","inputs":[],"outputs":[{"name":"","type":"string"}]},{"name":"symbol","inputs":[],"outputs":[{"name":"","type":"string"}]},{"name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"name":"transfer","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"TransferResult"}]},{"name":"transferFrom","inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"TransferResult"}]}],
"events":["TransferEvent","ApprovalEvent"]}The ABI defines the ERC20 contract's events and functions. Download it from Etherscan (USDC) and place in abis/ERC20.json. Standard IPFS JSON format. Without it, mappings won't recognize params. Pitfall: truncated ABI = ignored events.
Deploy the Subgraph
graph auth --studio YOUR_API_KEY
graph deploy --studio erc20-transfers
# Ou pour Hosted Service:
graph deploy --service-name mainnet YOUR_SUBGRAPH_ID --ipfs https://api.thegraph.com/ipfs/Authenticate with your API key. graph deploy builds, uploads to IPFS, and deploys to Hosted Service. Replace YOUR_API_KEY and YOUR_SUBGRAPH_ID. Monitor the dashboard for sync. Time: 5-10 min for 1M+ transfers. Pitfall: forget auth = 401 error.
Query the Subgraph
Once deployed, query via GraphQL Playground: https://api.studio.thegraph.com/query/YOUR_ID/erc20-transfers/version/latest. Example query:
graphql
query {
transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
id
from
to
value
blockTimestamp
}
}
Result: JSON with the 10 latest USDC transfers.
Best Practices
- Start with a recent startBlock: Reduces indexing from days to minutes.
- Use composite IDs: tx.hash + logIndex avoids duplicates.
- Limit entities: Index only necessary events to scale.
- Test locally: Run
graph testbefore deploy. - Migrate to decentralized: After Hosted, move to L2/Substreams in 2026.
Common Errors to Avoid
- Missing/invalid ABI:
graph codegenfails → Always download from a reliable source. - BigInt vs Number: Overflow on values > 2^53 → Always use
BigInt.fromString(). - No .save(): Entities not persisted → Check every handler.
- StartBlock too low: Endless indexing → Aim > 10M for mainnet.
Next Steps
- Official docs: The Graph Docs
- Advanced example: Uniswap V3 subgraphs
- Learni Web3 Training: Master Solidity + The Graph in 20h.
- Tools: SubQuery (alternative), Goldsky (hosted).