The Graph
The question Aqua cannot answer about itself, and why an indexer is the only way to ask it.
Before an agent can act for a wallet, it has to know what it is allowed to do. On Aqua that turns out to be the hard part.
mapping(address maker =>
mapping(address app =>
mapping(bytes32 strategyHash =>
mapping(address token => Balance)))) private _balances;
event Shipped(address maker, address app, bytes32 strategyHash, bytes strategy);The mapping is private and four levels deep, so nothing enumerates it. Reading one balance needs
a strategy hash you already hold. And no event parameter is indexed.
Measured, not asserted
That last claim is the one everything rests on, so it is worth asking the chain rather than
reading the source. bun scripts/check-subgraph.ts does exactly that before it asks the index:
what the chain can answer, asked directly:
Shipped logs in the last 200,000 blocks: 1
topics per log: 1–1
→ only topic0, the signature. No parameter is indexed, so logs cannot be
filtered by maker, by app or by token. Only by "a Shipped happened".One topic — the event signature. You cannot ask for a maker's logs, because maker is not indexed.
If any parameter were indexed, a log would carry two topics or more and the script would say so instead of the conclusion. A check that cannot fail is not a check.
So "which mandates does this wallet have, and what is left in each?" has no on-chain answer at any speed. The Graph is not making this faster. It is the only way to ask.
What the subgraph indexes
Aqua's four events on Arbitrum One, plus our own account factory. It is deployed to Subgraph Studio and serving live data — a mocked dataset would not qualify and would not be useful.
Against the busiest maker on the chain, which is not ours:
maker 0xef9f7f4006fe95afede04f6916e72556a957ebbc
mandates 49, of which 11 still active, across 5 tokensForty-nine strategies under one address, and no way on chain to learn that any of them exist. A docked mandate comes back marked docked rather than merely empty — Aqua zeroes the ledger and emits no per-token event, so from outside a revocation and a spent balance look identical, and the schema keeps Aqua's own three-state sentinel to tell them apart.
Where it is load-bearing
Inside the enclave. The confidential workflow asks the index what an account's live mandates could still be asked to pay, and raises its liquid buffer to that number. The floor may only be raised, so an index that is unreachable, empty or lagging can never make the decision wrong — only less good. The run says which buffer it used.
In the dapp. "What this wallet may spend" is answered from the subgraph, because the chain cannot answer it.
In the enclave's fleet. Which accounts exist at all comes from the index, so an account opened a minute ago is managed without anybody editing a file.
The cache, and why it is not in the path
A backend endpoint sits in front of Studio and remembers an answer for a minute, so a hundred
visitors asking the same question cost one query. It knows no GraphQL beyond the operation name —
the queries live in @helico/plugin-thegraph — and the browser falls back to Studio when the
cache is absent, which three browser checks prove by unplugging it. Reading Studio directly is
what made these panels work when everything of ours was down, and a smaller bill is not worth
losing that quietly.
What we do not use
Subgraph Studio, and nothing else. No Substreams, no Token API, and no Subgraph MCP server — that last one was planned and is not built. Said here because a page about an integration should be as clear about its edges as about its middle.