Skip to main content
In this guide you deploy a vault that holds USDT for an AI agent, give the agent a key that can only pay inside the vault’s rules, and send a payment from it.
Everything on this page uses testnet (chain 968). Get free test tokens from the faucet.

How it works

The vault has two roles, and they use different keys. Every pay call must pass four checks in the contract:
  1. The vault isn’t paused.
  2. The recipient is on the allowlist.
  3. If an identity contract is set, it says the recipient is trusted. See Identity hooks.
  4. The amount fits in what’s left of today’s limit. The day resets at 00:00 UTC. See Spending limits.
If the agent’s key leaks, the attacker gets the same limits the agent had. You pause the vault, withdraw the funds and set a new operator.

What you’ll build

  • AgentVault, a contract built on OpenZeppelin’s Ownable, Pausable, ReentrancyGuard and SafeERC20.
  • 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.
This contract isn’t audited. Use it to learn the pattern, and get it reviewed before it holds meaningful funds.

Prerequisites

  • A Foundry project from Set up Foundry, with the uzo-dev keystore 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 0x75edC9335175Fc0552D51D48439F229c10420fe3 and has 6 decimals.
  • The bot-defi project and .env file 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
A few choices worth knowing:
  • pay is 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.
  • withdraw has no whenNotPaused, so the owner can always get funds out.
  • usdt is immutable. To hold a different token, deploy another vault.
4

Test the rules

Save this test file, then run it.
test/AgentVault.t.sol
All seven tests should pass:
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. 1000000 is 1 USDT.
Save the 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 bot-defi project, add these to .env:
.env
vault-pay.ts
The script reads the vault’s state first, then simulates 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
The three refusals cost nothing: each failed in simulation, so no transaction was sent. Open your payment link on BOTScan, and the vault’s Logs tab shows a Paid event.
To try the vault without spending tBOT, run it against a local fork with anvil --fork-url https://rpc.bohr.life. Point --rpc-url and the viem transport at http://127.0.0.1:8545, and use cast rpc anvil_impersonateAccount to move USDT from a funded testnet address.

Manage the vault as the owner

Troubleshooting

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.
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.
The second argument is what’s left today, in token units. Wait until 00:00 UTC, send less, or raise the limit with setDailyLimit.
The owner paused the vault. Call unpause from the owner wallet when you’re ready.
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.
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.
Last modified on October 3, 2026