Overview
Tillwarden gives an AI agent its own spending account on Robinhood Chain. The money stays in a Safe that you own. A Safe module holds the rules. The agent has its own small key and can only ask for payments inside those rules. Before anything is signed, the firewall simulates the payment and checks it. Every payment in or out leaves a signed receipt.
Three parts work together:
- Contracts on chain: the module (rules and execution), the factory (opens accounts) and the TillJar (fees).
- The engine: firewall, relayer, indexer, receipts and watchdog, behind
/api. - Tools: this site and its app, plus the SDK, CLI and MCP server.
Accounts
An account is a Safe 1.4.1 (the L2 variant) with the Tillwarden module enabled. You can open a new one in one transaction through the factory, or attach the module to a Safe you already have with one Safe transaction.
- Owners and threshold are normal Safe settings. Tillwarden does not change them.
- The factory and module deployers get no rights over any account.
- To leave, disable the module in your Safe. The money never left it.
Rules
Rules are set per token (ETH, USDG or any ERC-20 you add). Each token has:
- Enabled: switched off means the agent cannot spend it.
- Max per payment: a cap on one payment.
- Daily and weekly budget: counted with the fee included. Days are UTC days; weeks start Monday 00:00 UTC.
- Ask above: payments above this amount wait for an owner (see approvals).
Account-wide settings: a payee list with an on/off switch, a separate contract-call list of (contract, function) pairs, allowed hours (UTC hours, empty means any time) and a per-minute limit (parked payments count too). Empty caps mean no cap.
Agent keys
An agent key is a plain address. It signs payment intents (EIP-712, domain Tillwarden, version 1). It cannot change rules, add owners or move money outside the rules.
- Make one in the app (shown once, with a download) or with
npx tillwarden keygen. Or paste an address you already have. - The owner then issues an agent API key by signing one message. The engine stores only a hash of it.
- The SDK signs locally. The private key is never sent to the engine.
Firewall
Every agent payment goes through eight checks before the firewall key co-signs it. If the owner set a firewall key, the module refuses any intent without that co-signature.
- Hidden instructions in what the agent read and in the memo.
- Account rules read live from the module.
- Allowed hours and the per-minute limit.
- Allowlist for payees (when on) and for contract calls (always).
- Balance and drain: more than half of a balance in one payment fails.
- Approvals in calldata: approvals, permits and hidden transfers fail.
- Contract check: code present, source verified.
- Simulation of the exact transaction on current chain state.
Verdicts: allow, needs approval (a human should look) or block. Try it on the firewall page; the hidden-instruction rules are listed there, read live from the engine.
Approvals
A payment or contract call above the "ask above" amount is parked on chain (at most 32 open per account): nothing moves, an ApprovalRequested event is emitted, and it shows in the app under pending. An owner approves or rejects it. After approval, the same signed intent is submitted again and executes, still checked against every other rule. Reject burns its nonce so it can never run.
The engine can also hold a payment the firewall marked "needs approval" before it reaches the chain. Those show in the same list and are released or rejected by an owner signature.
Watchdog and freeze
The watchdog learns what normal looks like for each account from its own receipts. It freezes the account when:
- one payment is more than 5 times the usual (median) payment in that token, once there are at least 5 earlier payments;
- payments in the last hour are more than 3 times the usual hourly rate (and at least 6);
- 3 or more payments go to never-paid addresses within 10 minutes.
It can only freeze if the owner set the engine's watchdog key on the account. It can never unfreeze. Owners can freeze at any time and unfreeze when they are ready. Each freeze leaves an alert with a plain reason.
Receipts
Every payment out and every payment in gets a receipt: direction, token, amount, fee, counterparty, memo, transaction, time (UTC), the hash of the previous receipt, its own hash, and a signature from the engine's receipt key. Receipts form a chain per account, so a missing or edited one shows. Verify one with GET /api/receipts/:id/verify or npx tillwarden verify <id>. Export as CSV from the app.
Money in
The pay button is one script tag. The buyer's wallet sends the payment straight to your Safe; nothing sits in between. Plain ETH or USDG transfers to the Safe are picked up too. Incoming payments pay no fee.
Contract calls
Agents can also call contracts, for example a swap router. Each allowed call is one contract plus one function (a 4-byte selector; 0x00000000 means a plain ETH send). Calls only go to allowed pairs, never to the Safe, the module, USDG, the jar or a token configured for the account. Approve, permit and transfer-style functions are refused. When a call needs to spend a token, the module grants exactly the declared amount, runs the call, and resets the allowance to zero in the same transaction. It measures the Safe's balances before and after and counts every drop against the budgets.
Fees and the TillJar
Each ETH or USDG payment by an agent pays a 0.1% fee on top of the amount, in the same token, to the TillJar contract. The payee always gets the full amount. Other tokens and incoming payments pay no fee.
Burning takes two calls, both open to anyone. poke() records a reference price; between 1 and 30 minutes later buyAndBurn() runs, only if the price has not moved too far from that reference. At most once every 30 minutes, with fixed caps per call. It swaps USDG to ETH on the USDG/WETH pool, buys $TILL where Pons lists it (the bonding curve, or the pool after graduation), and sends it all to the dead address. There is no owner withdraw. See $TILL.
HTTP API
Base https://tillwarden.fun/api. JSON in and out. Amounts are decimal strings in token units ("2.5"). Errors look like { "error": "code", "message": "…" }.
/api/healthChain id, latest block, whether contracts are deployed, indexer state./api/configToken, ticker, launch text, fee, contract addresses, engine public keys, known tokens./api/firewall/checkBody { to, amount, token?, memo?, context?, data?, safe? }. Returns verdict, checks, simulation, summary. Rate limited per IP./api/firewall/examplesThe five playground scenarios, the demo account and the hidden-instruction rules./api/accounts/:safeOwners, agents, rules and spending per token, balances, schedule, keys, frozen./api/accounts/:safe/receipts?limit&cursor. Also /receipts.csv./api/accounts/:safe/pendingPayments waiting for an owner, on chain and held by the engine./api/accounts/:safe/statsIn, out and fees per day. ?days=30./api/accounts/:safe/alertsWatchdog alerts with reasons./api/agent/payBearer agent API key. Builds the intent and runs the firewall; returns typed data to sign./api/agent/submitBearer key. { intentId, agentSig }. Co-signs, submits, returns paid, pending_approval or blocked./api/agent/meBearer key. Account, limits left, balances./api/agent/receiptsBearer key. The agent's account receipts./api/receipts/:id/verifyRecomputes the hash, checks the chain link and the signature./api/pay/:safe/intent?amount&token. The exact transfer for a pay button./api/networkAccounts opened, paid by agents, received, blocked, fees to the jar, $TILL burned.Contracts
Solidity 0.8.28. No proxies, no upgrade path, no admin that can move user funds.
- Module
- deploying
- Factory
- deploying
- TillJar
- deploying
- Safe singleton
- –
Limits and caveats
- Not audited. Use small amounts. Read own risk.
- Allowlisted contracts are trusted. A bad contract on your allowlist can do what a call with the declared allowance allows, within the budgets.
- Only ETH and tokens configured on the account are measured during a call. Do not leave owner approvals to allowlisted contracts on other tokens.
- The per-minute limit uses fixed UTC minutes, so up to twice the limit can land around a minute boundary.
- The hidden-instruction check is pattern based. It catches common attacks, not every possible one. The on-chain rules are the hard limit.
- The jar's $TILL buy uses a spot price floor with a slippage cap; each call is capped and runs at most every 30 minutes.
- The engine is a single service. If it is down, agents cannot pay through it; the rules and your Safe are unaffected.