Search projects, hackathons, sponsors and tech

    ← All projects

    DSAP Protocol

    Spending limits for AI agents: ENS-published rules, World ID approval, Intercepta-screened payments

    Demo

    About

    DSAP (Delegated Spend Authorization Protocol) lets you give an AI agent a budget without giving it your wallet. The agent never holds a key or builds a transaction. It asks DSAP to pay for an x402 API, swap on Uniswap, or send USDC, and every request gets one of three answers: allow, block, or ask a human. Each agent gets its own ENSv2 name, like scout.alice.agent-latch.eth, with its own Permissioned Resolver. Its rules live in text records on that name: an autonomous limit, a hard limit, a daily cap, and the allowed actions, tokens, and targets. The API reads the name every time it decides. If the name is missing or expired, or no policy is published, the agent cannot act. Tightening a limit writes to the name right away. Loosening one needs the owner's signature. Under the demo policy, 0.2 USDC runs on its own, 0.5 waits for the owner, and 2 is refused. When an action waits, the owner gets a World App push. In our World mini app they see the exact action, sign Approve with their World App wallet, and then complete a fresh World ID for Agents check. Our backend validates World's token before anything runs. If the owner denies, the approval expires, the check fails, or the action changed, nothing executes. For x402 payments, DSAP gets the seller's 402 quote and screens the payee with Intercepta before signing anything. A risky payee is blocked, and Intercepta's reasons show in the dashboard. A clean payee is paid with an EIP-3009 authorization that our own x402 facilitator settles on Sepolia. After a human approval, DSAP quotes and screens again, in case the seller changed its payee. Agents connect through an MCP server, so Claude can browse a merchant catalog, pay for resources, and request swaps, all within the owner's rules. Everything runs live on Ethereum Sepolia, and the owner side runs in World App.

    How it's made

    World: the owner signs in with MiniKit walletAuth (SIWE). A desktop signs in by showing a QR code that the phone confirms. Approvals arrive as World App notifications. Approving takes a wallet signature bound to that one approval, then an RFC 8628 device authorization against World ID for Agents using our confidential client. The API polls the token endpoint and verifies the ID token against World's JWKS. It checks iss, aud, exp, and acr (orb), and requires auth_time to be after the check started. Then, under a Postgres row lock, it rechecks the approval, the owner, the action, and the ENS name before executing. The client secret, the device code, and World's sub never leave the backend. We dropped World's browser authorization-code flow after about 5.5 hours, because the sandbox auto-approved inside the browser that started the request. Mocked identities mint a new sub on every proof, so we bind the human to their World App wallet instead. ENSv2 on Sepolia (viem with hand-written ABIs): for each owner we deploy a UserRegistry proxy under agent-latch.eth. We register each agent label in it with Enhanced Access Control role bitmaps, deploy a Permissioned Resolver per agent, and write the agent's ETH address and policy text records. The spending decision is read from ENS at request time, so ENS is the source of truth, not decoration. The policy engine is a pure function of those records; it does not import World, Uniswap, or Intercepta. Registration takes about 80 seconds on Sepolia, so it runs as a background job with REGISTERING and FAILED states shown in the UI. Uniswap: LatchGateHook is a v4 beforeSwap hook built from the official v4-template. A permit binds one action id, the pool, the direction, the exact amountSpecified, a humanApproved flag, and a deadline, and the latch key signs it (EIP-191). The hook checks that signature, marks the action id used, and emits LatchedSwap. Six Foundry tests pass. It is deployed on Sepolia at 0x9A3b22908730Fda371E546d9C7ED30e063E28080, on its own USDC/WETH pool. A permitted 0.1 USDC swap landed; the same swap with no permit reverts PermitMissing(). Live agent SWAP still goes through v3: one SwapRouter02 exactInputSingle (USDC to WETH, 0.01% pool), a 95% QuoterV2 floor, and an exact-amount approve. The agent supplies only the amount. x402 + Intercepta: we implemented both the x402 v2 seller and the facilitator (/facilitator/verify and /facilitator/settle), using EIP-3009 transferWithAuthorization on Circle's Sepolia USDC. Before the executor signs, the API sends the quote's payTo to Intercepta's quick scan. Any toxic score or risk trait blocks the payment, and the reasons go into the audit log. A payment approved by a human is quoted and screened again before it settles. Stack: Bun + TypeScript monorepo. The API is Elysia on Render, with Postgres on Neon through Drizzle; migrations apply on startup. The dashboard and the World mini app are one React + Vite app on Vercel, which calls the API through a typed Eden client and an /api rewrite. Agents connect through a stdio MCP server (list_merchants, quote_resource, pay_resource, request_swap, wait_for_approval). Each agent authenticates with an alk_ key stored as SHA-256. An agent that registers a signing address must also sign every request with EIP-191 and a nonce. The policy engine, executors, and signers are separate packages. Only the executor holds a key. Foundry for the hook. A ping every 5 minutes keeps the free Render instance awake.