
Xander
Graph-native risk engine that escalates to World ID Selfie Check only when real evidence earns it.
Demo
About
THE PROBLEM Every large token claim or airdrop has fought the same battle after the fact, with no reasoning a flagged user can inspect: LayerZero flagged over 800,000 addresses as sybils in one sweep; Linea's 2025 airdrop initially flagged 50.45% of eligible wallets before walking the number back; Arbitrum's shared-funding-source heuristic restricted real users who'd withdrawn from the same exchange hot wallet as an actual sybil ring. Detection happened once, inside a backend nobody could see into, with no appeal beyond a forum thread. The same failure mode reappears the moment an AI agent, not a human, is the one holding the wallet. An agent that can move funds is usually handed a key and a system prompt telling it what not to do — and a prompt is a suggestion, reachable by anything that can write into the model's context. The limit was never actually enforced; it was described. WHAT WE BUILT Xander is two things built as one system. The foundation is a Graph-native risk engine: every fact behind a decision — a transfer, a deposit, a borrow — is pulled live from The Graph and stored with its source, deployment, and block number attached, then scored by five named, auditable features (funding correlation, timing, wallet-age similarity, shared counterparties, protocol-behavior similarity) combined by weights stored in a database table, never a hardcoded number. On top of that foundation sits a general-purpose agent trust and authorization runtime: every actor — wallet, human, or AI agent — gets a 7-dimension trust context built from real evidence, where every dimension is either a known value or explicitly UNKNOWN, never a silently-assumed zero. Trust resolves to a scoped, expiring capability, never a standing key, and that capability is enforced at a real execution boundary — including a live on-chain role check, not just a database flag. Nothing here is a simulation: the risk score gates a real World ID Selfie Check when a wallet needs it, agents mint real ENS identities and real on-chain reputation, and they can pay each other over a real, working x402 machine-payment flow. HOW YOU USE IT A wallet submits a claim; evidence is fetched live from The Graph; the risk engine clusters and scores it; only a claim that actually lands in the escalation band ever sees a Selfie Check, bound to that exact wallet and claim so a proof can't be replayed onto a different one. In parallel, an operator can register an agent, watch it move through a verification lifecycle (DRAFT → VERIFYING → HUMAN_BACKED → ACTIVE), and see the exact capability it holds and why. Every proposed action resolves to ALLOW, LIMIT (an attenuated grant, not a refusal), CHALLENGE, REVIEW, or BLOCK — computed by policy, never asserted by the agent. If something goes wrong, an incident opens automatically, containment (revoke or restrict) happens before an AI investigator is even asked to look at it, and the investigator's own recommendation can only tighten that decision, never loosen it — enforced in code, not just policy. A human operator carries the final authority on a phone: approve, deny, limit, freeze, or revoke, over a channel that's bound to that operator's device and can't be replayed. INTEGRATIONS The Graph is the evidence layer for the entire product — Token API for wallet history, Standardized Subgraphs (one Messari schema query reused across every lending/DEX/yield protocol tagged into it, zero protocol-specific code), Substreams for a live, reorg-safe, cursor-resumable stream off Base Sepolia, and Subgraph MCP powering an investigation agent whose every claim is checked against its own tool-call trace before it's trusted. World ID Selfie Check is the escalation step, fired only once risk crosses a real threshold. ENS (ENSv2) gives every agent its own on-chain identity and revocable role, deployed and live on Sepolia. ERC-8004 supplies real, cross-verified on-chain reputation instead of a hardcoded "unknown." x402 lets agents transact machine-to-machine in real Base Sepolia USDC, policy-gated at every step.
How it's made
TypeScript throughout. The backend is Express + Prisma/Postgres + Redis/BullMQ, with a real Rust/WASM Substreams module bridged into Node over @connectrpc/connect-node, Temporal (@temporalio/*) running the durable workflows behind authorization, capability mutation, and incident response, and Mastra (@mastra/core, @mastra/mcp) driving the investigation agent over Subgraph MCP. The frontend is Next.js 15 (App Router) with a console for operators and a separate mobile-first "Remote Authority" app, sharing typed API clients that deliberately do not share an auth model — more on that below. Roughly 33 Prisma models span the two halves: the original evidence/risk schema (wallets, clusters, evidence events, risk weights) and the newer actor/trust/capability/agent/incident/operator schema, wired together at one locked interface (getOrComputeClusterRisk / refreshWalletEvidence) so the newer layer is provably additive rather than a rewrite. THE GRAPH: WHERE EVIDENCE BECOME PROVENANCE, NOT ASSUMPTION A risk score is only as trustworthy as the evidence under it, so nothing is inferred. The DeploymentRegistryEntry table is the single place "which protocols we support" lives — adding a lending market is an INSERT, never a code change, because the same lending-cdp query module runs unmodified against every deployment tagged into that Messari schema family. Substreams delivery is cursor-resumable and reorg-safe; a chain reorg triggers handleUndo, and cursor state is checked and resumed correctly, verified against a real 1,521-event Base Sepolia stream. The investigation agent doesn't get to assert anything for free either — every sentence in its report is checked against its own recorded tool-call trace, and an uncited claim is rejected before the caller ever sees it. WORLD ID: A BIOMETRIC STEP THAT ONLY FIRES WHEN IT'S EARNED The RP-signature is signed server-side with a secret signing key that never reaches the client, so a request can't be forged into looking pre-approved. The proof's signal is bound to a hash of the wallet and the specific claim (or agent context) it's for, so a completed proof can't be lifted onto a different wallet or a different claim by the same wallet. Every nullifier is stored and checked for reuse before a claim resolves. A timed-out or invalid verification never falls through to an approval — it holds exactly like a failed Graph query does. This was run for real: a phone running the real World App completed a real Selfie Check that this backend's verification logic accepted end to end. ENS: IDENTITY THAT CANNOT GRANT ITSELF AUTHORITY Every agent mints a real subname under a deployed Sepolia registry (0x8993Df8b8f5C10d9B3d157B7191431429c51F039), and freezing or revoking an agent performs a real on-chain role revocation, not just a database flag. But identity and authority are kept structurally separate: minting a name grants zero capabilities on its own — trust and capability come from a completely different evaluation path. The database stays authoritative for security decisions; the chain is a public, checkable mirror of them, and a reconciliation pass (built in the hardening phase) treats any case where the chain shows more authority than the database as an urgent, revoke-only drift to repair. ERC-8004: REPUTATION THAT REFUSES TO BE INVENTED Three live Sepolia registries — Identity, Reputation, Validation — are read directly over RPC, cross-checked against each other (getIdentityRegistry() on both the Reputation and Validation contracts must return the same Identity address, or the deployment isn't trusted as one coherent system). The deployed contracts turned out to diverge from the EIP draft in shape (bytes32 tags instead of string, a 2-value not 3-value getSummary), so the client was written against what's actually on-chain. An agent with no feedback and no validations returns null for its reputation dimension, never a synthetic 0 or 1 — and where both exist, validations are weighted over feedback (0.6/0.4), because a validation is a check a contract actually ran and feedback is just an opinion anyone can post. X402: MACHINE-PAYABLE, STILL POLICY-GATED The live x402.org facilitator speaks protocol v2, which is wire-incompatible with v1, so the client had to be built against the real deployed version rather than the spec's first draft. Payment moves as a real Base Sepolia USDC EIP-3009 authorization to a receive-only address Xander never holds the key for. Every outcome — including a refusal — is written to a payment ledger. An agent whose capability doesn't cover the requested spend gets a 403 (refused by policy), never a 402 (payment required) — the two failure modes look similar but mean opposite things, and conflating them would let a denied agent think it just needed to pay more. THE CAPABILITY MODEL, AND WHY NOTHING HOLDS A STANDING KEY An agent never receives blanket authority. It receives a Capability, scoped, expiring, and re-evaluated continuously — a live piece of on-chain evidence can attenuate or suspend it mid-flight (mutateTrustForWallets), and a first-time evaluation or an improvement in trust is never allowed to silently expand authority on its own. Enforcement at the actual execution boundary returns one of three outcomes — CONFIRMED, FAILED, or UNKNOWN — and an unreachable boundary resolves UNKNOWN, which blocks exactly like a FAILED does. A dependency-policy table enumerates every external dependency the system relies on and states, for each one, explicitly, whether losing it blocks authorization or degrades safely — there is no IGNORE value permitted anywhere in that table. OTHER THINGS WORTH MENTIONING The test suite (749 tests, zero vi.mock() module mocks anywhere) runs its files serialized rather than parallel — not for coverage reasons, but because 30+ files sharing one real Postgres and one real Redis produced a steady drip of failures that had nothing to do with the code under test, and serializing removed the cause instead of chasing each symptom individually. One of those symptoms was a genuine bug, not a flake: an "unknown wallet" test screened a literal 0xdead...beef-style address, which turned out to have 82 real evidence rows from a live Token API fetch and silently inverted the assertion to a pass. It now generates a random address per run. Fixture rows are deduplicated forever on re-seed but originally kept their original createdAt, so every fixture wallet quietly resolved to PENDING_REVIEW from wall-clock time alone six hours after seeding — fixed with a --refresh-only seed mode that re-stamps freshness without touching the relative timing every fixture scenario is encoded in. The upstream Subgraph MCP server doesn't compile on Windows as-is (tokio::signal::unix is called unconditionally despite being #[cfg(unix)]-gated) and needed a ctrl_c() fallback patched in locally. And a known-funder registry — labelled exchange/bridge addresses scored separately from unlabelled ones, max(unlabelled, labelled × 0.15) — exists specifically to stop the Arbitrum false-positive pattern from reproducing here: a benign shared funder is heavily down-weighted, but a ring still scores full risk even if it routes one decoy transfer through an exchange first.