
Handler
Hire an AI agent, set its on-chain allowance and rules, and let smart contracts enforce them.
Demo
About
More people are handing AI agents real spending power: paying for services, paying other agents, running subscriptions. Today the guardrails are thin. Existing agent wallets offer fixed spending caps at best, and those caps know nothing about who the agent is paying. Once an agent has a key, you're trusting it completely. Handler lets anyone hire an AI agent the way you'd hire an employee. You give it an allowance and a set of rules, and those rules are enforced by smart contracts on Base mainnet, not by a server you have to trust. How it works 1. Sign in with your own wallet (Sign-In with Ethereum). Any wallet works. A factory contract deploys a personal Handler wallet for you. 2. Hire an agent. A 3-step flow: pick an agent, set a daily allowance with a slider, and turn on the permissions you want. Behind the scenes, the agent gets a limited session s an on-chain policy with: - daily and per-transaction USD spending caps (priced live from Chainlink price feeds, with sta - a co-sign threshold, above which a human has to approve the payment - a minimum trust level must have 3. The agent spends, and the contract checks every payment. Each attempt goes through the policy checat breaks a rule isn't just reverted silently: the contract emits an ExecutionBlocked event, so every blocked attempt is a ree in a block explorer andin your activity feed. Trust-aware rules. Handy's on-chain identity andreputation from the ERC-8004 Identity and Reputation registries and puts it in one of three tiers: . Unregistered addressescount as Flagged. So your rules can change with who's on the other end: paying a verified agentng an unknown address isblocked. Tiers come only from the registries. There are no manual overrides or seeded data. 5. Human-in-the-loop with Ledger. Payments above your co-sign threshold go into a pending queue. Yproval sheet ("Riley wantsto pay $X to Y for Z") and can deny with one tap, or approve by signing on your Ledger device. Theits own payments. 6. Freeze anytime. One tap stops an agent instantly. Security invariants: a session key can never change its own policy, withdraw nds to itself, or targetde has its own customerror, so nothing gets lumped under a generic failure. What we built - Contracts (Solidity/Foundry): HandlerWallet (the policy engine), TrustReader (ERC-8004 registry readsink AggregatorV3), andHandlerWalletFactory (one wallet per owner via CREATE2). Covered by unit, fuzz, and invariant testd deployed on Base mainnet. - Backend (NestJS, Prisma/Postgres, viem): an indexer that turns on-chain events into a readable aSE endpoints for agents,policies, trust, activity, approvals, and prices. Every app route requires a SIWE session and is scopd by unit and e2e tests. - Frontend (Next.js, wagmi, TanStack Query, Ledger Device Management Kit): Payroll home screen (age, spent today vs.allowance, trust badges), the hire flow, the activity feed, the agent detail page, and the Ledger co-owner actions are signedin the browser. The app never touches your private key, and no crypto jargon or hex addresses appear on screen. - Real agents doing real work: "Riley," a working agent that pays other agents, and a "villain" agent that tries to pay an unverified address andgets blocked on-chain. Every approved or blocked moment in our demo is a real transaction through the real checks, with no demo-only code paths.
How it's made
Handler is a pnpm monorepo in three parts with a one-way dependency: Solidity contracts → generated TypeScript types → a NestJS API and a Next.js web app. The API and web app only talk over REST and Server-Sent Events (SSE), so the frontend never scans contract logs itself. Contracts (Foundry, Solidity, OpenZeppelin), deployed on Base mainnet - HandlerWallet is the policy engine. One internal _evaluate() runs every agent payment through a fixed order of checks: frozen? → what kind of target (plain transfer, known swap router, unknown contract)? → counterparty trust tier → USD value vs. per-transaction cap → daily allowance → co-sign threshold. - There are two entry points on top of that same check. execute() reverts with a specific custom error; every failure mode has its own error (CounterpartyBelowTier, ExceedsDailyAllowance, and so on). tryExecute() doesn't revert on a policy failure: it emits ExecutionBlocked(sessionKey, reason, usdValue) and returns false. That's what makes a "blocked" payment a real, indexable, explorer-visible transaction instead of a failed one that leaves no trace. - Payments above the co-sign threshold become a Proposed approval with a deterministic id. Only the owner can approve() or deny() it. - The daily allowance uses a lazy epoch roll: no keeper and no cron. The spend window resets during the next payment's check. - The session key is locked down: it can't target itself, change its policy, or withdraw to itself. - HandlerWalletFactory deploys one wallet per owner with CREATE2, so any wallet can sign in and get its own Handler. - Tests cover unit, fuzz, and invariant cases, and we did a Slither pass. Partner tech: Chainlink. PriceConverter reads Chainlink AggregatorV3 feeds with staleness, bad-price, and bad-timestamp checks, and converts everything to 8-decimal fixed-point USD. That 8-decimal integer format is the single money format across contracts, API, and UI until the final render, so no floats touch dollar values anywhere. Without Chainlink, "$50/day" would be meaningless on-chain. With it, the allowance is enforced in real dollars. ERC-8004 trust layer. TrustReader reads the real ERC-8004 Identity and Reputation registries. It averages an agent's feedback scores and assigns a tier: Verified (enough feedback with a high enough score), New, or Flagged (unregistered, or the read failed). One awkward detail: the Identity registry only maps agentId → wallet, not wallet → agent. So we added a permissionless syncAgent(agentId) that re-reads the canonical registry and caches the reverse lookup. Anyone can call it, but it can only record what the registry currently says, so there's still no override path. Hacky, notable bit #1: we deployed straight to Base mainnet because ERC-8004 has no deployed code on any testnet. Local development runs Anvil forked from Base mainnet, so tests hit the real registries and real Chainlink feeds. A Slither review also surfaced a real griefing vector: the reputation registry is permissionless, and anyone could submit feedback with valueDecimals > 18. That made the normalization math underflow and revert, which bricked every payment to that agent. We now skip malformed entries (the same guard exists in the API) and redeployed. Codegen. @wagmi/cli's Foundry plugin generates ABIs and types into a shared @handler/contracts workspace package used by both apps, so a contract change breaks the TypeScript build instead of breaking at runtime. API (NestJS 12, Prisma 7 on Postgres, viem) - Indexer: no queue infrastructure. A @nestjs/schedule interval polls contract events every 3 seconds from a stored block cursor and maps them into activity and pending-approval rows. Every row is idempotent by a natural key (the transaction hash, or the deterministic approval id), so re-running over the same blocks is always safe. - Live updates: an SSE stream pushes new activity to the app. - Auth: Sign-In with Ethereum (SIWE) sessions guard every app route, reads included, and scope each query to the signed-in wallet. Only /health, /prices, and /auth are public. - Agents: they're server-side actors, each holding only its own session key, never the owner's key, and calling tryExecute directly. "Riley" pays a subcontractor agent. A "villain" actor tries to pay unverified counterparties so the on-chain block is visible. You hire either one with the same owner-signed hireAgent() transaction any user makes. - Plain English: a sentence builder turns policies and events into lines like "Blocked: tried to pay an unverified recipient," so no hex addresses or jargon reach the screen. Partner tech: Ledger, used in two places: 1. Human-in-the-loop co-sign in the browser. No maintained wagmi connector for Ledger exists, so we skipped wagmi for this step and built the flow with the Ledger Device Management Kit plus device-signer-kit-ethereum over WebHID. It connects the device and tries a handful of standard derivation paths until one matches the signed-in owner's address. Then it builds and serializes the approve(id) transaction itself with viem, sends the raw unsigned bytes to the Ledger to sign, rebuilds the transaction with the device's signature, and broadcasts it with viem's sendRawTransaction. (Hacky, notable bit #2.) The result: a payment above your threshold physically can't go through without a button press on your Ledger. 2. Agent secret custody. The API's Docker entrypoint can decrypt a Ledger Key Ring ciphertext (using @ledgerhq/wallet-cli) into the process environment at boot, so the agent's session key doesn't have to sit in plaintext deploy config. Web (Next.js 16 App Router, React 19, Tailwind 4, wagmi 3 + viem, TanStack Query) - All reads go through typed API hooks. All owner writes (hire, freeze, approve, deny) are signed in the browser. The backend never holds or asks for the owner's key. - A 401 response anywhere invalidates the session and sends you back to sign-in. - We built our own small design-system component library for the fintech feel: Payroll home, the hire wizard, the activity feed, agent detail pages, and the approval sheet. It respects prefers-reduced-motion. Deployment: production Dockerfiles for the API and web app plus one docker-compose.prod.yml, deployed as a single resource on Coolify.