Skip to content
Learni
View all tutorials
Web3

How to Create a Subgraph with The Graph in 2026

Lire en français

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

terminal
npm install -g @graphprotocol/graph-cli
npm install -g @graphprotocol/graph-node
npm install -g @graphprotocol/graph-ts

Graph 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

terminal
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

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.ts

This 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

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

terminal
graph codegen
graph build

graph 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

src/erc20.ts
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

abis/ERC20.json
{"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

terminal
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 test before deploy.
  • Migrate to decentralized: After Hosted, move to L2/Substreams in 2026.

Common Errors to Avoid

  • Missing/invalid ABI: graph codegen fails → 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).