Concepts
Warden is one smart contract account per owner. It holds the funds. Each agent gets its own key and a policy stored in the account, and the account checks every payment against that policy before it moves anything.
The owner#
The owner is a single address, usually a browser or hardware wallet. Only the owner can:
- register an agent or replace its policy (
setAgent), and revoke it (revokeAgent); - approve or reject held payments (
approve,reject); - pause and unpause every agent at once (
setPaused); - withdraw any token (
withdraw), including while paused; - change how long held payments stay approvable (
setRequestTtl); - hand the account to another owner, in two steps:
transferOwnership(next), thenacceptOwnership()from the new address.
Anyone can deposit. The owner key never goes near the agent.
Agents#
An agent is any address registered with a policy. The agent sends transactions from its own key and pays its own gas; the account checks the policy and executes. An agent can do two things:
pay(token, to, amount, memo)sends ETH (token = address(0)) or an ERC-20.call(target, value, data, memo)calls a contract. Nativevaluecounts against the ETH limit, and ERC-20 calldata such astransferorapprovecounts against that token's limit.
It can also ask, for free, what would happen: check(agent, token, to, amount, target, data) returns Allowed, NeedsApproval, or Denied with a reason, and never reverts.
The policy#
A policy is what setAgent stores for one agent:
- Limits, one per token: the most per payment (
perTx), the most per window (perPeriod), and the window length (period). A token with no limit cannot be spent at all. - Allowlist: the addresses the agent may pay or call, unless
anyRecipientis on. For a contract you can also name the functions it may call. - Approval thresholds, per token: a payment at or above the threshold waits for the owner.
- Validity:
validAfterandvalidUntilin unix seconds.
Replacing a policy replaces all of it at once. The policy reference covers every field.
Windows#
Spending caps use fixed windows, not rolling ones. The window that contains a moment now starts at floor(now / period) * period. With a one-day period, windows run from 00:00 to 24:00 UTC. When a new window starts, the spent total is zero again.
Two consequences worth knowing:
- An agent with a 50 USDC daily cap can spend 50 at 23:59 UTC and 50 more at 00:00. If that matters, use a shorter period with a smaller cap, or a lower per-payment cap.
- Editing a policy never refills the window. Spending recorded in the current window stays counted under the new caps, and it survives a revoke and re-register too.
Approvals#
When a payment passes every cap but is at or above the agent's threshold for that token, the account does not execute it. It stores a request and emits ApprovalRequested with the amount, recipient, memo and expiry. Nothing moves.
The owner then approves or rejects. approve checks the policy again, without the threshold, because a lot can change between the request and the decision: the policy may be tighter, the window fuller, the account paused or short of funds. If the check fails, the approval reverts with the reason and the request stays pending until it expires. Requests expire after requestTtl, 24 hours by default; an expired request cannot be approved.
Payments over a cap are denied outright, never parked, because approving them would fail anyway.
Receipts and the policy hash#
Every movement of value emits an event, and the events are the audit log:
| Event | When |
|---|---|
Spent | Value left the account under an agent's policy. call emits one per amount it counted. |
Called | An agent's contract call ran. |
ApprovalRequested | A payment is waiting for the owner. |
Approved, Rejected | The owner decided. Rejected carries the reason. |
AgentSet, AgentRevoked | A policy was written or removed. |
Paused, Deposited, Withdrawn | Account-level changes. |
Spent and Called carry policyHash: keccak256(abi.encode(policy, targets, selectors, thresholds)) as passed to setAgent. A receipt therefore proves which exact policy the money was spent under. getAgent returns the stored configuration, so anyone can hash it again and compare. The dashboard does this on every agent page.
Pause#
setPaused(true) freezes every agent: check returns PAUSED, and so does approve. Withdrawals keep working, so the owner can move funds out while things are frozen.
What runs where#
Nothing runs on a Warden server. The dashboard reads the chain and asks your wallet to sign. The SDK and the MCP server run next to your agent and talk to the chain directly. If this website disappeared, the accounts and their policies would keep working.