the-graph
The Graph decentralized indexing protocol — subgraph development (schema.graphql, AssemblyScript mappings, subgraph.yaml manifest), GraphQL queries, Subgraph Studio deployment, hosted service migration, indexing optimization, and Graph Client for type-safe queries.
npx @cutdnoise/add-skill the-graphSkill Assets (1)
- SKILL.md
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/