From subgraph to nest: indexing Graph Horizon

Most of this runs on v0.6.2. Multi-contract nests, Arbitrum One, per-event tables, user-authored views/*.sql, init --from, and the nuthatch check parity framework all ship today. Still design targets - marked where they appear - are the auto-generated GraphQL layer, SSE streams, and AI-assisted schema and test generation from an ABI or a subgraph manifest. See the roadmap for the dividing line.

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, the resolved abis/, and a schema.json, ready for your views/ and checks/.

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.
  • allocations - Created, Resized, and Closed folded into current state and status.
  • delegations - Delegated, 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.

Parity you can check, not just claim

A nest ships checks/*.sql - read-only queries over its sealed data - and nuthatch check compares each result to a recorded fixture, printing a row-level diff and exiting non-zero on a mismatch. For this nest the fixtures are the deployed subgraph's own answers at a pinned block, so nuthatch check is the parity test. It is hermetic - committed fixtures, no live endpoint - so it runs in CI with no network.

nuthatch check

Planned: generating a schema, views, and a golden test suite from an ABI, or translating a subgraph manifest with init --from-subgraph, with every proposed view flagged for human review. Today you author the views; nuthatch check is what proves them.

Serve it

nuthatch dev serves one local endpoint: /tables and /table/{name}, read-only /sql, balances, /metrics, and the built-in MCP server. An auto-generated GraphQL layer and SSE streams - shaped so queries written against the original subgraph carry over with little change - are planned:

planned - 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 finished nest is published as horizon-nest, and the subgraph it is based on is worth reading. Read the manifesto, or star the repo.