Agentic Mini apps on TheGraph -aka dune analytics on steroids


Atlas turns a question about onchain activity into a mini app: a live interface generated from real Graph data, that can also hold a wallet and act inside a policy you set.
A mini app is not a dashboard. It watches live data, renders whatever interface fits the shape of that data, and — if you let it — takes an action when a condition breaks. Every published one gets an ENS name, an address, and an onchain identity, thereby making ENS names as self contained agentic mini-apps themselves.
The Graph had everything it needed to be the consumer analytics layer of crypto — more chains, more protocols, real time, decentralized — but it only ever shipped for developers. Atlas is the consumer surface it never built. Because the interface is generated per question, an app exists the moment you ask for it, including for the long tail nobody would ever hand-build.

The important move is at the Resolver: it picks a schema family (lending-cdp, dex-amm-extended, bridge, perp-futures, …), never a single subgraph id. One question then fires the same query shape at every healthy deployment of that family across every network we have one on, and merges the results. A question nobody anticipated still resolves, because the schema is shared even when the protocol is new to us.
A representative run:
18 sources queried → 13 healthy, 5 dead skipped by health check → 74 rows in 2.8s → $0.0014
That ~28% dead rate is real, which is why sources are health-checked at generation time and frozen into the app's manifest. All 16 of 16 seed mini apps in the registry are built by this exact pipeline — resolve, health-check, fan out, compose — not written by hand. Re-measuring every one of them costs $0.0084 total.
analytics ────────────── monitoring ────────────── autonomous
read-only watches + alerts holds a wallet, acts
sits flush lifts, live-blue rim stands proud, orange rim
The tier is not decoration. An autonomous app always renders its policy strip, its kill switch and its trade log — you can see what a thing is allowed to do by looking at it. The shipped theme is skeuomorphic (Dieter Rams / Braun), and a single data-skin attribute re-expresses the whole system in a different material with no component changes, no catalog changes, no round trip to the agent. That theme swap is the proof that the generated UI is data, not code.
The model emits A v0.9.1 — a declarative document plus a data model. The client holds the approved component catalog and the document may only reference components by name. For a generated interface that can move money, that containment is the entire safety argument.
Form follows data, not keywords: a bounded ratio becomes a gauge, a ranked categorical becomes a leaderboard. The composer reads the shape of what came back, not words in the prompt.

Polling a subgraph means an autonomous guard is up to five minutes late. For an app that displays, that's a preference. For one that spends, it's a correctness bug — so triggers run off a real Substreams gRPC subscription, one tick per block, each carrying the cursor the stream resumes from.
Four properties the design turns on:
blockUndoSignal is journalled and rewinds; it never becomes a trigger signal. The right answer to "that block didn't happen" is to stop, not to act.<block>:<hash> and the ledger dedupes.healthFactor could make an autonomous app trade on demand.The whole chain runs to a real transaction:
Arbitrum block 487540654 (real, off arb-one.streamingfast.io)
→ Substreams tick → trigger "healthFactor < 1.15" fired
→ POLICY OK approve $25 → 0x036CbD53… (allowlisted, under the $50/tx cap)
→ session key signed → tx 0x5a44e9d5…9d78
→ status success, block 44604106, 1 Approval event
→ allowance 25 USDC, read back off chain
V It is an approve, and it is described as one — granting the router an allowance is genuinely the first step of a swap, but it is not a swap, so approve is its own action kind rather than a swap the manifest lies about.

