
Mamoru
Non-custodial savings account on Base. Passkey Safe, scoped Uniswap session, live to 25 USDC.
Demo
About
Mamoru is a non-custodial savings account on Base. You own a Safe smart account through a passkey on your device and download a recovery kit before anything else happens. The server never holds an owner key. A session key can act for the account only inside a written policy: it may call Uniswap v3 on Base, on registry addresses, with the account as recipient and a positive minimum on every swap and mint. You can revoke it at any time, and you can leave without Mamoru: with the kit and your passkey, you revoke the sessions, close the position and withdraw, with our engine, app and indexer all off. As of 26 September 2026, the app runs a live path on Base with a hard cap of 25 USDC per account. Your passkey signs starting the allocation, transferring funds out and stopping it; a relayer sends those signed transactions. Once started, the engine enters and manages a Uniswap v3 position for you with a session key, bundling its own userOps to EntryPoint v0.7. Above the cap, and for the session-key attacks and the harvest, everything is still only demonstrated on a Base fork. The dashboard shows Base only. Every figure carries its chain id and its source, as a chip such as "Base · block N" or "MultiBaas · Base · checked at block N". It shows what the account holds, what needs your decision, what Mamoru would do next and why, and the live state of the Uniswap pool in the plan, read from Base every 2 minutes. The full cycle is verified on a Base fork pinned at block 51811000 with chain id 31337. Fork results are not capital and do not use MultiBaas. The last full run passed 45 of 45 scenarios: 24 session attacks with a leaked key, 3 walkaway scenarios, 9 engine scenarios from deposit through harvest plus the live-path receipt and bundle checks, and 9 checks of the verification plane itself. Live app: https://app.mamoru.lol. Repo: https://github.com/ottodevs/mamoru.
How it's made
Mamoru is a Bun workspaces monorepo in TypeScript, https://github.com/ottodevs/mamoru. It runs on Cloudflare Workers: the app Worker serves a Vite 8, React 19, TanStack Router and Query and Tailwind 4 single-page app plus a Hono API; a second, cron-triggered engine Worker reads Base every 2 minutes into a shared D1 database. The API serves the dashboard only from D1. Account. The account is a Safe 1.4.1 with the Safe7579 adapter, so it uses ERC-7579 modules and ERC-4337 EntryPoint v0.7. The owner is a WebAuthn passkey through Safe's WebAuthn shared signer, bound inside Safe.setup through a MultiSend 1.4.1 delegatecall, so the counterfactual address commits to the passkey. The recovery kit holds every public parameter needed to deploy that exact Safe without us. There is no account SDK: everything is encoded with viem 2.56.9 against addresses pinned in packages/registry/base.json, each checked by code hash at Base block 51811000. Sessions. packages/account/sessions/index.ts (toSmartSession, line 103) encodes a Rhinestone Smart Session. The grants in packages/policy name the allowed contracts and selectors and add argument rules through UniActionPolicy, plus time and usage limits. A session cannot install modules, change owners, approve an attacker or pay anyone but the account. Uniswap. packages/uniswap-v3/src/index.ts builds SwapRouter02.exactInputSingle (line 24) with the account as recipient and a positive amountOutMinimum, and NonfungiblePositionManager mint (line 54) with ticks on the pool spacing and positive minimums, plus decreaseLiquidity, collect and burn. quote.ts quotes through QuoterV2 at a fixed block and derives the minimum. v1 uses one pool, USDC/cbBTC 0.05% on Base. In production the engine reads its slot0, liquidity, a TWAP and one hour of Swap, Mint and Burn at the safe block. Live path. apps/mamoru-operator is a Bun service, separate from the two Cloudflare Workers, gated by MAMORU_LIVE=1 and a 25 USDC per account cap. It holds a relayer key that sends the owner's passkey-signed Safe transactions (start, transfer out, stop) and bundles the engine's own ERC-4337 userOps to EntryPoint v0.7 with the same handleOps call the fork scenarios use. MultiBaas. The engine Worker reads the pool's Swap, Mint and Burn events from a Curvegrid MultiBaas deployment on Base, with a read-only key, and checks every row against Base RPC logs before it stores it; a probe matched 10 of 10 rows with nothing missing on either side. Rows older than the index start block come from Base RPC logs and are labeled. The browser never calls MultiBaas. Verification. The fork runner in packages/scenarios starts Anvil on a Base fork pinned at block 51811000 with chain id 31337 and refuses Base chain ids, unpinned blocks and keys in argv. Session attacks send real batches to EntryPoint.handleOps and must be rejected by the chain. The engine scenarios run the same decision code against the fork through a loopback bundler that speaks the standard ERC-4337 methods (Alto is pinned but not installed): a deposit is entered with a swap and a mint, the swap minimum recomputed from QuoterV2 equals the signed one, the bundler receipt matches the UserOperationEvent, and a harvest converts fees to USDC and credits the Savings Log. How we worked. We wrote the spec first with GitHub Spec Kit: constitution, spec, plan, dashboard spec, scenarios, threats and tasks are in inputs/ and specs/001-mamoru-v1. Brais and Otto decided the product, the design and every merge. AI agents implemented in parallel lanes on separate branches, and a second model reviewed each lane (docs/process/ai-attribution.md).