Search projects, hackathons, sponsors and tech

    ← All projects

    CustodIA

    Chat-native DeFi agent that proposes; you sign an EIP-712 boundary on ENSv2, policy enforces it

    Demo

    About

    CustodIA is a runtime, not a bot. You type an intent in a chat (web, Telegram or WhatsApp): "Keep ETH/USDC inside a 5% drawdown", "Protect me if ETH drops 15%", "Buy 500 USDC of ETH". The agent researches with live The Graph data, buys a risk analysis with an x402 micropayment on Hedera, and answers with a generated, schema-validated interface: sliders, toggles and charts whose bounds are clipped to the risk context, never invented by the model. You adjust the boundary and sign it once as an EIP-712 mandate with typed constraints (max trade, max notional, max drawdown, allowed assets, rebalance permission, expiry). The mandate hash is anchored on an ENSv2 task subname ({task}.{you}.custodia.eth) and the agent receives a role scoped to a single text record on that name. From then on a pure, tested policy engine evaluates every action against the mandate before anything moves, and you can revoke the agent's role on-chain at any moment. After revocation the agent's next write reverts. When you want real execution, you deploy a TaskVault from the task page and fund it with Sepolia ETH. Orders typed in chat are quoted on Uniswap V3, approved per action by a policy signer, executed by an execution signer that cannot withdraw, and the contract itself enforces the assets, caps, cooldown, expiry and router you signed. The agent can also act alone inside the boundary: it sells to USDC when the drawdown limit is hit and proposes rebalances over chat that you confirm with a YES. The point is separating intelligence from authority. The LLM only proposes. Bounds come from deterministic risk numbers computed from live market data. The human signs the envelope. The policy engine, the ENSv2 role and the vault enforce it. Requests outside the envelope become a "needs a new signature" screen instead of an execution. Everything runs on Sepolia and Hedera testnet with real transactions, live data and no mocks on the qualifying path.

    How it's made

    pnpm monorepo in TypeScript: Next.js 15 web app, a worker, a Hono risk API, a delegated signer subprocess, and packages for schema (Zod), agent, policy, graph, ens, registry, vault, runtime and db (Drizzle on Neon Postgres, PGlite for tests). Contracts are Solidity 0.8 with Foundry. The agent is an OpenAI tool loop (chat.completions.runTools with Zod function tools, gpt-5.6-luna). Its only way to produce UI is an emit_ui_spec tool whose output is validated against a frozen Zod schema and then clipped server-side to the RiskContext, so a bound outside the risk envelope is a validation error, not a fallback. The Graph: market context comes from the Uniswap V3 Messari standardized subgraph through the gateway. Prices are derived from the pool tick (10^(dec1-dec0)/1.0001^tick), realized vol from hourly snapshots, every snapshot carries its block, cached 30 s. The pool constant is verified by name at runtime because the first one we hardcoded was the wrong fee tier. Hedera: the risk API is gated with @x402/hono and @x402/hedera (pinned 2.25.0), priced in HBAR, settled through the Blocky402 facilitator. The Hedera key exists only in apps/signer: the agent sends the payment-required header on stdin and gets a payment-signature back on stdout, calls are serialized, and the policy engine must allow the pay_x402 action before the signer is asked. Receipts are checked against the mirror node. ENS: each task is a free wildcard subname on a PermissionedResolver deployed through the VerifiableFactory on Sepolia. The operator writes owner, mandate hash, agent and status text records; then authorizeTextRoles grants the agent a role for xyz.custodia.status only. Revoking that role makes the next agent write revert with EACUnauthorizedAccountRoles, which pnpm verify:ens proves live. The trap that cost us the most: authorize* takes a DNS-encoded name while setText takes a namehash. Mandates are EIP-712 typed data bound to an immutable stored proposal (proposalId plus constraints hash), so what you sign is exactly what was rendered. Non-authorizable intents (needs_human, compare_strategies) are refused with a 409. Runtime durability: every chat turn is a run with an append-only event stream and a job on a Postgres queue leased with FOR UPDATE SKIP LOCKED; agent runs are never auto-retried; channel deliveries go through an outbox. Telegram and WhatsApp are transports paired to the wallet, signing always happens on the web. Hacky bits worth mentioning: a dependency-free Chrome DevTools client drives the end-to-end demo with screenshots across all ten prompt cases; the pre-signature receipt is composed server-side so the UI numbers match the mandate byte for byte; Neon's IPv6 records stalled cold connections on our box, so the db client forces ipv4first; the demo wallet was stocked by swapping through the Sepolia Uniswap V3 pools from a script because the faucets only give ETH.