The Graph
The Graph is a decentralized indexing protocol for querying blockchain data. Subgraphs define which smart contract events to index, how to transform them into entities, and expose them via a GraphQL API. The protocol supports Ethereum, Arbitrum, Optimism, Base, Polygon, Avalanche, BSC, Celo, Gnosis, and 40+ other networks.
What You Probably Got Wrong
-
Hosted service is DEPRECATED -- do not use
graph deploy --node https://api.thegraph.com/deploy/. Use Subgraph Studio exclusively. Hosted service endpoints stopped serving queries in Q2 2024. All documentation referencing--node https://api.thegraph.com/deploy/is outdated. -
Mappings are AssemblyScript, NOT TypeScript -- despite
.tsfile extensions, subgraph mappings compile to WebAssembly via AssemblyScript. This means: no closures, no union types, no optional chaining (?.), no nullish coalescing (??), noArray.map/filter/reduce, noJSON.parse, no async/await, no try/catch. If you write standard TypeScript, the build will fail with cryptic errors. -
graph-tstypes are NOT standard TS types --BigInt,BigDecimal,Bytes,Address, andethereum.Eventcome from@graphprotocol/graph-ts. They are NOTbigint,number, orUint8Array. You must useBigInt.fromI32(),BigDecimal.fromString(), andAddress.fromString()constructors. Arithmetic uses method calls:a.plus(b),a.minus(b),a.times(b),a.div(b). -
graph codegenmust run before build -- entities and contract bindings are auto-generated fromschema.graphqland ABIs. If you skip codegen, imports likeimport { Transfer } from '../generated/ERC20/ERC20'will fail. Always rungraph codegenafter ANY change to schema or ABIs. -
Entity IDs must be
BytesorString, not numeric -- the@entitydirective requires anidfield of typeID!which maps toBytesorStringin AssemblyScript. UsingBigIntorIntas entity ID causes schema validation failure. -
store.getreturns nullable --Entity.load(id)returnsEntity | null. You must null-check before accessing fields. AssemblyScript does not have optional chaining, so you need explicitif (entity != null)blocks. -
Subgraph Studio requires authentication per machine --
graph auth --studio <deploy-key>stores the key in~/.graph. This is per-machine, not per-project. CI/CD must re-auth on each run.
Core Packages
npm install --save-dev @graphprotocol/graph-cli @graphprotocol/graph-ts
| Package | Purpose | Min Version |
|---|---|---|
@graphprotocol/graph-cli |
CLI for init, codegen, build, deploy | 0.80.0 |
@graphprotocol/graph-ts |
AssemblyScript runtime library (types, store API) | 0.35.0 |
Subgraph Development Lifecycle
graph init --> graph codegen --> graph build --> graph deploy
1. Initialize a Subgraph
# Interactive init from a deployed contract
graph init --studio my-subgraph
# Non-interactive: specify all options
graph init --studio my-subgraph \
--protocol ethereum \
--network mainnet \
--contract-name MyContract \
--contract-address 0x1234567890abcdef1234567890abcdef12345678 \
--abi ./abis/MyContract.json \
--start-block 18000000
This generates:
subgraph.yaml-- manifestschema.graphql-- entity definitionssrc/my-contract.ts-- mapping stubs (AssemblyScript)abis/MyContract.json-- contract ABIpackage.jsonwith graph-cli and graph-ts
2. Define the Schema (schema.graphql)
Entities map to database tables. Each entity needs an id: ID! field.
type Token @entity {
id: Bytes!
name: String!
symbol: String!
decimals: Int!
totalSupply: BigInt!
holders: [TokenHolder!]! @derivedFrom(field: "token")
}
type TokenHolder @entity {
id: Bytes!
token: Token!
address: Bytes!
balance: BigInt!
lastTransferBlock: BigInt!
lastTransferTimestamp: BigInt!
}
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
value: BigInt!
token: Token!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
Schema rules:
@entitymarks a type as a stored entity@entity(immutable: true)for append-only entities (events) -- improves indexing speed significantly@derivedFrom(field: "token")creates a virtual reverse lookup without storing data- Supported scalar types:
ID,Bytes,String,Boolean,Int(i32),BigInt,BigDecimal Bytes!is preferred overString!for IDs derived from addresses or hashes -- it avoids hex encoding overhead
3. Write the Manifest (subgraph.yaml)
specVersion: 1.2.0
indexerHints:
prune: auto
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: ERC20
network: mainnet
source:
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
abi: ERC20
startBlock: 6082465
mapping:
kind: ethereum/events
apiVersion: 0.0.9
language: wasm/assemblyscript
entities:
- Token
- TokenHolder
- Transfer
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
- event: Approval(indexed address,indexed address,uint256)
handler: handleApproval
file: ./src/mapping.ts
Manifest rules:
specVersion: 1.2.0is the current specstartBlockshould be the contract deployment block -- indexing from block 0 wastes hours- Event signatures must match the ABI exactly, including
indexedkeywords indexerHints.prune: autoenables automatic pruning of historical entity versions to reduce disk usageapiVersion: 0.0.9is the current mapping API version
4. Write AssemblyScript Mappings
// src/mapping.ts -- this is AssemblyScript, NOT TypeScript
import { Transfer as TransferEvent } from "../generated/ERC20/ERC20";
import { Token, TokenHolder, Transfer } from "../generated/schema";
import { BigInt, Bytes, Address } from "@graphprotocol/graph-ts";
// Zero address constant -- reused across handlers
const ZERO_ADDRESS = Address.fromString(
"0x0000000000000000000000000000000000000000"
);
export function handleTransfer(event: TransferEvent): void {
// Create immutable Transfer entity (append-only, never updated)
let transfer = new Transfer(event.transaction.hash.concatI32(event.logIndex.toI32()));
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;
// Load or create Token entity
let token = Token.load(event.address);
if (token == null) {
token = new Token(event.address);
token.name = "";
token.symbol = "";
token.decimals = 0;
token.totalSupply = BigInt.fromI32(0);
}
transfer.token = token.id;
transfer.save();
token.save();
// Update sender balance (skip mint events where from == zero address)
if (event.params.from != ZERO_ADDRESS) {
let senderId = event.address.concat(event.params.from);
let sender = TokenHolder.load(senderId);
if (sender == null) {
sender = new TokenHolder(senderId);
sender.token = token.id;
sender.address = event.params.from;
sender.balance = BigInt.fromI32(0);
}
sender.balance = sender.balance.minus(event.params.value);
sender.lastTransferBlock = event.block.number;
sender.lastTransferTimestamp = event.block.timestamp;
sender.save();
}
// Update receiver balance (skip burn events where to == zero address)
if (event.params.to != ZERO_ADDRESS) {
let receiverId = event.address.concat(event.params.to);
let receiver = TokenHolder.load(receiverId);
if (receiver == null) {
receiver = new TokenHolder(receiverId);
receiver.token = token.id;
receiver.address = event.params.to;
receiver.balance = BigInt.fromI32(0);
}
receiver.balance = receiver.balance.plus(event.params.value);
receiver.lastTransferBlock = event.block.number;
receiver.lastTransferTimestamp = event.block.timestamp;
receiver.save();
}
}
5. Codegen and Build
# Generate types from schema.graphql and ABIs
graph codegen
# Compile AssemblyScript to WebAssembly
graph build
Common build errors:
ERROR TS2322: Type 'X | null' is not assignable to type 'X'-- null-check before useERROR TS2304: Cannot find name 'Transfer'-- rungraph codegenfirstWARNING: using deprecated apiVersion-- updateapiVersionin subgraph.yaml
6. Deploy to Subgraph Studio
# Authenticate (one-time per machine)
graph auth --studio <DEPLOY_KEY>
# Deploy with version label
graph deploy --studio my-subgraph --version-label v0.1.0
AssemblyScript Reference
Type System
| Graph Type | AssemblyScript Class | Constructor |
|---|---|---|
Bytes |
Bytes |
Bytes.fromHexString("0x..."), event.address |
BigInt |
BigInt |
BigInt.fromI32(0), BigInt.fromString("1000000") |
BigDecimal |
BigDecimal |
BigDecimal.fromString("1.5") |
Address |
Address |
Address.fromString("0x...") |
String |
string |
Standard string literal |
Int |
i32 |
Standard i32 literal |
Boolean |
boolean |
true / false |
BigInt Arithmetic
import { BigInt } from "@graphprotocol/graph-ts";
let a = BigInt.fromI32(100);
let b = BigInt.fromI32(50);
let sum = a.plus(b); // 150
let diff = a.minus(b); // 50
let product = a.times(b); // 5000
let quotient = a.div(b); // 2
let remainder = a.mod(b); // 0
let power = a.pow(2); // 10000
// Comparison
let isGreater = a.gt(b); // true
let isEqual = a.equals(b); // false
let isZero = a.isZero(); // false
BigDecimal Arithmetic
import { BigDecimal, BigInt } from "@graphprotocol/graph-ts";
let price = BigDecimal.fromString("1234.56");
let amount = BigDecimal.fromString("100");
let total = price.times(amount); // 123456.00
let divided = price.div(amount); // 12.3456
// Convert BigInt to BigDecimal for decimal math
let raw = BigInt.fromString("1000000000000000000"); // 1e18
let decimals = BigInt.fromI32(18);
let divisor = BigInt.fromI32(10).pow(decimals.toI32() as u8);
let normalized = raw.toBigDecimal().div(divisor.toBigDecimal()); // 1.0
Bytes Operations
import { Bytes, Address, ethereum } from "@graphprotocol/graph-ts";
// Create unique entity IDs from event data
let id = event.transaction.hash.concatI32(event.logIndex.toI32());
// Concatenate two Bytes values
let compositeId = event.address.concat(event.params.user);
// Convert Address to Bytes
let addr: Bytes = event.params.to;
// Hex string from Bytes
let hex = event.transaction.hash.toHexString();
Critical AssemblyScript Restrictions
These will cause build failures. No workaround exists:
// FORBIDDEN: closures / arrow functions as callbacks
// array.map(item => item.id) <-- WILL NOT COMPILE
// FORBIDDEN: union types
// let x: string | null <-- use nullable: string | null is OK only for class fields
// FORBIDDEN: optional chaining
// entity?.field <-- WILL NOT COMPILE
// FORBIDDEN: nullish coalescing
// entity ?? defaultValue <-- WILL NOT COMPILE
// FORBIDDEN: Array.map / filter / reduce
// Use a for loop instead:
let ids = new Array<string>();
for (let i = 0; i < items.length; i++) {
ids.push(items[i].id.toHexString());
}
// FORBIDDEN: JSON.parse
// Use graph-ts json module if available, or decode manually
// FORBIDDEN: try/catch
// Errors in handlers cause the subgraph to fail and halt indexing
// FORBIDDEN: async/await
// All handlers are synchronous
Nullable Field Patterns
import { Token } from "../generated/schema";
// Loading an entity returns nullable
let token = Token.load(id);
if (token == null) {
// Entity does not exist yet -- create it
token = new Token(id);
token.name = "Unknown";
token.symbol = "???";
token.decimals = 18;
token.totalSupply = BigInt.fromI32(0);
}
// Safe to use token here -- guaranteed non-null
token.totalSupply = token.totalSupply.plus(amount);
token.save();
Handler Types
Event Handlers (most common)
Triggered when a specific event is emitted. Fastest and most reliable.
# subgraph.yaml
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
- event: Approval(indexed address,indexed address,uint256)
handler: handleApproval
import { Transfer } from "../generated/ERC20/ERC20";
export function handleTransfer(event: Transfer): void {
// event.params contains decoded event parameters
let from = event.params.from;
let to = event.params.to;
let value = event.params.value;
// event.block contains block metadata
let blockNumber = event.block.number;
let timestamp = event.block.timestamp;
// event.transaction contains tx metadata
let txHash = event.transaction.hash;
let gasPrice = event.transaction.gasPrice;
}
Call Handlers
Triggered on function calls. Slower than event handlers. Not supported on all networks.
callHandlers:
- function: transfer(address,uint256)
handler: handleTransferCall
import { TransferCall } from "../generated/ERC20/ERC20";
export function handleTransferCall(call: TransferCall): void {
let to = call.inputs._to;
let value = call.inputs._value;
let success = call.outputs.value0; // return value
}
Block Handlers
Triggered on every block (or filtered blocks). Use sparingly -- very expensive.
blockHandlers:
- handler: handleBlock
filter:
kind: polling
every: 100
import { ethereum } from "@graphprotocol/graph-ts";
export function handleBlock(block: ethereum.Block): void {
let number = block.number;
let timestamp = block.timestamp;
let hash = block.hash;
}
GraphQL Query Patterns
Query endpoint: https://gateway.thegraph.com/api/{api-key}/subgraphs/id/{subgraph-id}
Basic Query
{
tokens(first: 10, orderBy: totalSupply, orderDirection: desc) {
id
name
symbol
totalSupply
}
}
Filtering with where
{
transfers(
where: {
value_gt: "1000000000000000000"
from: "0xabcdef1234567890abcdef1234567890abcdef12"
blockTimestamp_gte: "1700000000"
}
first: 100
orderBy: blockNumber
orderDirection: desc
) {
id
from
to
value
blockNumber
}
}
Filter suffixes:
field-- exact matchfield_not-- not equalfield_gt/field_gte-- greater than / greater or equalfield_lt/field_lte-- less than / less or equalfield_in/field_not_in-- in arrayfield_contains-- substring match (String only)field_starts_with/field_ends_with-- prefix/suffix match
Pagination
The Graph limits results to 1000 per query. For large datasets, paginate using first + skip or cursor-based pagination with id_gt.
# Skip-based (simple but slow for deep pages)
{
transfers(first: 100, skip: 200, orderBy: blockNumber) {
id
value
}
}
# Cursor-based (fast for any depth -- preferred)
{
transfers(
first: 1000
where: { id_gt: "0xlast_seen_id" }
orderBy: id
) {
id
from
to
value
}
}
Pagination limit: skip maxes out at 5000. For datasets beyond 5000, use cursor-based pagination with id_gt.
Time-Travel Queries
Query entity state at a specific block number.
{
tokens(block: { number: 18000000 }) {
id
name
totalSupply
}
}
Full-Text Search
Requires a @fulltext directive in the schema.
# schema.graphql
type _Schema_
@fulltext(
name: "tokenSearch"
language: en
algorithm: rank
include: [{ entity: "Token", fields: [{ name: "name" }, { name: "symbol" }] }]
)
{
tokenSearch(text: "USDC") {
id
name
symbol
}
}
Data Source Templates (Dynamic Contracts)
For factory patterns where new contracts are deployed at runtime (e.g., Uniswap pairs, lending pools).
# subgraph.yaml
templates:
- kind: ethereum
name: Pair
network: mainnet
source:
abi: Pair
mapping:
kind: ethereum/events
apiVersion: 0.0.9
language: wasm/assemblyscript
entities:
- Swap
abis:
- name: Pair
file: ./abis/Pair.json
eventHandlers:
- event: Swap(indexed address,uint256,uint256,uint256,uint256,indexed address)
handler: handleSwap
file: ./src/pair.ts
// In factory handler -- dynamically create a new data source
import { Pair as PairTemplate } from "../generated/templates";
export function handlePairCreated(event: PairCreated): void {
// Start indexing the new pair contract
PairTemplate.create(event.params.pair);
}
Contract Reads (eth_call in Mappings)
Read on-chain state from within a mapping handler.
import { ERC20 } from "../generated/ERC20/ERC20";
import { Address } from "@graphprotocol/graph-ts";
export function handleTransfer(event: TransferEvent): void {
// Bind to the contract at its address
let contract = ERC20.bind(event.address);
// try_ methods return ethereum.CallResult which has reverted flag
let nameResult = contract.try_name();
let symbolResult = contract.try_symbol();
let decimalsResult = contract.try_decimals();
let token = new Token(event.address);
// Always use try_ to handle contracts that revert on view calls
if (!nameResult.reverted) {
token.name = nameResult.value;
} else {
token.name = "Unknown";
}
if (!symbolResult.reverted) {
token.symbol = symbolResult.value;
} else {
token.symbol = "???";
}
if (!decimalsResult.reverted) {
token.decimals = decimalsResult.value;
} else {
token.decimals = 18;
}
token.save();
}
Contract read rules:
- Always use
try_prefixed methods -- non-try methods abort the handler on revert - Contract reads are
eth_calls at the handler's block -- they see state at that block - Reads are slow compared to event data -- minimize them
- Some contracts (proxies, non-standard ERC20s) revert on
name()orsymbol()-- always handle reverts
Indexing Performance Tips
Use Immutable Entities
Entities marked @entity(immutable: true) are append-only. The indexer skips update tracking, reducing storage I/O by up to 80% for high-volume event entities.
type Transfer @entity(immutable: true) {
id: Bytes!
from: Bytes!
to: Bytes!
value: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}
Use Bytes for Entity IDs
Bytes IDs are stored as raw bytes. String IDs require hex encoding/decoding on every load/save. For entities keyed by address or tx hash, Bytes is 2-3x faster.
Set startBlock Correctly
Never index from block 0. Set startBlock to the contract's deployment block or the block of the first relevant event.
# Find deployment block using cast
cast receipt <TX_HASH> --rpc-url $RPC_URL | grep blockNumber
Enable Pruning
indexerHints:
prune: auto
Prune removes historical entity versions. Subgraphs that do not need time-travel queries should enable pruning.
Minimize Contract Reads
Each try_* call is an RPC request during indexing. Cache values in entities instead of re-reading on every event.
// BAD: reads contract on every Transfer event
let name = contract.try_name();
// GOOD: read once, store in entity
let token = Token.load(event.address);
if (token == null) {
token = new Token(event.address);
let name = contract.try_name();
token.name = name.reverted ? "Unknown" : name.value;
}
Batch Entity IDs
Use event.transaction.hash.concatI32(event.logIndex.toI32()) for unique IDs per event within a transaction. This avoids string concatenation overhead.
Graph Client (Frontend Integration)
Type-safe GraphQL client for querying subgraphs from frontend or Node.js.
Installation
npm install @graphprotocol/client-cli graphql
npx graphclient init
Configuration (.graphclientrc.yml)
sources:
- name: MySubgraph
handler:
graphql:
endpoint: https://gateway.thegraph.com/api/{api-key}/subgraphs/id/{subgraph-id}
Usage in Application
import { execute } from "../.graphclient";
import { gql } from "graphql";
const GET_TOKENS = gql`
query GetTokens($first: Int!) {
tokens(first: $first, orderBy: totalSupply, orderDirection: desc) {
id
name
symbol
totalSupply
}
}
`;
async function fetchTokens(): Promise<void> {
const result = await execute(GET_TOKENS, { first: 10 });
if (result.errors) {
throw new Error(`Query failed: ${result.errors[0].message}`);
}
const tokens = result.data.tokens;
for (const token of tokens) {
console.log(`${token.symbol}: ${token.totalSupply}`);
}
}
Subgraph Composition (Multiple Data Sources)
Index multiple contracts in a single subgraph.
dataSources:
- kind: ethereum
name: USDC
network: mainnet
source:
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
abi: ERC20
startBlock: 6082465
mapping:
kind: ethereum/events
apiVersion: 0.0.9
language: wasm/assemblyscript
entities:
- Token
- Transfer
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
- kind: ethereum
name: WETH
network: mainnet
source:
address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
abi: ERC20
startBlock: 4719568
mapping:
kind: ethereum/events
apiVersion: 0.0.9
language: wasm/assemblyscript
entities:
- Token
- Transfer
abis:
- name: ERC20
file: ./abis/ERC20.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.ts
Both data sources share the same mapping file and entity types. The mapping code must handle both contracts.
Grafting (Resume from Existing Subgraph)
Deploy a new subgraph version that starts from an existing subgraph's indexed state instead of re-indexing from scratch.
features:
- grafting
graft:
base: QmExistingSubgraphDeploymentId
block: 18500000
Grafting rules:
baseis the deployment ID (Qm... hash) of the source subgraphblockis the block to graft from -- the new subgraph inherits all entity state at this block- Grafting is for development iteration -- production subgraphs should be indexed from scratch
- Grafted subgraphs cannot be published to the decentralized network
Common File Structure
my-subgraph/
abis/
ERC20.json
Factory.json
src/
mapping.ts # AssemblyScript event handlers
factory.ts # Factory pattern handlers
helpers.ts # Shared utility functions
generated/
schema.ts # Auto-generated from schema.graphql (do not edit)
ERC20/ERC20.ts # Auto-generated from ABI (do not edit)
schema.graphql # Entity definitions
subgraph.yaml # Manifest
package.json
tsconfig.json
Indexing Alternatives
The Graph is the standard for decentralized indexing, but it's not always the best fit. Consider alternatives for specific use cases.
When NOT to Use The Graph
- Small projects (<5 entity types, simple queries): Setup overhead exceeds benefit
- TypeScript-first teams: AssemblyScript mapping layer adds friction
- Real-time data (<2 second freshness): Subgraph indexing has inherent latency (block confirmation + indexing time)
- Complex joins/aggregations: GraphQL limitations make multi-entity analytics painful
- Rapid iteration: Subgraph deployment and syncing takes minutes to hours
Ponder
TypeScript-native indexing framework. Write handlers in TS (not AssemblyScript), get automatic GraphQL API, and iterate with hot reloading.
// ponder.config.ts
import { createConfig } from "@ponder/core";
import { http } from "viem";
import { ERC20Abi } from "./abis/ERC20";
export default createConfig({
networks: {
mainnet: { chainId: 1, transport: http(process.env.PONDER_RPC_URL_1) },
},
contracts: {
ERC20: {
network: "mainnet",
abi: ERC20Abi,
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
startBlock: 6_082_465,
},
},
});
// src/ERC20.ts — event handler in TypeScript (not AssemblyScript)
import { ponder } from "@/generated";
ponder.on("ERC20:Transfer", async ({ event, context }) => {
const { Account, Transfer } = context.db;
await Account.upsert({ id: event.args.from });
await Account.upsert({ id: event.args.to });
await Transfer.create({
id: event.log.id,
data: {
from: event.args.from,
to: event.args.to,
amount: event.args.value,
timestamp: Number(event.block.timestamp),
},
});
});
Why choose Ponder: 10-15x faster iteration (hot reload, no deploy wait), full TypeScript (no AssemblyScript learning curve), viem types, automatic GraphQL API, runs locally or self-hosted. Best for teams that want subgraph-like indexing without the AssemblyScript tax.
Dune Analytics
SQL-based blockchain analytics platform. Best for historical analysis, cross-protocol queries, and dashboards -- not real-time application backends.
-- Top USDC transfers in last 24 hours
SELECT
"from",
"to",
value / 1e6 AS usdc_amount,
block_time
FROM erc20_ethereum.evt_Transfer
WHERE contract_address = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
AND block_time > now() - interval '24 hours'
ORDER BY value DESC
LIMIT 20;
Why choose Dune: Pre-indexed data across all major chains, SQL interface, community dashboards, no infrastructure to manage. Not suitable for: real-time dApp backends (query latency 5-30s), programmatic API access requires paid plan.
Direct RPC + Multicall3
For simple read-heavy patterns, skip indexing entirely. Batch onchain reads with Multicall3.
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
// Batch multiple reads in a single RPC call
const results = await client.multicall({
contracts: [
{ address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: pool, abi: poolAbi, functionName: 'slot0' },
{ address: pool, abi: poolAbi, functionName: 'liquidity' },
],
});
Multicall3 is deployed at 0xcA11bde05977b3631167028862bE2a173976CA11 on 70+ chains (same address everywhere via CREATE2).
Why choose direct RPC: Zero infrastructure, real-time data, simple reads. Not suitable for: historical queries, event aggregation, complex entity relationships.
Decision Matrix
| Use Case | Recommended | Why |
|---|---|---|
| Production dApp backend | The Graph | Decentralized, reliable, GraphQL API |
| Rapid prototyping | Ponder | Hot reload, TypeScript, fast iteration |
| Analytics dashboard | Dune | SQL, pre-indexed, cross-protocol |
| Simple token balances | Multicall3 | Zero infra, real-time, trivial setup |
| Historical event aggregation | The Graph or Ponder | Both handle event indexing well |
| Cross-chain queries | Dune | Pre-indexed multi-chain data |
| Real-time price feeds | Direct RPC | Lowest latency |
References
- Subgraph Studio: https://thegraph.com/studio/
- Official docs: https://thegraph.com/docs/en/
- Graph Explorer (find existing subgraphs): https://thegraph.com/explorer
- AssemblyScript docs: https://www.assemblyscript.org/
- graph-ts API reference: https://thegraph.com/docs/en/developing/graph-ts/api/
- Supported networks: https://thegraph.com/docs/en/developing/supported-networks/