SpendControl Documentation
SpendControl is a treasury protocol for AI agents. It lets you fund an agent with real tokens, enforce per-transaction and daily spending limits on-chain, and get a full audit trail of every spend with a mandatory reason.
The Problem
AI agents increasingly need to spend money — paying for APIs, buying compute, tipping services. But giving an agent full wallet access is dangerous. One hallucination, one prompt injection, and your funds are gone.
The Solution
SpendControl puts a smart contract between your money and your agent:
- Deposit tokens into a vault you own
- Set rules — daily limits, per-transaction caps, per token
- Agent operates within limits — the contract enforces them, not the agent
- Every transaction is recorded with a mandatory reason, on-chain
The agent can never exceed its budget. You can pause the vault instantly. Every spend is auditable.
Quick Start
- Create a vault on the SpendControl dashboard. Connect your wallet and click "Create Vault." Enter your agent's address.
- Deposit tokens — ETH, USDC, DAI, or any ERC20. Use the dashboard or call the contract directly.
- Set per-token spending limits — configure a daily budget and a per-transaction cap for each token your agent will use.
-
Give your agent the skill file or MCP config — paste one line for the skill file, or add the MCP server to your agent's
settings.json. - Agent starts spending within rules — every transaction is on-chain with a reason. Monitor activity in the dashboard.
Smart Contracts
SpendControl consists of two contracts: AgentVault (the vault itself) and AgentVaultFactory (deploys vaults cheaply).
AgentVault
The main contract. Each vault is owned by one wallet and operated by one agent address.
Deposits
deposit(address token, uint256 amount)Deposit any ERC20 token into the vault. Requires prior approval.
depositETH()Deposit ETH. Automatically wraps to WETH inside the vault.
Withdrawals
withdraw(address token, uint256 amount)Owner withdraws tokens from the vault. Only the owner can call this.
Spending
spend(address token, address to, uint256 amount, string reason)Agent spends tokens. The reason parameter is mandatory — the transaction reverts if it is empty. Enforces daily and per-transaction limits.
Staking
stakeETH(bool yieldOnly)Stake ETH via Lido. If yieldOnly is true, principal is locked and the agent can only spend yield.
harvestYield()Crystallize staking yield. Can be called once per day.
Limits
setTokenLimits(address token, uint256 daily, uint256 perTx)Set per-token daily budget and per-transaction maximum. Owner only.
View Functions
balanceOf(address token) → uint256Current vault balance of a token.
remainingDailyBudget(address token) → uint256How much the agent can still spend today for a given token.
spendableYield() → uint256Harvested yield available for the agent to spend.
getExpense(uint256 index) → ExpenseGet a single expense record by index.
getRecentExpenses(uint256 count) → Expense[]Get the N most recent expense records.
Security Features
- SafeERC20 for all token transfers (handles non-standard tokens like USDT)
- ReentrancyGuard on all state-changing functions
- Access control — owner and agent roles, strictly separated
- Zero-address checks in
initializeandspend - Approval hygiene — resets allowance to 0 before re-approving
AgentVaultFactory
Deploys new vault instances using EIP-1167 minimal proxies. Each vault is a lightweight clone of the implementation contract, making deployment cost roughly ~$1 instead of ~$20.
createVault(address agent) → addressDeploy a new vault. The caller becomes the owner, agent gets spending rights.
getVaultsByOwner(address owner) → address[]List all vaults owned by an address.
getVaultsByAgent(address agent) → address[]List all vaults where an address is the authorized agent.
Why EIP-1167? Minimal proxy clones delegate all calls to a shared implementation contract. The clone itself is only 45 bytes of bytecode, so deployment is extremely cheap. The implementation is immutable after deploy — nobody can change the logic.
Lido Staking
Vaults can stake deposited ETH via Lido to earn staking yield. The yield can fund your agent's operations without touching your principal.
How stETH Rebasing Works
When you stake ETH through Lido, you receive stETH. Unlike most tokens, stETH balances grow automatically via a daily rebase. If you hold 10 stETH today, tomorrow you'll hold slightly more — no claiming required. The contract tracks the difference between your original deposit and the current balance to calculate yield.
Yield-Only Mode
Call stakeETH(true) to enable yield-only mode. In this mode:
- Your principal is locked — the agent cannot touch it
- Only the yield that grows on top is available for spending
harvestYield()crystallizes the accumulated yield (callable once per day)spendableYield()shows what the agent can currently spend- The agent can never access the principal, only harvested yield
Economics
| Staked ETH | APY (~3.5%) | Monthly Yield | Daily Budget |
|---|---|---|---|
| 1 ETH | 0.035 ETH/yr | ~$3/mo | ~$0.10/day |
| 10 ETH | 0.35 ETH/yr | ~$30/mo | ~$1/day |
| 100 ETH | 3.5 ETH/yr | ~$300/mo | ~$10/day |
Self-funding agents. Stake 10 ETH and your agent gets approximately $30/month to operate with — forever — without you ever topping up the vault.
Agent Integration
There are four ways to connect an agent to its vault: a skill file (simplest), an MCP server (native Claude Code tools), a Python SDK, or direct CLI commands.
Skill File
The fastest way to give any LLM agent access to its vault. Paste one line into your agent's prompt or system message:
Read https://spendcontrol.xyz/skill.md — my vault is 0xYourVaultAddress
The skill file contains all the instructions and ABI the agent needs to check its budget, spend tokens, and report expenses. No SDK required.
MCP Server
For Claude Code and other MCP-compatible agents, the MCP server exposes native tools.
Installation
git clone <repo-url>
cd mcp-server
npm install
Claude Code settings.json
{
"mcpServers": {
"spendcontrol": {
"command": "node",
"args": ["path/to/mcp-server/index.js"],
"env": {
"VAULT_ADDRESS": "0xYourVaultAddress",
"AGENT_PRIVATE_KEY": "0xYourAgentKey",
"RPC_URL": "https://your-rpc-url"
}
}
}
}
Available tools
| Tool | Description |
|---|---|
check_budget |
Check remaining daily budget for a token |
spend |
Spend tokens from the vault with a reason |
get_history |
Retrieve recent expense records |
get_vault_info |
Get vault balances, limits, and configuration |
Python SDK
Installation
pip install -e sdk/python
Usage
from yieldvault import VaultClient
client = VaultClient(
rpc_url="https://your-rpc-url",
vault_address="0xYourVaultAddress",
agent_private_key="0xYourAgentKey"
)
# Check how much the agent can spend today
budget = client.check_budget("0xUSDC_ADDRESS")
print(f"Remaining daily budget: {budget}")
# Spend tokens
tx = client.spend(
token="0xUSDC_ADDRESS",
to="0xRecipient",
amount=5_000000, # 5 USDC (6 decimals)
reason="Paid for API credits"
)
# List supported tokens
tokens = client.get_tokens()
# Get expense history
history = client.get_history(count=10)
Methods
| Method | Description |
|---|---|
check_budget(token) |
Remaining daily budget for a token |
spend(token, to, amount, reason) |
Spend from vault with mandatory reason |
get_tokens() |
List all tokens in the vault |
get_history(count) |
Recent expense records |
CLI (cast)
You can interact with the contracts directly using Foundry's cast. Useful for scripting and debugging.
Check vault balance
cast call $VAULT "balanceOf(address)(uint256)" $TOKEN_ADDRESS
Check remaining daily budget
cast call $VAULT "remainingDailyBudget(address)(uint256)" $TOKEN_ADDRESS
Spend tokens (as agent)
cast send $VAULT "spend(address,address,uint256,string)" \
$TOKEN $RECIPIENT $AMOUNT "Reason for spending" \
--private-key $AGENT_KEY
Deposit ERC20 (as owner)
# Approve first
cast send $TOKEN "approve(address,uint256)" $VAULT $AMOUNT \
--private-key $OWNER_KEY
# Then deposit
cast send $VAULT "deposit(address,uint256)" $TOKEN $AMOUNT \
--private-key $OWNER_KEY
Deposit ETH (as owner)
cast send $VAULT "depositETH()" --value 1ether \
--private-key $OWNER_KEY
Set spending limits (as owner)
cast send $VAULT "setTokenLimits(address,uint256,uint256)" \
$TOKEN $DAILY_LIMIT $PER_TX_LIMIT \
--private-key $OWNER_KEY
Stake ETH with yield-only mode
cast send $VAULT "stakeETH(bool)" true --private-key $OWNER_KEY
Harvest yield
cast send $VAULT "harvestYield()" --private-key $OWNER_KEY
Get recent expenses
cast call $VAULT "getRecentExpenses(uint256)" 5
Expense Reports
Every spend is recorded on-chain with a mandatory reason. The agent literally cannot spend without explaining why — this is enforced at the contract level, not by the frontend.
On-Chain Event
Every successful spend emits an AgentSpent event:
event AgentSpent(
address indexed agent,
address indexed token,
address indexed to,
uint256 amount,
string reason
);
Expense Storage
All expenses are also stored in the contract's expenses[] array, queryable at any time:
getExpense(index)— retrieve a single expense by indexgetRecentExpenses(count)— get the N most recent expenses
The dashboard reads these records and displays them in an activity log with token, amount, recipient, reason, and timestamp.
Full auditability. Because reasons are stored on-chain, anyone can verify what an agent spent money on — even without the dashboard. It is a permanent, tamper-proof receipt.
Deployed Contracts
Factory Addresses
| Network | Factory Address | Explorer |
|---|---|---|
| Ethereum Mainnet | 0x93e3F6F081F0f5bef1EF9CD42D7924E258e8073B |
etherscan.io |
| Base Sepolia | 0xF6CFA83764D0B1E0417a74FfB8d915985DFd3642 |
sepolia.basescan.org |
Token Addresses
Ethereum Mainnet
| Token | Address |
|---|---|
| WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
| USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| DAI | 0x6B175474E89094C44Da98b954EedeAC495271d0F |
| stETH | 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 |
Base Sepolia (Testnet)
| Token | Address |
|---|---|
| WETH | 0x4200000000000000000000000000000000000006 |
| USDC | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| DAI | 0x7683022d84F726a96c4A6611cD31DBf5409c0Ac9 |
Security
SpendControl is designed with defense-in-depth. Multiple independent safeguards ensure that even if one layer fails, funds remain protected.
- SafeERC20 — all token transfers use OpenZeppelin's SafeERC20 library, which handles non-standard tokens like USDT that don't return a boolean on transfer.
- ReentrancyGuard — every state-changing function is protected against reentrancy attacks.
- Zero-address checks —
initialize()andspend()reject the zero address to prevent accidental burns. - Approval hygiene — before setting a new token approval, the contract resets the existing allowance to 0. This prevents the well-known approval race condition.
- EIP-1167 minimal proxy — the implementation contract is immutable after deployment. Nobody can change the vault logic.
- Owner can pause instantly — if an agent is compromised, the owner can pause the vault with a single transaction, freezing all spending immediately.
- Per-token limits enforced by contract — spending limits are checked on-chain. The frontend, agent, and SDK cannot bypass them.
- Reason is mandatory — the
spend()function reverts if the reason string is empty. Agents cannot spend silently. - Audited against EthSkills security checklist — the contracts have been reviewed against the standard smart contract security checklist.
Key principle: The contract enforces all rules. Even a malicious or hallucinating agent cannot exceed its budget, skip the reason field, or access locked principal. Security does not depend on the agent behaving correctly.
FAQ
How much does it cost to create a vault?
Approximately $1. The factory uses EIP-1167 minimal proxies, which deploy a tiny 45-byte clone instead of the full contract bytecode. This is roughly 20x cheaper than a standard deployment.
Can the agent steal my principal?
No. In yield-only mode (stakeETH(true)), the principal is locked at the contract level. The agent can only spend harvested yield. Even if the agent is compromised, your principal is safe.
What if the agent goes rogue?
Pause the vault instantly. The owner can call the pause function with a single transaction. This freezes all spending immediately. Even without pausing, the agent is bounded by its daily and per-transaction limits.
Can I use any token?
Yes. Any ERC20 token works. The vault uses SafeERC20 to handle non-standard tokens like USDT. You set limits per token, so you have granular control.
How does Lido yield work?
When you stake ETH through Lido, you receive stETH. The stETH balance rebases daily — it grows automatically as staking rewards accrue. The vault tracks the difference between your original deposit and the growing balance to calculate yield. Call harvestYield() once per day to crystallize the yield for spending.
Is the code open source?
Yes. All contracts are verified on their respective block explorers. The source code, tests, and deployment scripts are publicly available.
SpendControl — Treasury Protocol for AI Agents