How it works
The vault has two roles, and they use different keys.
Every
pay call must pass four checks in the contract:
- The vault isn’t paused.
- The recipient is on the allowlist.
- If an identity contract is set, it says the recipient is trusted. See Identity hooks.
- The amount fits in what’s left of today’s limit. The day resets at 00:00 UTC. See Spending limits.
What you’ll build
AgentVault, a contract built on OpenZeppelin’sOwnable,Pausable,ReentrancyGuardandSafeERC20.- A Foundry test suite for its rules.
vault-pay.ts, a viem script that pays from the vault with the agent’s key and prints the vault’s reason when it refuses.
Prerequisites
- A Foundry project from Set up Foundry, with the
uzo-devkeystore and some tBOT. - Some testnet USDT in that wallet. Claim Test USDT from the BOT Chain faucet, or swap tBOT for it on BDEX V2. Testnet USDT is
0x75edC9335175Fc0552D51D48439F229c10420fe3and has 6 decimals. - The
bot-defiproject and.envfile from Wrap BOT, for the script.
Steps
1
Install OpenZeppelin
In your Foundry project:Add
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ to remappings in foundry.toml if it isn’t there.2
Add the identity interface
The vault can ask another contract whether a recipient is trusted. This interface is all it needs to know about that contract.
src/IAgentIdentity.sol
3
Write the vault
src/AgentVault.sol
payis the only function the operator can call. It runs every check before it moves any tokens.- Errors carry data.
DailyLimitExceeded(amount, remaining)tells the agent exactly how much it can still send, so your tool can pass that back to the model. withdrawhas nowhenNotPaused, so the owner can always get funds out.usdtisimmutable. To hold a different token, deploy another vault.
4
Test the rules
Save this test file, then run it.
All seven tests should pass:
test/AgentVault.t.sol
test/AgentVault.t.sol
test/AgentVault.t.sol
Output
5
Create the agent's key
The agent needs its own key, separate from yours. Create one and import it into a keystore:Paste the new private key when asked. Note the agent’s address. Never reuse your owner key as the operator.
6
Deploy the vault
The constructor takes the owner, the operator, the token and the daily limit in token units. Save the
1000000 is 1 USDT.Deployed to address. To verify the contract, see Verify with Foundry.7
Fund the vault and allow a recipient
Run these from the owner wallet. The first sends 0.02 USDT to the vault. The second lets the agent pay one address. The third gives the agent a little tBOT for gas.Check what’s left to spend today:
8
Pay from the agent
In your The script reads the vault’s state first, then simulates
bot-defi project, add these to .env:.env
vault-pay.ts
pay. If the vault would refuse, the simulation fails, nothing is sent, and you see the contract’s error name and arguments.Verify it worked
This run used a vault on testnet with a 0.01 USDT daily limit, on 2026-10-02. It paid 0.006 USDT, tried the same payment again, tried an address that wasn’t allowed, then tried again after the owner paused the vault:Output
Paid event.
Manage the vault as the owner
Troubleshooting
Vault refused: NotOperator
Vault refused: NotOperator
The key in
AGENT_PRIVATE_KEY isn’t the vault’s operator. Check with cast call YOUR_VAULT_ADDRESS "operator()(address)" --rpc-url bot_testnet. If you deployed with the owner as operator by mistake, call setOperator from the owner.Vault refused: RecipientNotAllowed
Vault refused: RecipientNotAllowed
The owner hasn’t allowed that address. Run
setRecipientAllowed with true from the owner wallet. Addresses are compared exactly, so make sure it’s the one you meant.Vault refused: DailyLimitExceeded
Vault refused: DailyLimitExceeded
The second argument is what’s left today, in token units. Wait until 00:00 UTC, send less, or raise the limit with
setDailyLimit.Vault refused: EnforcedPause
Vault refused: EnforcedPause
The owner paused the vault. Call
unpause from the owner wallet when you’re ready.The simulation fails with a token error
The simulation fails with a token error
The vault doesn’t hold enough USDT, so the token’s
transfer reverts. Check with cast call 0x75edC9335175Fc0552D51D48439F229c10420fe3 "balanceOf(address)(uint256)" YOUR_VAULT_ADDRESS --rpc-url bot_testnet and send more.insufficient funds for gas
insufficient funds for gas
The agent’s address pays gas for
pay. Send it a little tBOT from the owner wallet.Next steps
Spending limits
How the daily cap and allowlist work.
Tool calling
Let a model call tools that use this vault.