VaultRadar, design spec
Date: 2026-09-05 (revised after independent review the same day). Event: ETHOnline 2026 (ETHGlobal), Start Fresh pool. Submission deadline: Sunday 2026-09-13, 12:00 EDT. Solo builder plus Claude Code.
1. One-line pitch
VaultRadar is a machine-payable, cross-protocol vault-risk service: one standardized query across every Messari-schema protocol plus a new ERC-4626 Substreams module, sold per request over x402 on Hedera and Arc, consumed by a risk-monitor agent. Requests and responses are sealed with post-quantum KEM so no intermediary learns the portfolio, a privacy tier lets the vendor learn only the protocol, and every receipt and data attestation is signed with ML-DSA-65 so it stays verifiable long after ECDSA is gone.
2. Prize targets and what each judge must see
Three partner picks (ETHGlobal caps a submission at three partners; all of a partner's tracks count as one pick).
| Partner / track | Must-show |
|---|---|
| The Graph, Composable / Standardized | One query template executed across N Messari deployments (yield-aggregator and lending schemas), pinned by deployment ID. A new ERC-4626 Substreams module composed from the Pinax erc4626 package, running on two chains through The Graph Market. README section "what the standards made easier". |
| The Graph, AI Use Case (From Scratch) | Agent reasons over live Graph data and makes decisions; independently refuses stale data. x402 pay-per-query (agent pays service; optional: service pays The Graph gateway via x402 on Base). One-prompt Substreams generation using the Substreams Skills, recorded honestly (see 5.2). |
| Hedera, AI & Agentic Payments | Live x402-gated service on Hedera testnet settled through Blocky402. At least one real paid request end to end in the video. Extras: metered per-call pricing, HTS USDC settlement, ERC-8004 identity with the PQ key anchored on-chain, UCP discovery profile, HCS audit trail of commitments. |
| Hedera, Improve the Harness | Open PR to hedera-dev/hedera-harness adding a Tier 3.5 x402Probe chain validator, branched from open PR #15 (ephemeral-signer HTS association). Tests, docs section, before/after evidence, 15-second clip inside the main video. |
| Arc, Agentic Economy | Agent built on Circle's Claude Agent SDK starter kit, pays via Gateway nanopayments on Arc testnet, decision logic tied to real signals (price, balances, attestation age, privacy policy). Working frontend showing a live Arc payment, plus architecture diagram. |
| Arc, Launch and Push to Mainnet | Same integration with a documented mainnet config path ready by 2026-09-30 (Arc mainnet opens 2026-09-16). |
Finalist criteria (Technicality, Originality, Practicality, Usability, WOW): the sealed-channel and PQ-receipt layer is the WOW; the standards leverage is the practicality.
3. Threat model for the privacy and PQ layer
- Portfolio disclosure to intermediaries. The vault list in a request, and the vault list plus verdicts in a response, pass TLS terminators, hosting proxies, logs, and traffic recorders positioned for harvest-now-decrypt-later. Mitigation: both request and response are sealed with a hybrid post-quantum KEM (section 5.4). The vendor itself does decrypt
scanrequests; only thetabletier hides holdings from the vendor. - Replay of a captured envelope. A third party replays a sealed request with its own payment to obtain the plaintext answer. Mitigation: the response is sealed to a client ephemeral key inside the envelope, the envelope binds the paying account, and the service rejects envelopes outside a 120-second window or with a seen nonce.
- Unverifiable receipts. ECDSA receipts cannot promise long-horizon verifiability to an auditor. Mitigation: ML-DSA-65 signatures over every receipt and attestation.
- Audit trail that leaks. A public HCS log of queries would leak customer interests. Mitigation: HCS stores commitments (hashes plus signatures), never content.
- Key substitution during discovery. A fake agent card could present a fake PQ key, and a registration file served from the same host cannot rule that out. Mitigation: the ML-DSA public-key hash is written on-chain as ERC-8004 metadata under the service's agent id; discovery reads it from the chain.
README boundary statement, verbatim: "The ML-DSA key is anchored on-chain by an ECDSA-controlled account. A verifier that has fetched and pinned the key once can verify receipts indefinitely without trusting ECDSA again. The anchor protects discovery today; it does not stop a future quantum adversary from re-pointing the registry. x402 payment signatures and chain consensus remain classical. There is no forward secrecy against later compromise of the service's KEM seed."
4. Architecture
┌─────────────────────── The Graph ───────────────────────┐
│ Messari standardized subgraphs The Graph Market │
│ (yield 1.3.1, lending 3.1.0) Substreams endpoint │
└──────────┬──────────────────────────────┬───────────────┘
│ GraphQL by deployment ID │ erc4626-vault-metrics.spkg
│ (Studio key / x402) │ hosted sink → Postgres (Neon)
▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ packages/service (Express) │
│ standardized query layer → unify → freshness → risk │
│ POST /hedera/v1/* x402 via @x402/express + Blocky402 (HTS USDC / HBAR) │
│ POST /arc/v1/* x402 via @circle-fin/x402-batching (Gateway, USDC) │
│ PQ: seal/open (ML-KEM-768+X25519), ML-DSA-65 receipts, attestations, card │
│ HCS commitments · ERC-8004 identity + on-chain pq metadata · well-known │
└───────────────┬───────────────────────────────────────────┬───────────────┘
│ 402 → pay → 200 + sealed body + receipt │ /v1/receipts (public commitments)
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ packages/agent (Claude Agent │ │ packages/dashboard (Next.js) │
│ SDK, Circle starter kit) │ │ catalog · freshness · runs │
│ discover → quote → choose │ │ payments per rail · HCS │
│ rail+tier → seal → pay → │ │ receipt verifier │
│ open → verify → decide │ └──────────────────────────────┘
└──────────────────────────────┘
Monorepo (bun workspaces for TypeScript; Cargo for the Substreams module):
vaultradar/
LICENSE MIT
.env.example every variable, documented
substreams/erc4626-vault-metrics/ Rust Substreams package
packages/core/ schema types, query templates, unify, freshness, risk, pq, receipts
packages/service/ x402-gated API, identity, HCS
packages/agent/ Claude Agent SDK agent
packages/dashboard/ Next.js UI
skills/vaultradar/SKILL.md how an agent uses the service (installable as a Claude Code skill)
scripts/ verify-deployments, identity bootstrap, demo
docs/ spec, architecture.md (mermaid), one-prompt.md
5. Components
5.1 Standardized data layer (packages/core/standardized)
- Deployment registry
deployments.json: curated Messari deployments with{ protocol, chain, schema: "yield-aggregator" | "lending", subgraphId, deploymentId, status, headLagSeconds, verifiedAt }. Queries always use the pinneddeploymentIdviahttps://gateway.thegraph.com/api/deployments/id/<Qm...>so a subgraph re-point cannot silently change the data. Initial candidates: Aave v3 (Ethereum, Base), Compound v3 (Ethereum), Spark (Ethereum), Morpho-Aave v3 (Ethereum), Euler (Ethereum), Yearn v2 (Ethereum, Arbitrum), Convex, Aura, Arrakis, Gamma. - Verification gate (
scripts/verify-deployments.ts, run on Sept 5 and before every demo): queries_meta { block { number timestamp } hasIndexingErrors }on each and writesheadLagSecondsandstatus:livewhen lag is at most 60 minutes,stalewhen larger,downwhen the query fails or reports indexing errors. Stale and down deployments stay in the registry on purpose; the stale-refusal demo uses them. If fewer than three deployments areliveon Sept 5, the Composable story rests on the Substreams pipeline on two chains plus the standardized query across whatever is live, and success criterion 3 is amended accordingly in the README. - Query templates, one per schema family, parameterized only by pagination:
yield.vaults(Vault, last 24 VaultHourlySnapshot, last 8 VaultDailySnapshot) andlending.markets(Market, last 24 MarketHourlySnapshot, last 8 MarketDailySnapshot). Both include_meta. Exact field names are confirmed by schema introspection in the first implementation task; required fields are share price or exchange rate, TVL USD, input and output token, input token balance, output token supply, deposit limit (yield only), daily deposit and withdraw USD (lending), snapshot timestamps and block numbers. - Gateway client: bearer Studio API key. Optional mode
UPSTREAM_X402=1uses@graphprotocol/client-x402against the gateway's x402 endpoints, paying USDC on Base ($0.01 per query); used only for the "no API keys anywhere" demo segment.
5.2 ERC-4626 Substreams module (substreams/erc4626-vault-metrics)
- Rust package importing Pinax
erc4626as a dependency (itsmap_eventsdecodesDepositandWithdrawfor every ERC-4626 vault by topic, no address list). initialBlockper chain set to about 28 days before head at deploy time (Ethereum: head minus 200k blocks; Base: head minus 1.2M blocks) so backfill fits the Market free tier and finishes in hours.- Modules:
map_vault_events: consumes Pinax events; emitsVaultEvent { vault, block, timestamp, kind, sender, owner, assets, shares, implied_share_price }withimplied_share_price = assets / sharesas a decimal string.store_vault_meta(set-if-not-exists, keyed by vault): on first sight of a vault,eth_callasset(), thendecimals()andsymbol()on the asset, anddecimals()on the vault; stores{ asset, asset_symbol, asset_decimals, share_decimals }.store_depositor_seen(set-if-not-exists, keyedvault:owner) andmap_new_depositors(reads that store's deltas, emits one record per CREATE) feedingstore_depositor_count(add, keyed by vault). Two chained stores because a store module cannot read its own deltas.store_vault_flows(add, keyed by vault): cumulative assets deposited, assets withdrawn, shares minted, shares burned; plusstore_last_call_block(set, keyed by vault).map_vault_metrics: for vaults touched in the block, emitsVaultMetrics { chain_id, vault, block, timestamp, share_price, share_price_source: "event" | "call", total_assets, total_supply, net_deposited_assets, net_flow_assets, depositor_count, last_event_block }. When a vault is touched andblock - last_call_block ≥ 300, aneth_callbatch tototalAssets()andtotalSupply()refreshes exactshare_price = totalAssets / totalSupply(decimals-aware) and updates the store; otherwisetotal_assetsandtotal_supplyare null andshare_priceis event-implied.net_deposited_assetsis cumulative deposits minus withdrawals and is never presented as TVL.db_out:DatabaseChangesforsubstreams-sink-sqlwithschema.sqldefiningvault_metrics(primary keychain_id, vault, block),vault_latest(primary keychain_id, vault), andvault_meta.
- Deployment: The Graph Market hosted sink to a Neon Postgres for Ethereum mainnet and Base. Fallback:
substreams-sink-sqlrun on the service host against the same Market endpoint. Package published to substreams.dev aserc4626-vault-metricsby Sept 7, because the hosted sink may require a published package. - Sink freshness comes from the sink's cursor block (stored in the sink's cursor table), not from a vault's
last_event_block, so quiet vaults are not misreported as stale. - One-prompt demo, recorded honestly: the first generation from a single prompt (events plus SQL sink) is recorded as the one-prompt run and committed as-is; the stores, eth_calls and review fixes follow in later commits.
docs/one-prompt.mdkeeps the prompt, the generated diff, and the follow-up diff. The README never claims the final module is one prompt.
5.3 Unified view, freshness, risk (packages/core)
UnifiedVault { id: "<chainId>:<address>", kind: "yield-vault" | "lending-market" | "erc4626", protocol, chain, asset: { symbol, decimals } | null, sharePrice, tvlUsd: string | null, history: [{ block, timestamp, sharePrice, tvlUsd | null, netFlowAssets | null }], sources: [{ kind: "messari" | "substreams", ref, block, timestamp, ageSeconds }], freshness: "fresh" | "stale" | "unavailable" }. All numerics are decimal strings.- Freshness:
freshif the source's reference block is at most 60 minutes behind chain head for Messari and 5 minutes for Substreams;staleotherwise;unavailableif the query failed or_meta.hasIndexingErrorsis true. The Messari reference block is_meta.block; the Substreams reference block is the sink cursor. Chain head comes from the JSON-RPC provider per chain, cached 15 seconds; RPC failure marks every source on that chainstale. - Risk flags and per-schema definitions:
share_price_drawdown_1h,_24h,_7d: relative drop in share price (yieldpricePerShare, lendingexchangeRate, erc4626share_price) over the window, thresholds 0.5 %, 2 %, 5 %. The 1 h flag requires hourly data (hourly snapshots or Substreams history); without it the flag is skipped, not guessed.tvl_outflow_24h: yield uses the 24 h change ininputTokenBalancerelative to the current balance; lending usesdailyWithdrawUSD - dailyDepositUSDrelative tototalDepositBalanceUSD; erc4626 usesnet_flow_assetsover 24 h relative tototal_assets(skipped whentotal_assetsis null). Threshold 20 %.deposit_limit_reached: yield only,inputTokenBalance ≥ depositLimitwhen a limit is set.stale_data: any source notfresh.
- Score: drawdown 1 h 30, 24 h 25, 7 d 20; outflow 25; deposit limit 10; sum capped at 100. Verdict:
okbelow 20,watch20-49,alert50 and above. Hard rule: if any source needed for a flag isstaleorunavailable, the verdict isunavailableand evidence names the source and its age. No verdict is ever inferred from partial data. RiskReport { vaultId, flags: [{ name, value, threshold, window }], score, verdict, evidence: [{ source, block, timestamp, ageSeconds }] }.
5.4 PQ, sealing, and receipt layer (packages/core/pq)
- Library:
@noble/post-quantum0.7.x only (public library; no code from prior projects). - Keys: derived at boot from secrets
PQ_SIG_SEED(32 bytes hex) andPQ_KEM_SEED(64 bytes hex) using the library's seeded key generation. No key material touches disk, so redeploys on Fly or Railway keep the same keys. Public keys and their SHA-256 hashes are published;kid = SHA-256(kem_pk)[0..8]identifies the KEM key in envelopes. KEM rotation is out of scope. - Signatures: ML-DSA-65 (FIPS 204), signature about 3.3 KB, public key about 2 KB. Signatures always travel in JSON bodies, never headers.
- Sealing: hybrid KEM
ml_kem768_x25519as exported by the library (the X-Wing style combiner), named"ml-kem768-x25519"in envelopes. Shared secret → HKDF-SHA-256 with info"vaultradar/seal/v1"→ AES-256-GCM. - Request envelope:
{ v: 1, kem: "ml-kem768-x25519", kid, ct, nonce, body }wherebodyencrypts canonical JSON of{ request, reply_pk, payer, ts, req_nonce }.reply_pkis a client ephemeral KEM public key;payeris the paying account (Hedera account id or Arc address);tsis a Unix timestamp;req_nonceis 16 random bytes. The clear headerX-VR-Countcarries the vault count for pricing. - Response envelope:
{ sealed: { kem, ct, nonce, body }, receipt }wherebodyencrypts canonical JSON of{ vaults, reports, attestations }toreply_pk. In clear mode (no sealing), the response is{ vaults, reports, attestations, receipt }and the receipt recordssealed: false. - Service checks before answering:
tswithin 120 seconds of server time,req_noncenot seen in the last 10 minutes,payerequals the payer in the verified payment payload the x402 middleware attaches to the request, andX-VR-Countequalsrequest.vaults.length. Any failure returns 422 before settlement, so nothing is charged. - Canonical JSON: deterministic key ordering, no whitespace, all numerics as strings (avoids RFC 8785 float rules); a small internal canonicalizer. Hashes are SHA-256 over canonical bytes.
- Receipt (signed):
{ v: 1, service: { erc8004: [{ chainId, agentId }] }, request_hash, response_hash, sealed, sources: [{ ref, chainId, block, timestamp }], price: { amount, asset, rail }, payment: { rail: "hedera" | "arc", txId }, tier: "scan" | "table", issued_at, nonce, hcs: { topicId } }plussig: { alg, pub_hash, value }.request_hashis SHA-256 of the canonical plaintextrequest, so the agent can recompute it;response_hashis SHA-256 of canonical{ vaults, reports, attestations };receipt_hashis SHA-256 of the canonical receipt withoutsig. The HCS sequence number is not in the signed receipt; it is looked up byreceipt_hash. - Attestation (one per vault in a response):
{ v: 1, vaultId, chainId, block, timestamp, sharePrice, tvlUsd, source }plussig. Individually signed, so it stays portable after the sealed response is opened. - Verification tool:
bun run verify <receipt.json> [response.json]and an agent tool; both check the signature, thepub_hashagainst the on-chain ERC-8004 metadata, and recomputeresponse_hashwhen the response body is present.
5.5 Service (packages/service)
- Express on bun, Node 22 compatible.
express.json()runs before every payment middleware so the x402 v2 request adapter'sgetBody()can validate envelopes in the price function. - Public unpaid routes (CORS
*on these only):GET /.well-known/agent.json(agent card with endpoints, prices, rails, PQ public keys and hashes, ERC-8004 ids, UAID when present;siginside the JSON body),GET /.well-known/ucp(UCP profile listing services and two x402 payment handlers),GET /.well-known/erc8004.json(registration file),GET /v1/catalog,GET /v1/receipts/:hashreturning{ receipt_hash, topicId, sequence | null, consensus_timestamp | null, initial_transaction_id | null }. - Paid routes are registered by full path (
"POST /hedera/v1/scan","POST /arc/v1/scan/s", ...) because the x402 adapter matches on the request path; handlers are shared functions.- Hedera
POST /hedera/v1/scan: price$0.001 + $0.0005 × countfrom the x402 v2 per-request price function readingX-VR-Count; the function also validates envelope structure. Asset HTS USDC0.0.429274. APOST /hedera/v1/scan-hbarvariant prices in tinybars for the harness validator. - Arc
POST /arc/v1/scan/s|m|lfor counts 1-5, 6-20, 21-100 at$0.003,$0.01,$0.05, because Circle's middleware is static per route. POST /{hedera,arc}/v1/table: body{ protocol, chainId }(sealed or clear), flat$0.06on both rails, a deliberate privacy premium sotableis never cheaper than a sealedscan. Returns every vault of that protocol. (Amended 2026-09-10: the original$0.03was below the$0.051a metered scan of the maximum 100 vaults costs, inverting the premium above 58 vaults;$0.06restores the stated invariant at every count.)
- Hedera
- x402 v2 order of operations: the middleware verifies the payment before the handler and settles only after a 2xx. Consequence: 4xx from the handler costs the payer nothing, and the signed Hedera transfer's validity window bounds handler time, so upstream work is capped at 60 seconds and returns 504 (unsettled) beyond that.
- Hedera rail:
@x402/express,@x402/core,@x402/hederapinned to one 2.x version; facilitatorhttps://api.testnet.blocky402.com. Arc rail:@circle-fin/x402-batchingcreateGatewayMiddlewareagainst Circle's testnet Gateway facilitator, networkeip155:5042002. - After settlement: enqueue an HCS message
{ receipt_hash, sig, issued_at }(about 4.6 KB, five chunks) to the service topic viaTopicMessageSubmitTransaction, retry with backoff; the mirror node'schunk_info.initial_transaction_idreassembles chunks. The receipt lookup reports the sequence when confirmed. - Identity bootstrap
scripts/identity.ts: create the HCS topic; register in the ERC-8004 IdentityRegistry0x8004A818BFB912233c491871b3d84c89A494BD9eon Hedera testnet (chain 296, via Hashio with an ECDSA-alias account and explicit gas limit) and Arc testnet (chain 5042002, gas paid in native USDC) withagentURIpointing at/.well-known/erc8004.jsonand metadatapq.sig.pubhash = <hex>set in the same flow; compute an HCS-14 UAID with@hashgraphonline/standards-sdkif time allows; write ids toservice.config.json. - Logging never includes decrypted bodies. Rate limit 60 requests per minute per IP on paid routes.
- Hosting: Fly.io or Railway with a stable public URL; all secrets via environment variables documented in
.env.example.
5.6 Agent (packages/agent)
- Derived from
circlefin/agent-stack-starter-kitsClaude Agent SDK variant (public starter kit). Model: latest Claude via the Anthropic API. - Wallets: Hedera testnet ECDSA account (
@x402/fetch+@x402/hederasigner) and Arc testnet key with a Gateway deposit (GatewayClient); the Circle CLI handles Arc wallet setup and balance checks. - Tools:
discover(url)(fetch the card, verify its ML-DSA signature, readpq.sig.pubhashfrom ERC-8004 metadata on Hedera and Arc RPC, compare),quote(request)(unpaid 402 probe per rail),pay_and_scan(request, policy)(choose rail byrail_preferenceand balances, choose tier byprivacy, seal with a freshreply_pk, pay, open the response, verify the receipt and attestations, persist toruns/<id>.json),verify_receipt,explain. (Amended 2026-09-10:quoteis local arithmetic over the pricing constants shared with the service, not a 402 probe, it makes no request. The probe's purpose was to learn what the service would actually demand, and that is instead enforced at the moment it is demanded: the per-rail ceilings refuse a 402 more than one percent over the quote before anything is signed, andcheckSettledPriceholds the receipt's stated price to the same band afterwards. A probe would also be a second round trip whose answer the real 402 could contradict anyway.) - Policy file
policy.json:{ budget: { hbar, usdc_hedera, usdc_arc }, privacy: "strict" | "balanced" | "cheap", rail_preference: "cheapest" | "hedera" | "arc", max_age_seconds }.strictalways buystable;balancedsealsscan;cheapallows clearscan. - Independent freshness: the agent rejects any attestation whose
timestampis older thanmax_age_secondsby its own clock, regardless of the service'sfreshnessfield, and records the rejection asinsufficient data. - Behavior: given a watchlist or wallet, budget the session, buy data, reason over
RiskReports, and output a decision per vault (hold,withdraw,rebalance,insufficient data) with citations: block numbers, deployment or package refs, payment tx ids, receipt hash, HCS sequence. Anyunavailableverdict or rejected attestation yieldsinsufficient data, never a guess. - Interfaces: CLI (
bun run agent watch --vaults ... --policy policy.json) and an interactive chat mode. Optional tool: the official Subgraph MCP for ad-hoc deployment discovery.
5.7 Dashboard (packages/dashboard)
Next.js app. Data sources: the service's public endpoints (catalog, receipts) fetched live, and agent runs from runs/<id>.json served by a route handler when run locally; for the hosted demo a sanitized run is committed as public/demo-run.json. Pages: catalog and freshness per source, runs with verdicts and evidence, payments per rail with HashScan and Arcscan links (must show a live Arc payment from /v1/receipts), HCS commitments, and a receipt verifier. No auth. Satisfies Arc's "working frontend".
5.8 Hedera Harness PR
Fork hedera-dev/hedera-harness, branch from open PR #15 (rebase when it merges). Add a Tier 3.5 x402Probe chain validator: given a URL, assert an x402 v2 PAYMENT-REQUIRED for hedera:testnet, pay with the ephemeral signer through Blocky402 (HBAR route), and confirm settlement on the mirror node. Include tests, a section in docs/authoring-a-recipe.md, and before/after evidence: a recipe whose x402 endpoint could not be validated before passes with the validator. Opened before submission; linked from the README with a 15-second segment in the main video.
6. Data flow for one paid request (Hedera rail)
- Agent calls
discover; verifies the card's ML-DSA signature and the key binding against on-chain ERC-8004 metadata. - Agent builds the request, generates
reply_pk, seals{ request, reply_pk, payer, ts, req_nonce }, POSTs to/hedera/v1/scanwithX-VR-Count. - Middleware's price function validates envelope structure and returns 402 with
PAYMENT-REQUIRED(exact scheme,hedera:testnet, HTS USDC, computed amount). - Agent signs a Hedera
TransferTransactionwhose transaction id names the Blocky402 fee payer and retries withPAYMENT-SIGNATURE. - Middleware calls Blocky402
/verify; on success the handler runs. - Handler opens the envelope, checks
ts,req_nonce,payerand count, runs standardized queries and the Postgres read (60-second cap), unifies, computes freshness and risk, signs attestations, seals{ vaults, reports, attestations }toreply_pk, signs the receipt, and responds 200. - Middleware settles via Blocky402
/settleand addsPAYMENT-RESPONSEwith the tx id. - Service enqueues the HCS commitment; agent opens the response, verifies receipt and attestations, applies its own age check, reasons, and prints the decision.
The Arc path differs in steps 3 to 7: Gateway middleware, EIP-3009 authorization, batched settlement.
7. Error handling
- Upstream subgraph error, timeout, or indexing errors: source
unavailable; dependent verdictsunavailable; the receipt lists the failed source. - Sink cursor more than 5 minutes behind head: Substreams source
stale; same rule. Amended 2026-09-12: the figure is 20 minutes, not 5. The sink indexes finalized blocks only, so the newest block it can ever write trails the head by Ethereum's finality lag, 64 to 95 blocks, 13 to 19 minutes. Five minutes was therefore unsatisfiable in practice: the sink ran correctly and every vault it backed still readstale, which the service reports asunavailable, "no data" for data that is final and right. Twenty minutes (raised to 30 after measuring the running sink at an 80-150 block band) clears the lag while still bounding staleness, and it stays below the agent's own defaultmax_age_secondsof 900s for the common case. - Envelope malformed at quote time: 400 before payment. Envelope fails
ts, nonce, payer or count checks after verify: 422, no settlement, nothing charged. - Handler exceeds 60 seconds: 504, no settlement.
- Payment verify or settle failure: standard x402 4xx; agent retries once on transient errors, then reports.
- HCS submit failure: retried with backoff; responses never block on HCS;
sequencestays null until confirmed. - Chain head RPC failure: every source on that chain
stale.
8. Testing
- Unit (bun test): canonical JSON, seeded key derivation determinism, sign/verify and seal/open round trips for request and response, replay rejection (old
ts, reused nonce, payer mismatch, count mismatch), freshness thresholds, risk rules including "unavailable on partial data", price function, receipt and response hash recomputation. - Substreams:
substreams runover a fixed block range with a snapshot of expectedVaultMetricsfor two known vaults, one with an eth_call refresh;substreams guifor manual checks. - Integration (env-gated, live testnets): one paid sealed
scanon Hedera, one on Arc, onetable, receipt verification, HCS sequence confirmed via mirror node. - End to end:
scripts/demo.shruns discover, quote, paid scan on each rail, verification, and prints the decision; used for the video.
9. Deliverables and success criteria
- Real paid request on Hedera testnet via Blocky402, visible on HashScan, with an HCS commitment.
- Real paid request on Arc testnet via Gateway, visible on Arcscan and in the dashboard.
- A scan returning at least three Messari-covered protocols plus at least one ERC-4626 vault not covered by Messari, all from live sources with per-source block heights (amended per the Sept 5 verification gate if fewer than three Messari deployments are live).
erc4626-vault-metricspublished on substreams.dev and running through The Graph Market on two chains.- Sealed request and sealed response round trip, ML-DSA-65 receipt verification, and on-chain key binding demonstrated.
- Agent decision with citations, and a demonstrated refusal on a stale deployment using the agent's own age check.
- Harness PR opened.
- README with architecture diagram, payment flow, "what the standards made easier", the boundary statement, run instructions;
LICENSE(MIT);.env.example;SKILL.md; 2-4 minute video with no AI voiceover; continuous commit history from 2026-09-04.
10. Schedule, day-one de-risking, cut order
Revised 2026-09-09: the build started four days late. The operative schedule and cut list are in docs/superpowers/plans/2026-09-09-vaultradar.md (Global Constraints and "Workstreams and order"); the harness PR, HCS-14 UAID and the Falcon option are cut, and the Arc rail has a go/no-go on 2026-09-10 evening. The table below is the original plan, kept for the record.
| Date | Focus |
|---|---|
| Sept 5 | Accounts, keys and funding (section 12); repo skeleton; verify-deployments gate; HTS-USDC paid request through Blocky402 including association; substreams run with one eth_call against the Market endpoint and a hosted-sink deploy of any spkg into Neon |
| Sept 6 | Arc hello-402 through Gateway; service data layer and unify ∥ Substreams events module (one-prompt run recorded) |
| Sept 7 | Substreams stores, eth_calls, sink, spkg publish ∥ Hedera rail, PQ seal/open, receipts |
| Sept 8 | Agent: discover, quote, pay, open, verify, policy ∥ dashboard skeleton ∥ Arc rail wired to shared handlers |
| Sept 9 | HCS commitments, ERC-8004 registration with metadata, UCP profile, table tier; harness PR in the evening |
| Sept 10 | Integration tests on live testnets, dashboard live data, SKILL.md, README draft |
| Sept 11 | Architecture diagram, docs, dry run of demo.sh, fixes |
| Sept 12 | Video, submission |
Day-one de-risk items, in order: the HTS-USDC paid request through Blocky402; a Substreams run with an eth_call plus a hosted-sink deploy into Neon; the count of live Messari deployments.
Cut order if behind: HCS-14 UAID and the Falcon option; then the harness PR; then dashboard extras beyond the Arc requirement; then the Arc rail, only if its hello-402 has not succeeded by Sept 7. Never cut: table, receipts, sealing.
11. Out of scope
On-chain PQ payment signatures; zero-knowledge proofs of risk computation; A2A negotiation; scheduled or streamed payments; ENS; KEM key rotation and forward secrecy; mainnet money beyond a few dollars of USDC on Base for the optional upstream x402 demo.
12. Account and funding prerequisites (Sept 5, builder)
- Subgraph Studio API key.
- Two Hedera testnet ECDSA accounts (service payTo and agent) from the portal faucet; both associate HTS USDC
0.0.429274; agent account funded with testnet USDC from Circle's faucet (20 USDC per two hours). - Circle developer account and CLI login; Arc testnet USDC from Circle's faucet for the agent, deposited to Gateway; separate native Arc testnet USDC in the ERC-8004 deployer account for gas.
- Hashio JSON-RPC access for the Hedera ECDSA-alias deployer, gas limit set explicitly (300k).
- Neon Postgres database, publicly reachable, for the hosted sink.
- substreams.dev login via GitHub for publishing; The Graph Market API key.
- Anthropic API key for the agent; Fly.io or Railway account for hosting.
13. Dashboard user and admin views (added 2026-09-10)
Requested after the first dashboard landed. Two views are added to packages/dashboard, each with a promised scope and a labelled stretch. Promised items ship; stretch items ship only if they fit without touching the promised ones.
13.1 Admin view (/admin)
Promised: a service-operator screen backed by a new read-only endpoint GET /v1/admin/metrics on the service, gated by Authorization: Bearer <ADMIN_TOKEN> (401 otherwise). Response shape, all numerics as strings:
{ uptimeSeconds, startedAt,
rails: { hedera: { enabled, facilitatorUrl, healthy, checkedAt }, arc: { enabled, facilitatorUrl, healthy, checkedAt } },
hcs: { enabled, topicId, pending, submitted, failed, lastSequence },
settlements: { hedera: { count, revenueAtomic, asset }, arc: { count, revenueUsd } },
requests: { scan, table, rejected4xx, unavailableVerdicts, lastRequestAt },
deployments: [{ protocol, chain, chainId, status, headLagSeconds, lastQueriedAt, lastError }],
heads: { "<chainId>": { ts, block, ok, checkedAt } },
keys: { sigPubHash, kemKid },
identity: [{ chainId, agentId, onChainPubHash, matches }] }
Counters live in an in-process Metrics object fed by the handlers (requests, verdicts), both rails' onSettled hooks (settlements and revenue), the HCS queue (pending, submitted, failed, last sequence), the data provider (deployment query outcomes, heads), and the identity reader (on-chain hash match, refreshed every 10 minutes). Counters reset on restart; that is stated in the UI. Rail health probes hit each facilitator's supported-kinds endpoint, cached 30 seconds.
The dashboard page renders every field with status colours, refreshes every 15 seconds, and reads the token from the ADMIN_TOKEN server-side environment variable only.
Stretch: a settlements table with the last 50 receipt hashes and transaction links.
13.2 User view (/portfolio)
Promised: a portfolio owner's screen. The user pastes a vault list (<chainId>:<address> per line) and presses "Scan now". A dashboard API route runs the agent client server-side with the operator-funded agent account (AGENT_HEDERA_ACCOUNT_ID / AGENT_HEDERA_KEY, policy from POLICY_PATH), pays the x402 request for real, verifies the receipt, saves a run, and returns verdicts and decisions. The page shows verdicts, flags, decisions with citations, the payment transaction link, and the receipt hash, and lists history for the same vaults from previous runs. Purchases are rate-limited per client address (one paid scan per 30 seconds) and capped by the policy budget. If the agent keys are absent the page says so and offers the demo run.
Stretch, in order: (a) enter a wallet address and discover its ERC-4626 positions by balanceOf multicall over the service's free vault list (GET /v1/vaults?chainId=), (b) pay from a browser wallet on Arc via the Circle Gateway scheme if the SDK accepts an injected account, (c) the same via a Hedera wallet. None of the stretch items is claimed anywhere until it works end to end.
13.3 Boundary and honesty
The promised user flow is "buy a scan from the browser with a real x402 settlement"; the payer is the operator's agent account, and the README says so. Browser-wallet payment is described only if a stretch item ships.