Subgraph Development for The Graph
The problem faced by all dApp developers: smart contracts don't store state history in a query-friendly format. eth_getLogs with event filtering is a crude tool: no sorting, no aggregation, no relationships between events from different contracts. As a result, the frontend either fetches tons of data and processes it on the client, or the team runs its own indexing backend. The Graph solves this problem in a standard way — you describe what to index, and the network does it for you.
A subgraph is essentially a declaration: which contracts to listen to, which events to process, how to transform data into entities. Writing it correctly the first time is harder than it seems.
Subgraph Architecture and Typical Mistakes
Project Structure
A subgraph consists of three parts: subgraph.yaml (manifest), schema.graphql (data model), AssemblyScript handlers (transformation logic). The manifest is the most important place:
dataSources:
- kind: ethereum
name: UniswapV3Pool
network: mainnet
source:
address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"
abi: UniswapV3Pool
startBlock: 12369621 # contract deployment block — mandatory
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Pool
- Swap
abis:
- name: UniswapV3Pool
file: ./abis/UniswapV3Pool.json
eventHandlers:
- event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
handler: handleSwap
startBlock — critical parameter. If you specify 0, indexing will run from the genesis block, synchronization will take days. Always specify the contract deployment block. Find it via Etherscan, "Contract Creation" field.
Schema Design: Think in GraphQL Queries
The schema should be designed based on what queries the frontend needs — not based on contract event structure. A typical mistake: making entities one-to-one with events. This leads to the frontend making N+1 requests.
The right approach — denormalized entities with pre-aggregated data:
type Pool @entity {
id: ID! # pool address
token0: Token!
token1: Token!
feeTier: BigInt!
totalVolumeUSD: BigDecimal! # cumulative volume — update on every Swap
totalValueLockedUSD: BigDecimal!
txCount: BigInt!
swaps: [Swap!]! @derivedFrom(field: "pool")
}
type Swap @entity {
id: ID! # txHash + logIndex
pool: Pool!
sender: Bytes!
recipient: Bytes!
amount0: BigDecimal!
amount1: BigDecimal!
amountUSD: BigDecimal!
timestamp: BigInt!
blockNumber: BigInt!
}
@derivedFrom — virtual relationship, doesn't store an array of IDs in the Pool record. This is important for performance: a pool with thousands of swaps won't grow in record size.
AssemblyScript Handlers: Where Everything Breaks
AssemblyScript is a strongly typed language that compiles to WebAssembly. TypeScript habits here are dangerous:
// WRONG — null reference in AS causes panic
let pool = Pool.load(event.address.toHexString())
pool.txCount = pool.txCount.plus(BigInt.fromI32(1)) // pool might be null
// RIGHT
let poolId = event.address.toHexString()
let pool = Pool.load(poolId)
if (pool === null) {
pool = new Pool(poolId)
pool.txCount = BigInt.fromI32(0)
pool.totalVolumeUSD = BigDecimal.fromString("0")
}
pool.txCount = pool.txCount.plus(BigInt.fromI32(1))
pool.save()
BigDecimal for financial values — mandatory. BigInt from contracts needs to be converted accounting for token decimals:
function convertTokenToDecimal(tokenAmount: BigInt, exchangeDecimals: BigInt): BigDecimal {
if (exchangeDecimals == BigInt.fromI32(0)) {
return tokenAmount.toBigDecimal()
}
return tokenAmount.toBigDecimal().div(
BigInt.fromI32(10).pow(exchangeDecimals.toI32() as u8).toBigDecimal()
)
}
Call Handlers and Block Handlers
Besides event handlers, there are two other types:
callHandlers — react to calls of specific functions. Used when the contract doesn't emit the necessary events (found in older contracts). Significantly slower than event-based indexing — The Graph must process each call trace.
blockHandlers — called on every block. Extremely expensive for hosted service and decentralized network. Use only if no alternative exists, mandatory with filter: { kind: once } or conditional logic inside.
Deployment and Network Operations
Hosted Service vs Decentralized Network
| Hosted Service | Decentralized Network | |
|---|---|---|
| Cost | Free (deprecated) | GRT tokens (Indexer fees) |
| Latency | Low | Higher (~100–500ms) |
| Censorship resistance | No (centralized) | Yes |
| SLA | No guarantees | Depends on Indexers |
| Suitable for | Development, testing | Production with decentralization requirement |
For production protocols — decentralized network. Graph Explorer allows you to monitor sync status and select Indexers.
# Deploy to Subgraph Studio
graph auth --studio <deploy-key>
graph codegen && graph build
graph deploy --studio <subgraph-name>
Debugging Slow Synchronization
If subgraph syncs slower than expected:
- Check the number of
callHandlers— replace witheventHandlerswhere possible - Ensure
startBlockis not too early - Check the number of
eth_callin handlers — each contract call from mapping is an additional RPC request - Use
ipfs.catminimally — slow operation
Typical speed: ~2000–5000 blocks/minute for event-only subgraph on hosted service. With callHandlers — 5–10 times slower.
What's Included
- Analysis of contract ABIs and identifying needed events/calls
- Schema design for specific frontend query patterns
- Writing and testing AssemblyScript handlers
- Deployment and sync monitoring
- GraphQL endpoint documentation and query examples







