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:

  1. Deposit tokens into a vault you own
  2. Set rules — daily limits, per-transaction caps, per token
  3. Agent operates within limits — the contract enforces them, not the agent
  4. 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

  1. Create a vault on the SpendControl dashboard. Connect your wallet and click "Create Vault." Enter your agent's address.
  2. Deposit tokens — ETH, USDC, DAI, or any ERC20. Use the dashboard or call the contract directly.
  3. Set per-token spending limits — configure a daily budget and a per-transaction cap for each token your agent will use.
  4. 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.
  5. 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) → uint256

Current vault balance of a token.

remainingDailyBudget(address token) → uint256

How much the agent can still spend today for a given token.

spendableYield() → uint256

Harvested yield available for the agent to spend.

getExpense(uint256 index) → Expense

Get 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 initialize and spend
  • 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) → address

Deploy 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 index
  • getRecentExpenses(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 checksinitialize() and spend() 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