The Graph
Onchain indexing for our lending work, via the graph-subgraph and graph-token-api
MCP servers.
When you need it
Direct RPC reads answer "what is the state now?". They are bad at
"what happened over the last six months?" — eth_getLogs caps at ~10k blocks per
call (../ekx-alchemy/SKILL.md), and paging a year of blocks is
slow, expensive in compute units, and fragile.
Reach for a subgraph when you need: historical event feeds, aggregations (total volume, per-user totals), sorting and filtering across many entities, or relations between entities.
Do not build one for current state — a single readContract is faster and always fresh.
Environment
GRAPH_GATEWAY_API_KEY= # SECRET — querying the decentralised network
GRAPH_TOKEN_API_JWT= # SECRET — the hosted Token API
Subgraph shape
# subgraph.yaml
dataSources:
- kind: ethereum
network: base-sepolia
source:
address: "0x…"
abi: AuctionManager
startBlock: 12345678 # ← the DEPLOY block, not 0
mapping:
eventHandlers:
- event: BidPlaced(indexed address,uint256)
handler: handleBidPlaced
Set startBlock to the contract's deploy block. Leaving it at 0 makes the
subgraph scan the entire chain history — hours of sync for zero extra data.
# schema.graphql
type Bid @entity {
id: ID!
bidder: Bytes!
amount: BigInt!
timestamp: BigInt!
auction: Auction!
}
// src/mapping.ts
export function handleBidPlaced(event: BidPlaced): void {
const bid = new Bid(event.transaction.hash.concatI32(event.logIndex.toI32()));
bid.bidder = event.params.bidder;
bid.amount = event.params.amount;
bid.timestamp = event.block.timestamp;
bid.save();
}
Mappings are AssemblyScript, not TypeScript. No closures, no try/catch, no
JSON.parse, strict typing. It looks like TS and is not — most confusing errors come
from that gap.
graph codegen && graph build
graph deploy --studio <subgraph-name>
Querying
const res = await fetch(SUBGRAPH_URL, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.GRAPH_GATEWAY_API_KEY}` },
body: JSON.stringify({
query: `{ bids(first: 20, orderBy: timestamp, orderDirection: desc, where: { auction: $id }) { bidder amount timestamp } }`,
}),
});
Query server-side — the gateway key is billable. Proxy through a route handler.
Defaults that surprise: first defaults to 100 and caps at 1000. Page with
skip, or better, cursor on id_gt for stable pagination — skip degrades badly past a few thousand.
Token API
graph-token-api gives balances and transfer history without writing a subgraph at
all. For "what tokens does this address hold", it is strictly less work than either a
subgraph or looping balanceOf.
Gotchas
startBlock: 0→ hours of pointless sync.- AssemblyScript, not TypeScript.
- Reindex on any schema or mapping change — it is not a migration, it is a full resync. Budget the time.
BigIntandBigDecimalare Graph types with their own arithmetic (.plus(),.times()), not JS numbers.- Indexing lag of a few blocks. Never use a subgraph to confirm a transaction the user just sent — poll the receipt.
- Gateway key server-side only.