AI agent with a spending limit for a treasury
This setup is used by DAOs or other organizations that want to utilize AI agents to manage their funds.
This setup uses Safe's allowance module (opens in a new tab). After activating it for a Safe Smart Account, you can set an allowance per token for a spender (the delegator). It can be a one-time allowance, or an allowance that resets after a certain time interval (for example, 100 USDC every day).
You can find an easy to run example for the allowance module in our example repository (opens in a new tab).
You can setup an allowance (spending limit) on a Safe Smart Account with the Safe Wallet (opens in a new tab) interface following this guide (opens in a new tab). Then, your agent can spend the allowance, as described in the last step.
Here are the important code snippets to get you up and running:
Pre-requisites
- A deployed Safe Smart Account
- The Smart Account should hold an amount of the ERC20 token for which the allowance will be given
Set and use a spending limit for the AI agent
Enable the Allowance module on your Safe
When you set a spending limit from Safe Wallet, the allowance module will be enabled automatically. You will use the Safe Protocol Kit. Here is a code example to enable it programmatically:
_16import Safe from '@safe-global/protocol-kit'_16import { getAllowanceModuleDeployment } from '@safe-global/safe-modules-deployments'_16_16const preExistingSafe = await Safe.init({_16 provider: RPC_URL,_16 signer: OWNER_1_PRIVATE_KEY,_16 safeAddress: safeAddress_16})_16_16// Add Module_16const allowanceModule = getAllowanceModuleDeployment({ network: '11155111' })!_16const safeTransaction = await preExistingSafe.createEnableModuleTx(_16 allowanceModule.networkAddresses['11155111']_16)_16const txResponse = await preExistingSafe.executeTransaction(safeTransaction)_16console.log(txResponse)
Set spending limit for AI agent
Now you can set a spending limit to your AI agent:
_55import { getAllowanceModuleDeployment } from '@safe-global/safe-modules-deployments'_55import Safe from '@safe-global/protocol-kit'_55import { getAllowanceModuleDeployment } from '@safe-global/safe-modules-deployments'_55import { OperationType, MetaTransactionData } from '@safe-global/types-kit'_55_55const ERC20_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'_55const preExistingSafe = await Safe.init({_55 provider: RPC_URL,_55 signer: OWNER_1_PRIVATE_KEY,_55 safeAddress: safeAddress_55})_55_55const allowanceModule = getAllowanceModuleDeployment({ network: '11155111' })!_55_55const allowanceModuleAddress = allowanceModule.networkAddresses['11155111']_55_55const callData1 = encodeFunctionData({_55 abi: allowanceModule.abi,_55 functionName: 'addDelegate',_55 args: [AGENT_ADDRESS]_55})_55// agent can spend 1 USDC per day:_55const callData2 = encodeFunctionData({_55 abi: allowanceModule.abi,_55 functionName: 'setAllowance',_55 args: [_55 AGENT_ADDRESS, // delegate_55 ERC20_TOKEN_ADDRESS, // token_55 1_000_000, // allowance amount (1 USDC)_55 1_440, // reset time in minutes (1440 mins = 1 day)_55 0 // reset base (fine to set zero)_55 ]_55})_55_55const safeTransactionData1: MetaTransactionData = {_55 to: allowanceModuleAddress,_55 value: '0',_55 data: callData1,_55 operation: OperationType.Call_55}_55_55const safeTransactionData2: MetaTransactionData = {_55 to: allowanceModuleAddress,_55 value: '0',_55 data: callData2,_55 operation: OperationType.Call_55}_55_55const safeTransaction = await preExistingSafe.createTransaction({_55 transactions: [safeTransactionData1, safeTransactionData2],_55 onlyCalls: true_55})_55_55const txResponse = await preExistingSafe.executeTransaction(safeTransaction)_55console.log(txResponse)
Let the AI agent use the spending limit
Now your agent has a spending limit, either set programmatically or from Safe Wallet.
Here is how the agent can spend it:
_72import {_72 createPublicClient,_72 http,_72 encodeFunctionData,_72 zeroAddress,_72 createWalletClient,_72} from 'viem'_72import { privateKeyToAccount } from 'viem/accounts'_72const ERC20_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'_72_72const allowanceModule = getAllowanceModuleDeployment({ network: '11155111' })!_72_72const allowanceModuleAddress = allowanceModule.networkAddresses[_72 '11155111'_72] as `0x${string}`_72_72const publicClient = createPublicClient({ transport: http(RPC_URL!) })_72_72// Read allowance module to get current nonce_72const allowance = await publicClient.readContract({_72 address: allowanceModuleAddress,_72 abi: allowanceModule.abi,_72 functionName: 'getTokenAllowance',_72 args: [safeAddress, AGENT_ADDRESS, ERC20_TOKEN_ADDRESS]_72})_72_72const amount = 1 // You might want to adapt the amount_72_72// generate hash_72const hash = await publicClient.readContract({_72 address: allowanceModuleAddress,_72 abi: allowanceModule.abi,_72 functionName: 'generateTransferHash',_72 args: [_72 safeAddress,_72 ERC20_TOKEN_ADDRESS,_72 AGENT_ADDRESS,_72 amount,_72 zeroAddress,_72 0,_72 allowance[4] // nonce_72 ]_72})_72_72const agentAccount = privateKeyToAccount(_72 AGENT_PRIVATE_KEY as `0x${string}`_72)_72const signature = await agentAccount.sign({_72 hash: hash as unknown as `0x${string}`_72})_72_72const { request } = await publicClient.simulateContract({_72 address: allowanceModuleAddress,_72 abi: allowanceModule.abi,_72 functionName: 'executeAllowanceTransfer',_72 args: [_72 safeAddress,_72 ERC20_TOKEN_ADDRESS,_72 AGENT_ADDRESS,_72 amount,_72 zeroAddress,_72 0,_72 AGENT_ADDRESS,_72 signature_72 ],_72 account: agentAccount_72})_72_72const walletClient = createWalletClient({ transport: http(RPC_URL!) })_72_72const tx = await walletClient.writeContract(request)_72console.log(tx)
In this example, your agent will get a daily spending limit of 10 USDC.
Next steps
You can find more info in the example repository or in the documentation about the allowance module.
If you have a technical question, feel free to reach out on Stack Exchange (opens in a new tab) with the safe-core tag.