Agent wallets with limits that hold
Fund one account and give each AI agent its own key and policy. The contract refuses any payment outside it, and says why.
Live on Base Sepolia. ETH and any ERC-20. The contracts are testnet code and have not been audited.
research-agent
agent 0x7a3e…a9e1
- USDC
- 5 per payment
- 50 per day
- Pays
- Inference API, Weather data
- Asks owner
- at 4 USDC and above
- Valid until
- 31 Dec 2026
15 October 2026
OpenAI disclosed that one of its research models, out of test ETH mid-task, derived keys from publicly known seed phrases and paid for a Sepolia deployment from a wallet it did not own. Agents need money of their own, with limits that hold when the agent goes off script. Sources: OpenAI's report, CoinDesk, Gizmodo.
Set a policy, then try to break it
Every stamp here comes from the same rules the contract runs, in the same order. Change a cap and watch the stamps on the payments update. Nothing here touches a chain.
Policy for research-agent
Payments to try
The stamp shows what the contract would decide right now.
- 3 USDC to Inference APImemo inv-1042
- 4.50 USDC to Weather datamemo q4-history
- 12 USDC to GPU rental
TARGET_NOT_ALLOWED - 2 USDC to 0x9f3a…c41d
TARGET_NOT_ALLOWED - 0.002 ETH to Inference API
TOKEN_NOT_ALLOWED - Call getAmountsOut on Swap routerread a price, moves nothing
- Call swapExactTokensForTokens on Swap router
SELECTOR_NOT_ALLOWED
Owner and clock
What the contract checked
Send a payment to see each gate it passes.
- Caller is a registered agent
NOT_AGENT - Account is not paused
PAUSED - Policy has started
NOT_YET_VALID - Policy has not expired
EXPIRED - Recipient is on the allowlist
TARGET_NOT_ALLOWED - Function is allowed on that contract
SELECTOR_NOT_ALLOWED - Token has a limit
TOKEN_NOT_ALLOWED - Amount is within the per-payment cap
OVER_PER_TX - Window total stays within the period cap
OVER_PERIOD - Amount is below the approval threshold
NeedsApproval - Account holds enough
INSUFFICIENT_BALANCE
Budget left in this window
- USDC50 of 50 left
Window resets in 15 h 0 min
Ledger
Nothing yet. Every attempt lands here, refused ones included.
Two keys, one place where the rules live
The agent signs its own transactions. The account holds the money and checks each one against the policy you set. A server is never in the path, so there is nothing to bypass.
Owner
Your browser wallet
setAgent · approve · setPausedAgent
Its own key, nothing else
pay() or call() from a hot key in your agent runtime, through the SDK or the MCP server.pay(USDC, api, 3)Warden account
Holds the funds and every policy
On every payment it asks, in this order:
- Is the caller an agent, and is the account live?
- Is the policy inside its valid dates?
- Are the recipient and the function allowlisted?
- Does the token have a limit, and is the amount inside it?
- Does the window still have room?
- Is the amount below the approval threshold?
- Does the account hold enough?
- Allowed
Executes in the same transaction. The account emits Spent with the memo and the policy hash.
- Needs approval
Parked as a request. Nothing moves until the owner approves, within 24 hours by default.
- Denied
Reverts with Denied(reason). The agent can read the same reason from check() before it pays gas.
Agent
Its own key, nothing else
pay() or call() from a hot key in your agent runtime, through the SDK or the MCP server.pay(USDC, api, 3)Warden account
Holds the funds and every policy
On every payment it asks, in this order:
- Is the caller an agent, and is the account live?
- Is the policy inside its valid dates?
- Are the recipient and the function allowlisted?
- Does the token have a limit, and is the amount inside it?
- Does the window still have room?
- Is the amount below the approval threshold?
- Does the account hold enough?
- Allowed
Executes in the same transaction. The account emits Spent with the memo and the policy hash.
- Needs approval
Parked as a request. Nothing moves until the owner approves, within 24 hours by default.
- Denied
Reverts with Denied(reason). The agent can read the same reason from check() before it pays gas.
Owner
Your browser wallet
setAgent, approve, setPaused.A dozen lines from agent to payment
The SDK asks the contract first and throws the exact reason before your agent spends gas. Models get the same calls as MCP tools.
check- Free view. Allowed, needs approval, or denied with a reason.
pay- Sends ETH or an ERC-20. Returns a receipt or a request id.
call- Calls an allowlisted contract, value and token calldata counted.
waitForApproval- Resolves when the owner approves or rejects.
Preview: @warden/sdk and @warden/mcp are not published yet. These samples follow the v0.1 interface.
import { createWardenAgent } from "@warden/sdk";
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
const agentWallet = createWalletClient({
account: privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`),
chain: baseSepolia,
transport: http(),
});
const agent = createWardenAgent({ publicClient, walletClient: agentWallet, account: WARDEN_ACCOUNT });
// Free: asks the contract what it would decide, moves nothing.
const decision = await agent.check({ token: "USDC", to: apiProvider, amount: "3" });
// Checks first and throws WardenDenied with the reason before any gas is spent.
const result = await agent.pay({ token: "USDC", to: apiProvider, amount: "3", memo: "inv-1042" });
// result.status: "executed" (with receipts) or "pending_approval" (with a requestId)What a policy stops, and what it cannot
Warden limits the damage a misbehaving or stolen agent key can do. It does not make a bad allowlist safe, and it cannot protect an owner key you lose.
The contract enforces
- An agent key moves only what its policy names: listed tokens, under the per-payment and per-window caps, to allowlisted addresses, between the valid-from and valid-until times.
- Payments at or above the approval threshold wait for your signature. You see the amount, recipient and memo before anything moves.
- ERC-20 transfer and approve calls count against the cap at their full amount, so an agent cannot approve its way around a limit.
- Agents can never call the account itself or the factory.
- One switch pauses every agent. Your withdrawals keep working while paused.
- Every spend emits a receipt with the hash of the exact policy it was spent under.
Still on you
- A leaked agent key can still spend up to its caps, on allowlisted recipients, until you pause or revoke it.
- Whoever holds the owner key controls the account. Keep it in a hardware wallet.
- An allowlisted contract that misbehaves can lose what the agent sends it.
- Approval fatigue: if you approve every request without reading it, the threshold protects nothing.
- Caps are in token units with no price oracle, so an ETH cap is worth a different dollar amount next week.
- Version 0.1 is unaudited testnet code.
Contracts
One factory, one account per owner. Read them on Basescan, or download the ABIs and call them from any client.
| Contract | Address on Base Sepolia (84532) |
|---|---|
| WardenFactoryCreates accounts at predictable addresses | 0x1a4aA3C45E9fdF8845dBdfcAb6428f5f7D0272F6 |
| WardenAccountImplementation every account clones | 0x2d8eFc03262f4cc905d344569a3f541B0233CA92 |
| USDCCircle test token used in the demo | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |