
Axis
AI treasury agent that trades on Uniswap and asks a World ID-verified human before big moves
Demo
About
AXIS is an autonomous treasury agent with a human in the loop for the moves that matter. AI agents are starting to hold money, which forces an awkward choice: either a human signs every transaction (so the agent isn't really autonomous), or the agent can do anything (so one bug or bad decision can drain the wallet). AXIS puts a policy between the two. The agent manages a treasury of ETH and USDC on Sepolia and keeps it near a 50/50 target. Every cycle it values the portfolio with a live Uniswap quote. When the allocation drifts more than 10% from target, it proposes a rebalancing trade, and a policy engine sorts the trade into one of three tiers: - Small trades (≤ 0.002 ETH) execute on Uniswap immediately, no human needed. - Medium trades (up to 0.02 ETH) are held until the treasury owner completes a fresh World ID verification for that specific trade. The backend validates the proof itself; the browser's word is never trusted. - Large trades, or anything over the 24h budget, are blocked outright and nobody can approve them. Before any of that, the agent is switched off. The owner turns it on by connecting Uniswap Wallet and signing a gasless EIP-712 "mandate" that sets a max per trade, a daily limit and an expiry. The policy enforces those limits, and the owner can revoke the mandate at any time with another signature from the same wallet, which pauses the agent immediately. A World ID approval is bound to a single proposal and can only be used once. It has to be recent (≤ 120s), and it must come from the same verified human who owns the treasury, matched on World ID's pairwise subject. If the user cancels, World ID returns an error, the approval window expires, or a different person verifies, the proposal is rejected and no transaction is sent. The dashboard shows treasury balances and allocation, the active delegation, trades waiting for approval, charts of allocation (with the target band and rebalance markers) and treasury value over time, a rebalance history with allocation before → after, who authorized each trade (the agent alone or a World ID-verified human) and Etherscan links, plus a live activity log of every decision and its reason.
How it's made
Backend: TypeScript on Node with Express, viem for chain access, and jose for JWT verification. The agent loop, policy engine, delegation, World ID and Uniswap client are small separate modules. State is kept in memory to keep the hackathon build simple. Uniswap: all pricing and execution go through the Uniswap Trading API on Sepolia. The agent calls /quote both to value the treasury (a small probe quote gives the USDC/ETH price) and to size each trade. For native ETH input it sends the swap straight away. For USDC input it checks the Permit2 allowance, sends a one-time approve if needed, signs the quote's permitData (EIP-712 PermitSingle) with the agent key, calls /swap and broadcasts the transaction. Without extra work, the API routed our trades through both v3 and v4 pools on Sepolia, and we show the route in the UI. World ID for Agents: we integrated the sandbox as a standard OpenID Connect provider. Each medium trade starts its own authorization-code flow with PKCE (S256), a random state bound to the proposal id, a nonce, prompt=login and max_age=120 to force a fresh verification. On the callback, the backend exchanges the code with the client secret (client_secret_basic), verifies the ID token signature against World's JWKS, and checks the issuer, audience, nonce and auth_time before it lets the agent execute. The first verified pairwise sub becomes the treasury owner, and later approvals must match it. The client secret never reaches the browser. Delegation with Uniswap Wallet: the frontend uses EIP-6963 to find the Uniswap Wallet extension even when other wallets are installed, and asks it to sign an EIP-712 Mandate { owner, agent, maxTradeWei, dailyLimitWei, expiry, nonce } with eth_signTypedData_v4. The backend verifies it with viem's publicClient.verifyTypedData. That covers plain EOAs, and should also cover a Uniswap smart wallet (Calibur via EIP-7702, checked through ERC-1271), though we only tested it with a plain EOA. The nonce is single-use, and revoking requires a personal_sign from the same owner. Frontend: plain HTML, CSS and JavaScript served by the backend (no build step), with Chart.js for the allocation and value charts. It supports light and dark themes and works on phone-width screens. Hacky bits worth mentioning: - The Uniswap extension refused to sign because the wallet's chain didn't match the chain ID in the typed data, so the page now asks the wallet to switch to Sepolia, signs for whatever chain the wallet is actually on, and the backend verifies against that chain. Replay is still prevented by the agent address and the single-use nonce. - Testnet liquidity is thin and asymmetric (ETH→USDC and USDC→ETH quotes implied very different prices), so the agent always values the treasury with the same probe direction to keep drift measurements consistent. - World ID needs an HTTPS redirect URI, so we ran the local server behind a cloudflared quick tunnel and serve a sector identifier document from the backend.