ARCHITECTURE.md section 10 (indexer entry) and apps/indexer/README.md.
Indexer sync: the finalized head, a bounded log fetch, decoding by registered handlers, validation against canonical block hashes with rollback on a mismatch, one atomic apply, and readers on a read-only role. Reallocation handlers load only when configured and are covered by in-memory tests. Source: ARCHITECTURE.md section 10.
Sync
The indexer is a plain Node process, not a NestJS application, because it has no request handling that Nest would organize. It uses viem to polleth_getLogs in bounded batches and writes a cursor atomically with each batch. Polling was chosen over a subscription because a subscription still needs the same cursor and gap-filling logic after any disconnect, so polling is the one path that has to be right. The default batch of 100 blocks fits the smallest eth_getLogs range limit that Monad’s documentation lists for public providers.
Finality
By default the indexer reads only up to the block the RPC reports for thefinalized tag. Monad’s documentation, checked 2026-10-10, maps latest to Proposed (speculative), safe to Voted, and finalized to Finalized, described as irreversible without a hard fork and reached two blocks, about 600 ms, after proposal. It also warns that responses including non-finalized data can change on a later identical request. Indexing finalized blocks only means the read-model never shows a fact the chain can still retract, at the cost of about two blocks of latency. A confirmation-depth mode (default 10 blocks) exists for an RPC that does not serve the tag, and the indexer never falls back to it on its own.
Finalized is taken as sufficient, not Verified. That is an assumption, recorded so that a consumer that settles value from this data revisits it: nothing the indexer writes authorizes moving capital.
Reorgs
Kept even in finalized mode, as a defence against a node serving inconsistent data. Before each batch the stored cursor hash is compared with the chain. On a mismatch the indexer rolls back to the newest stored block hash that is still canonical, deletes every row above it, and re-indexes. A divergence deeper than the configured window stops the process rather than guessing.What it indexes today
AgentSpendPolicy:LimitsSet,CallAllowed,Spent(and the traded-asset event).- EntryPoint v0.7
AccountDeployed, and Kernel v3.1PermissionInstalled,PermissionUninstalled, andNonceInvalidated. - The settlement asset’s ERC-20
Transferwherefromortois a followed account, from the configured contract only, never a token named by a client. - With
INDEXER_REALLOCATION_ADDRESSset:Reallocated, stored once per followed side (reallocation.sentandreallocation.received),AgentRegistered,PrincipalSet, andAgentRetiredfor followed accounts, andPolicyCommitted,PolicyRevoked, andEvaluationAdvanced. Event signatures are parsed fromcontracts/src/IAgentReallocation.soland tested against it.
Read-model and writers
Rows are decoded events, and current state (limits, allowlist, session permissions) is projected from them as views, so a rollback never leaves a stale derived row. The indexer is the only writer of chain-sourced rows. The worker never depends on it for a decision, and a test fails if the worker declares or imports the indexer package. One worker job,principal, reads the read-model, up to the finalized bound only. An indexer outage therefore delays a principal change and nothing else.
How Envio is used
Envio HyperSync is used in one place, and it is optional. It is an independent second source of logs that the public evidence page uses to check the indexer’s own rows. The indexer itself still reads chain logs through viem and an RPC; HyperIndex and HyperRPC are not used. What is implemented:- The API computes a cross-check of the indexed
account.deployed,spend.*, andreallocation.*rows against HyperSync for the same recent block window, using the same comparison code as the operator scriptbun run indexer:crosscheck. Matching is by block number, log index, and kind, then account, emitting address, block hash, transaction hash, and every decoded field. GET /public/evidencereturns the result as a separatecrossCheckfield:source(envio-hypersync),status(match,mismatch, orunverified), the block range, the row counts in the indexer and in HyperSync, the matched, missing, and field-mismatch counts, the HyperSync height, andcheckedAt. It contains no row, address, hash, or amount.- The evidence page shows it as the card “Independently cross-checked with Envio HyperSync”. Only
matchis shown as passing. An unreachable source, a source behind the range, a failed or stale run, or a field of an unexpected shape is shown as unverified. - The comparison never runs on the request path. The API serves a cached result and refreshes it in the background, with one shared cache, a single refresh in flight, and a bound on refreshes per minute, so a public caller cannot trigger more HyperSync calls than that bound and the evidence data never waits on Envio.
- It is off unless
ENVIO_API_TOKENis set in the API environment. With no token the API makes no call to Envio and the field is absent.
ENVIO_API_TOKEN in the API environment (deploy/README.md, step 3).
Not yet observed live: no run against the real HyperSync service with a real token has been made, so the behavior above is covered only by tests with fakes. Whether the free token’s rate limit suffices for the window, and whether HyperSync’s block hash for a Monad block always equals the RPC’s, are hypotheses.
Not yet indexed
The identity registry has no contract yet.AgentCommitments exists but is not deployed and has no handler, because the worker reads it directly. The reallocation handlers are tested only against an in-memory chain: the testnet read-model was synced without the contract, so enabling them there needs the read-model rebuilt first (apps/indexer/README.md), and the contract has emitted no Reallocated event because reallocation is off. An opt-in run on 2026-10-10 synced a finalized batch without error but indexed no rows because the spend policy was not yet deployed. Since 2026-10-11 (start block 70167444) the first agent’s rows have been indexed.