AI advisor that puts idle stablecoins to work at rates two sources agree on. You sign every step.
Crosscheck is an AI advisor for idle stablecoins. Connect a wallet, say in plain English how careful you want it to be, and it finds you a lending position on Ethereum mainnet — without ever taking custody of your money.
Most AI DeFi copilots have the same two holes. They read a single data source and present the number as fact, and they reason about your balances on an inference endpoint you cannot see. Crosscheck closes both.
Every rate is read from two independently-indexed subgraphs on The Graph — the Aave-native one and the Messari standardized one — and reconciled against each other. When they disagree beyond tolerance the market is rejected as EVIDENCE_INCONSISTENT, rather than silently picking a winner. A "corroborated" badge only appears when both sources agreed. It also uses The Graph's Token API to discover what your wallet actually holds, and per-account subgraph history to compute what you have really earned rather than what a rate implies.
The reasoning step runs through the 0G Compute Router in private trust mode, which routes only to TEE-attested providers. It fails closed: with no private tier available it returns an error instead of quietly downgrading to an unattested provider, and the interface never claims private inference without the attestation metadata to back it up.
Critically, the model never makes the risky decision. Eligibility, risk ceilings, liquidity floors and allocation caps are all deterministic code. The model only explains a choice among candidates that already passed your mandate — it cannot invent an option, raise a cap, or relax a risk tier. Ask for 40% of 1,000 USDC and the most it will ever offer is 400.
Execution is non-custodial and resumable. Transactions are prepared unsigned and handed to your wallet to sign, step by step. If the position you want is in a different asset than you hold, it routes a swap through the Uniswap Trading API first — and the deposit then uses the swap's confirmed on-chain output rather than its quote, because those are not the same number. Today it deposits into Aave V3 and reviewed Morpho vaults.
Without API keys the app runs on fixtures and labels every source as a fixture in its developer panel. A canned response is never dressed up as live data.
Live: https://crosscheck-defi.vercel.app
STACK
Next.js 16 (App Router) and React 19 in TypeScript, Tailwind v4 with shadcn primitives, wagmi + viem + RainbowKit for wallet and chain reads, zod for every boundary, TanStack Query for client caching, bun as package manager and runtime, deployed on Vercel. All orchestration runs server-side; the browser never receives an API key.
ARCHITECTURE
Everything external sits behind a port with two implementations: a live adapter and a fixture. Each port reports its own origin ("live" or "fixture"), and the UI surfaces that per source — so a canned response can never be mistaken for real data. Environment variables are validated per concern rather than all at once, so an unfunded 0G key does not stop the market-data path from going live.
The core rule is that the LLM cannot influence policy. Eligibility, risk tiers, liquidity floors and allocation caps are deterministic functions with unit tests. The model receives an already-filtered candidate list and only produces an explanation. It has no path to add a candidate or loosen a bound.
THE GRAPH — corroboration, and two findings we're passing upstream
Rates come from two independently-indexed mainnet subgraphs, queried live through the gateway: the Aave-native one and the Messari standardized Lending 3.1.0 one. They are reconciled per market; disagreement beyond tolerance rejects the market outright.
Building the second adapter surfaced two things worth reporting. First, Messari's rate field is APR, not APY, despite the schema documenting it as "percentage APY" — applying per-second compounding reproduces the on-chain APY to five decimal places on both USDC and USDT at block 25611966. Any adapter reading that field naively is off. Second, utilizationRate is not comparable across sources: the subgraph field and on-chain totalVariableDebt/totalAToken use different denominators (0.8861 vs 0.8947 for USDC at the same block).
We also use The Graph's Token API (via Pinax) for wallet discovery, and per-account subgraph history to compute realised yield from actual flows rather than inferring it from a rate.
The Token API had the most surprises. Auth is the raw key in X-Api-Key — a Bearer token is rejected unless it's a JWT minted from that key, and the key is what a user actually has. The free tier caps a page at ten rows and 403s anything larger, so paging is mandatory; we bound it at ten pages and report truncation instead of silently dropping. Balances come back ordered by recency and are mostly airdropped junk — one real mainnet wallet led with "Ten Commandments" and "From Vitalik". Nothing is filtered by name or heuristic: valuation comes from Aave's oracle, and unpriceable tokens are counted but never named or guessed at (one wallet had 100 of them).
The notable bug: our first cut intersected the Token API results with chain reads, keeping only tokens present in both — and dropped every stablecoin from a wallet that plainly held them, because the recency pages missed them. The fix was to union the sources. Corroboration decides what you trust, not what you're allowed to see.
0G — private inference that fails closed
Reasoning runs server-side through the 0G Compute Router with X-0G-Provider-Trust-Mode: private, which routes only to TeeML providers. The result is read back from x_0g_trace.tee_verified and rendered as evidence; the privacy sentence shown to the user is derived from that metadata, so the copy cannot overstate what actually happened.
Two things we only learned by probing live: verify_tee is a body field, not a header (we had it as a header first and it silently did nothing), and testnet cannot prove this gate at all — its only TeeML model is an image model, so a funded mainnet key is the only way to demonstrate private text inference. We run glm-5.2 specifically because it was the only TeeML text model on mainnet with more than one provider, which makes it the least likely to 503 mid-demo. Falling back to a different model is acceptable; falling back to a weaker trust tier is not, so an unavailable private tier returns 503 rather than quietly downgrading.
UNISWAP — the swap leg
When the source asset differs from the destination, we route through the Trading API: /check_approval, then /quote with permitAmount EXACT, then the wallet signs the returned Permit2 EIP-712 payload, then /swap with the calldata submitted unchanged. The approval targets Permit2, not the swap router.
Two non-obvious details drove the design. Quote output drifts between consecutive calls, so a refreshed quote always gets a fresh signature and permits are never reused — which forced a server-side store for the raw quote object, because /swap needs the exact object /quote returned and the Permit2 signature binds to that quote id. And the permit has to be priced immediately before signing rather than at plan creation, because approving Permit2 on chain takes longer than a quote stays fresh.
Execution is a resumable sequence, not a bundle: the real output of a swap isn't known until its receipt, so the deposit step reads the confirmed balance from chain logs rather than trusting the quoted amount. A failed step survives a page refresh.
ONE MORE HACK
Citations are capped at three. One citation per holding overflowed the model's token ceiling on an eight-asset wallet and truncated the JSON mid-array, which failed the whole answer — a cap was cheaper and more predictable than streaming repair.
TESTING
523 unit tests and 10 Playwright specs, including one that asserts no credential is ever served to the browser. Probe scripts re-verify each external integration against the live APIs and refresh the recorded evidence.