atlas-apps.eth is registered and wrapped on Sepolia and issues a subname per mini app. The ENS record asserts the Agentic ID; the registry on 0G Chain asserts the name; the ERC-7857 token stores the name it was minted against. mutuallyVerified is true only when all three agree, and it is read off the chain on every resolve, never assumed.
Eight subnames are issued; three mutually verify today, and the table in the README says exactly why each of the other five does not — four were published under a previous parent name and their tokens still assert it, and the name↔token binding is deliberately immutable, so renaming a parent cannot drag old tokens along. One (aave-guard-fork) is left broken on purpose as the artifact that exposed a swallowed revert in the fork-registration path.
The contenthash resolves without us: read contenthash off Sepolia, fetch the CID from ipfs.io, parse the manifest. Five of the eight issued names do that today. Three point at bytes that no longer exist anywhere, because they were published while the content store was a gitignored local directory — a CID is the hash of its bytes, so that is unrecoverable rather than pending, and it's stated as such rather than quietly dropped.
/api/mcp — the same URL every published app writes into its ENS agent-endpoint[mcp] record. Five read-only tools: list_schemas, plan_mini_app, query_graph, build_mini_app, resolve_mini_app. Nothing there signs or spends, because an MCP endpoint is a URL strangers point agents at.SKILL.md at the repo root so an agent can decide whether The Graph covers this at all before trying.smart-session mode (ERC-7579 + Rhinestone Smart Sessions) the allowlist, expiry and lifetime cap are enforced by the account itself. onchainEnforced is the return value of a live isSessionEnabled() call, never a constant, and the running session-eoa mode says plainly that every row is server-enforced.Named explicitly, because a vague scope claim is worse than a small one: a real swap from a stream trigger (the transaction is real, the swap is not built); per-account positions (the standardized fan-out reads protocol-level scalars, so healthFactor conditions evaluate false and are marked inert rather than shown as armed); per-app wallets (one session key currently signs for every app, and the autonomous panel says so); a shared browsable registry (the board is per-browser localStorage plus the measured seed snapshot); x402 receipts (implemented, never exercised — no key set); and Subgraph MCP consumption (the env var exists, nothing calls it, so it is a gap, not an integration).
The README marks every claim that was verified by reading it back from a chain or a gateway, and strikes through two claims that an audit found overstated rather than deleting them.
Stack. Next.js 16 (App Router) + React 19 + TypeScript, Tailwind v4 with shadcn/ui and Base UI, deployed on Vercel. Contracts are Solidity + Foundry on 0G Galileo (chain 16602). Chain access is viem end to end; Privy handles the human wallet; permissionless + @rhinestone/module-sdk handle the smart-account signer modes. Zod validates every boundary. No database — state is localStorage, IPFS via Pinata, a generated JSON snapshot, and the chains themselves.
The Graph is the load-bearing data source, three products deep.
Standardized Subgraphs. is a 96-entry registry — 86 verified deployment IDs across 11 schema families and 4 networks (Arbitrum One, Optimism, Base, Mainnet). resolves a question to families, not ids; fires one query shape at every healthy deployment and merges. Health checks live in with a 90s TTL and a 2s timeout, because a quarter of standardized deployments are dead at any moment and a demo that hides that is lying.
Substreams. is a real gRPC subscription (@substreams/core over Connect/HTTP-2) against arb-one.streamingfast.io, with a cursor store, <block>:<hash> dedupe keys, and blockUndoSignal handling. verifies it both directions — a breach run that fires and a control run over healthy blocks that fires nothing, because a harness that can only print ✅ proves nothing:
breach: blocks 487508073 → 487508075 in 1.5s → TRIGGER fired → POLICY OK → journaled
control: blocks 487509578 → 487509580, healthy throughout → 0 firings
x402. implements keyless per-query payment by hand rather than via the client package: POST unpaid, parse the real 402 challenge, answer it with an EIP-3009 signature at the published ~$0.01/query. This is the mechanism that makes forking coherent — a forked app pays with its own wallet instead of inheriting a shared API key. X402_PRIVATE_KEY is unset, so no query has actually been paid this way, and the submission says so rather than implying a receipt exists.
0G does the inference and the identity. Planning and UI composition run on 0G Private Computer (router-api.0g.ai/v1, model 0gm-1.0-35b-a3b) pinned to verified routing in — the router's default Standard mode spans unverified community channels, where verify_tee can return no attestation at all. Proof of inference is a chain, not a screenshot:
TEE run (tee_verified: true) → request id
→ manifest.provenance.attestationRef 0g://f1ade7e8-e9ca-4fb2-b5c9-79cd400e3195
→ keccak256 0xa651240d…52f1
→ MiniAppRegistry.attestationHash ← same value, onchain, bound to token and name
implements the ERC-7857 V2 eip-7857-draft interface properly rather than shaped-alike: AES-256-GCM metadata encryption with dataHash = keccak256(ciphertext), ECDSA proofs the onchain verifier actually accepts, receiver binding, stale-metadata rejection, single-use 48-byte nonces. The reference verifier ships two stubs (verifyPreimage returns true for any blob; the TEE check is a // TODO) and neither was copied.
ENS is the identity layer, not a label. writes addr, contenthash, ENSIP-26 agent-context and agent-endpoint[web|mcp], and the ENSIP-25 agent-registration[<erc7930>][<agentId>] binding, with the ERC-7930 chain-address encoding verified against ENSIP-25's own worked example. reads the 0G side of the binding back and mutuallyVerified is the conjunction of all three assertions.
ETHRegistrarController is not an authorised controller there, so register() reverts while available() still returns true — a silent dead end. The path that works is TestnetV1PremigrationRegistrar (0xdf60C561Ca35AD3C89D24BbA854654b1c3477078), free, one transaction, data: []. Then the name comes back unwrapped, so NameWrapper.setSubnodeRecord reverts Unauthorised until you setApprovalForAll + wrapETH2LD. That sequence is the single largest time sink of the build and it is written down in so the next team doesn't lose the same day.getEnsResolver returns a non-resolver on Sepolia. For one of our names it answers 0x422484c2…, where every addr/text/contenthash call reverts; the registry's own resolver(node) answers 0xE99638b4…, which holds the records. We ask the registry first and keep the UniversalResolver path second for wildcard/CCIP names.resolveRegistrarMode() degrades to mock without a registrar private key, and readRecords skipped the resolver entirely in mock mode — so the public deployment resolved nothing while working perfectly on the machine holding the key. Reading needs an RPC and nothing else. Same shape as the IPFS bug below: "it resolves on my machine" is not a property of a decentralized record.pinJSONToIPFS wraps a document as UnixFS and returns a dag-pb CID (bafybei…) while local mode addresses the raw block (bafkrei…). Enabling Pinata naively would have minted new addresses and orphaned every contenthash already written to Sepolia. pinFileToIPFS with cidVersion: 1 reproduces the local CID exactly, and now asserts it per pin: if Pinata ever returns a CID we did not derive, the publish fails rather than writing an unverifiable address onchain.MiniAppRegistry.registerFork reverts ParentUnknown when the parent was never published — correct, so lineage can't be faked. But the publish path passed a parent key whenever a manifest carried forkedFrom, and every bundled app is unpublished. So: ENS written, token minted, registerFork reverts, caught as a generic "registry write failed" — and the 0G half of the mutual proof silently missing. Fixed by asking the contract's own get(parentKey) first and registering without the lineage link when the parent is genuinely absent, surfacing lineageSkipped in the publish warnings. Losing attribution is recoverable from the manifest; losing the registration costs the safety primitive a stranger uses before funding a name.[email protected] denominates in ETH — cumulativeTradeVolumeUSD does not exist on Marketplace, so asking for it is a hard error that zeroed the whole family. A broken SushiSwap price feed put $72384163253T at the top of a leaderboard because the shape detector re-sorted and undid the suspect ranking. And isActive: true — the normal state of every healthy market — rendered "Is Active has fired" in an alert banner on a wallet-holding app. pins all four.spentUsd now comes from the same server-side number the lifetime cap is metered against, so $0.00 is a measurement. valueTransactedUsd had no writer once the ticker went, so it renders nowhere — a figure with no writer is worse than a missing one, because it looks like a measurement.isConditionEvaluable — the same grammar the evaluator uses, asked a different question. Applying it to the seed apps found zero of ten evaluable: four grammatically valid on impossible paths, five unparseable prose, one null. All ten were rewritten to protocol-level premises the fan-out genuinely answers, and one that would have alerted on the first block it saw was re-armed with measured headroom.--priority-gas-price 2gwei. The chain enforces a 2 gwei floor and rejects forge's default estimate outright.pnpm test runs the typecheck first, on purpose. tsx strips types and runs; it never type-checks, so the suite alone goes green on code that does not compile.
