The Graph Integration (Data Indexing)
A typical problem that comes up: the frontend makes dozens of eth_call and getLogs on every page load. On mainnet this is slow, on public RPC nodes—unreliable, and when you need aggregation or historical data—simply impossible through direct calls. The Graph solves this correctly: you write a subgraph once that indexes contract events, and get a GraphQL API with arbitrary queries.
Subgraph Architecture
A subgraph consists of three components:
subgraph.yaml — the manifest. Describes data sources: which contracts to listen to, from which block (startBlock), which events and functions to process. Critical: startBlock should be the contract deployment block, not zero—otherwise indexing takes days.
schema.graphql — entity types. This is what will be available through GraphQL. Designed based on frontend needs, not contract event structure—these are different things.
mappings.ts — AssemblyScript handlers. Transform raw events into schema entities.
Schema Design
The most common mistake is making the schema a mirror of contract events. If the event is Transfer(address from, address to, uint256 amount)—don't need a TransferEvent entity. Think about queries: "what's the user's current balance", "top holders", "volume in 24h".
type Token @entity {
id: ID!
totalSupply: BigInt!
holderCount: Int!
}
type Account @entity {
id: Bytes!
balance: BigInt!
transfersIn: [Transfer!]! @derivedFrom(field: "to")
transfersOut: [Transfer!]! @derivedFrom(field: "from")
}
type Transfer @entity(immutable: true) {
id: Bytes!
from: Account!
to: Account!
amount: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
}
@entity(immutable: true) for Transfer—important optimization. Immutable entities aren't stored in the undo buffer, indexing is 30–40% faster.
Handlers: Nuances
AssemblyScript isn't full TypeScript—no null via ?., no Array.from(), no standard JS methods. This is a frequent source of errors for developers coming from frontend.
// Correct entity loading or creation
function getOrCreateAccount(address: Address): Account {
let account = Account.load(address)
if (account == null) {
account = new Account(address)
account.balance = BigInt.fromI32(0)
}
return account as Account
}
export function handleTransfer(event: TransferEvent): void {
let from = getOrCreateAccount(event.params.from)
let to = getOrCreateAccount(event.params.to)
from.balance = from.balance.minus(event.params.value)
to.balance = to.balance.plus(event.params.value)
from.save()
to.save()
// Immutable — create once, don't load
let transfer = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
)
transfer.from = from.id
transfer.to = to.id
transfer.amount = event.params.value
transfer.blockNumber = event.block.number
transfer.timestamp = event.block.timestamp
transfer.save()
}
Call handlers and block handlers
Besides events, The Graph can process function calls (callHandlers) and each block (blockHandlers). Call handlers are needed when a contract doesn't emit events for needed operations—legacy contracts suffer from this. Block handlers—for periodic snapshots (e.g., daily stats). Both types significantly slow down indexing, especially block handlers—use only if you can't do without them.
Deployment and Exploitation
Hosted Service vs Decentralized Network
Hosted Service (The Graph Network hosted)—centralized, free, suitable for development and MVP. No SLA guarantees, can be unavailable.
Decentralized Network—indexers receive GRT for queries, curation signal affects priority. For production with uptime requirements—the right choice, but need to understand the economics: query costs depend on volume.
Self-hosted Graph Node—full control, no GRT costs, but infrastructure load. Requires: PostgreSQL 14+, IPFS node, Ethereum archive node (or Firehose). Minimum requirements for production: 16 CPU, 64GB RAM, NVMe SSD.
Typical Indexing Problems
Subgraph fails with "store error"—usually an attempt to save an entity with a null field declared non-nullable in the schema. Check all fields before save().
Indexing stalls on a specific block—usually a call handler on a reverted transaction. Need to add handling via receipt.status.
Data divergence from on-chain—subgraph doesn't account for reorgs. For critical data, need minEthereumBlockConfirmations in the manifest or additional logic on the client.
Frontend Integration
Standard stack: Apollo Client or urql for React apps. The Graph supports subscriptions via WebSocket—for realtime updates without polling.
const POSITIONS_QUERY = gql`
query UserPositions($account: Bytes!, $skip: Int!) {
positions(
where: { owner: $account, liquidity_gt: "0" }
orderBy: createdAt
orderDirection: desc
first: 100
skip: $skip
) {
id
pool { token0 { symbol } token1 { symbol } feeTier }
liquidity
depositedToken0
depositedToken1
}
}
`
Pagination via first/skip works until skip: 5000—beyond that The Graph returns an error. For large datasets, need keyset pagination via id_gt.
Timelines and What's Included
In 2–3 days: schema design for client tasks, writing mappings for all events and calls, fork testing, deployment on Hosted Service or self-hosted node setup, basic frontend integration or GraphQL endpoint provision.







