From subgraph to nest: indexing Graph Horizon

This is a design preview, not the current binary. It describes where Nuthatch is headed — multi-contract nests, user-authored views, generated tests, and a GraphQL layer. Today the binary indexes a single contract (ERC-20 transfers) with an incremental balance view, read-only SQL over sealed data, and a built-in MCP server; see the quickstart. What follows is written in the present tense for readability, but marks the target, not today's feature set.

This example indexes The Graph's own network activity — operator mappings, allocations, indexing rewards and query fees, delegation balances — without using The Graph to do it. It runs on your hardware, against Arbitrum One, from three contracts.

It is a real community subgraph, not a toy. It is based on Paul Barba's Horizon Indexer Performance subgraph, and it was chosen precisely because it is awkward: three contracts, state derived across their events, delegation balances that fold deposits and withdrawals together, and rewards and fees bucketed by hour and by day. An ERC-20 transfer counter this is not.

One command, three contracts

shell
nuthatch init --chain arbitrum-one \
  0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03 \
  0xb2Bb92d0DE618878E438b55D5846cfecD9301105 \
  0x3bE385576d7C282070Ad91BF94366de9f9ba3571

The ABIs come from Sourcify — all three contracts are verified — and each contract's deployment block is detected automatically, so the backfill knows where to start. What you get back is a nest: a scaffolded project with nuthatch.toml, a views/ directory, and a tests/ directory.

A nest is the unit of indexing definition — a directory of config and views you can commit, share, and re-run anywhere. A published nest is reusable directly: nuthatch init --from <git-url> pulls someone else's and runs it against your own node.

Queryable before you model anything

Every event in those ABIs is a table the moment the nest exists — allocation_created, stake_delegated, query_fees_collected, and the rest — filling in as the backfill streams through history. Before you write a single line of modelling, you can ask real questions of the raw layer:

the ten biggest allocations opened in the last 10,000 blocks
select subgraph_deployment_id,
       indexer,
       tokens / 1e18 as grt
from allocation_created
where block_number > (select max(block_number) from allocation_created) - 10000
order by tokens desc
limit 10;

In a subgraph you get nothing until the handlers are written and the thing has synced. Here the raw event layer is free, and it is queryable while you are still deciding what the entities should be.

Entities are views, not handlers

The subgraph expresses its entities as AssemblyScript handlers: imperative read-modify-write against an entity store, one function per event, mutating rows by hand. In the nest those become declarative incremental views. The engine maintains them as events arrive, and on a reorg it retracts the affected rows and lets every view that depends on them correct itself. There is no rollback code, because there is nothing to roll back by hand.

views/indexers.sql
create view indexers as
select indexer,
       sum(tokens_indexer_rewards)                as total_rewards_earned,
       sum(query_fees)                            as total_query_fees,
       count(*) filter (where status = 'Active')  as active_allocation_count
from allocations
left join reward_events using (allocation_id)
group by indexer;

The rest of the subgraph maps across the same way:

  • operators — the latest OperatorSet per indexer.
  • allocationsCreated, Resized, and Closed folded into current state and status.
  • delegationsDelegated, Locked, and Withdrawn folded per delegator into a running balance.
  • daily and hourly aggregations — ordinary time-bucketed views of rewards, fees, and delegation events, maintained incrementally.
  • global stats — a view over the views above.

The global-singleton entity is a well-known subgraph anti-pattern: every event contends on one hot row, and it serialises writes across the whole indexer. As an incremental aggregate it costs nothing — a running total the engine already keeps.

Tests you can trust more than the author

init also generates a golden test suite: fixture blocks in, exact entity states asserted out, run with nuthatch test. When the scaffolder proposes views — from an AI translation of handlers, or from a plain-English --describe — the deterministic tests are the artefact you actually review. You are trusting the assertions, not the prose that produced them.

nuthatch test

Migrating an existing subgraph is mechanical where it can be: nuthatch init --from-subgraph subgraph.yaml converts the manifest and schema directly, and flags every proposed view translation of a handler for human review rather than assuming it got the logic right.

Serve it

nuthatch dev brings up one local endpoint: SQL, an auto-generated GraphQL schema, SSE streams, and the built-in MCP server. The GraphQL is shaped so that queries written against the original subgraph carry over with little change:

top indexers by rewards
{
  indexers(first: 10, orderBy: total_rewards_earned, orderDirection: desc) {
    indexer
    total_rewards_earned
    total_query_fees
    active_allocation_count
  }
}

The MCP server is the same endpoint from an agent's side. Point Claude, or any client, at your own instance and ask which indexers earned the most this week — schema discovery is built in, so it knows the shape without being told, and nothing leaves your machine.


So this page described indexing The Graph's network without The Graph — on your own hardware, with no deploy key, no gateway, and no query fees. The subgraph it is based on is worth reading. Read the manifesto, or star the repo.