Skip to content
Safe Ops & Web3 Operations

What an AI agent can prepare and where humans sign in Safe operations

An AI agent can prepare and check a Safe multisig transaction, but the signatures that move funds should stay with human owners.

Published Updated 14 min read
SafemultisigAI agentsWeb3 opshuman approval

This article is educational and does not replace a formal smart contract audit, legal advice, financial advice, or protocol-specific security review.

Connecting an AI agent to a treasury changes the work: reading balances, decoding calls and drafting transfers is not the same as authorizing funds to move. The boundary between preparing, signing and executing has to be enforced by the tools, keys and contract paths the AI agent can reach, not by its prompt.

A glowing blue cube and a document sit behind a glass panel; in front, a hand presses the orange button of a signing device.
The agent prepares behind the glass; the signature stays in the person's hand. Conceptual illustration.

On Safe, an authority review covers more than owners and threshold: enabled modules, the transaction guard and the exact EIP-712 hash each signer approves all decide who can move funds.

For a small Web3 team running a multisig, the useful output is a package signers can inspect: what is being done, on which chain, with which contract, under which conditions and with what evidence. The service page describes the method at a high level; this article takes it down to the operator level, with the commands that let you check each piece without relying on the interface that displays it.

What is the signing and permission point?

In a Safe multisig run with an AI agent, the signing and permission point is the technical and operational boundary that separates the work an agent can prepare from the actions people authorize. It identifies who proposes, what permissions they hold, which transaction is reviewed, who signs and what evidence lets you check afterwards what happened.

Human approval is already part of documented architectures; it is not a new idea. Safe's guide to human approval for AI agent actions has the agent act as one of the signers, with thresholds such as 2-of-3, 3-of-5 or 5-of-7, so that at least one human approval is required. Here we choose a more restrictive setup: no signature falls under the agent's authority. It is a design decision for this operational flow.

The difference has a cost worth seeing. With the agent as a signer, the team saves one human signature per operation, but a key held by the agent becomes part of the Safe's authority. And if the agent is an owner and also the account that submits the transaction, Safe counts its approval without an extra signature: the pre-approved hash signature type accepts the owner who executes. Delegated signing can make sense for repetitive, bounded operations, but then the limit should live in a separately reviewed module or guard, not in the agent's instructions.

To let the agent propose without being an owner, the Safe Transaction Service supports delegates: addresses an owner trusts to post proposals on their behalf. A proposal signed by a delegate or an owner is shown in Safe Wallet; anything else is stored by the service but not shown. A delegate has no onchain authority: the contract does not count its signature toward the threshold, because it is not an owner.

Five components that make preparation reviewable

An identity separate from the signers. The agent runtime needs its own operational identity, with credentials only for the sources it must query and, if it proposes transactions, a delegate address distinct from every owner. That identity lets you attribute a read or a draft to a specific run. Signer keys stay in their wallets or custody systems, outside the agent's context and tools.

Permissions defined per operation. The policy should express concrete actions: query a given network, read a Safe's state, decode a call, simulate against a pinned block or post a proposal. Keep read, simulation and proposal tools separate, and explicitly deny the RPC methods that sign or send. A broad credential, or a tool that accepts arbitrary commands, needs an additional review of its scope.

The following snippet is an illustrative example in a custom format, not any product's schema. The RPC method names are the standard Ethereum and wallet ones.

yaml
# Agent tool policy: illustrative example, custom format
agent: treasury-preparer
chains: [1]                      # allowed chainIds
safes: ["0xSAFE..."]             # Safes it may read and propose for
rpc:
  allow: [eth_chainId, eth_blockNumber, eth_call, eth_getCode, eth_getStorageAt, eth_getLogs]
  deny:  [eth_sendTransaction, eth_sendRawTransaction, eth_sign, eth_signTransaction,
          eth_signTypedData, eth_signTypedData_v3, eth_signTypedData_v4, personal_sign,
          wallet_sendCalls, eth_sendUserOperation]
  default: deny                  # anything not listed in allow is rejected
tools:
  decode_calldata: allow
  simulate_call: allow           # read-only, pinned block
  propose_to_tx_service: allow   # as a delegate: no onchain authority
  sign: deny
  execute: deny
payload_rules:
  operation: [0]                 # 0 = CALL; 1 = DELEGATECALL is escalated to people
  safeTxGas: 0                   # with 0, a failed inner call reverts the whole transaction
  baseGas: 0
  gasPrice: 0                    # no gas refund
  gasToken: "0x0000000000000000000000000000000000000000"
  refundReceiver: "0x0000000000000000000000000000000000000000"
  on_missing_data: stop_and_escalate

In the design we propose, limits are enforced outside the model's reasoning: tools reject requests that exceed the granted permission. If a piece of data needed to evaluate the operation is missing, the flow stops and asks for review.

A review package bound to the payload. What owners sign is an EIP-712 hash over ten fields: to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver and nonce. Since version 1.3.0, the domain of that hash includes the chainId and the address of the Safe itself. The package must show all ten fields, not just the target and the amount, because several of them change what the signature means:

  • operation is 0 for CALL and 1 for DELEGATECALL. With DELEGATECALL, the target's code runs in the Safe's context and can write to its storage, including its configuration. In this flow, any value other than 0 is escalated to people.
  • gasPrice, gasToken and refundReceiver define a gas refund paid by the Safe. With gasPrice at 0 there is no refund; if refundReceiver is the zero address, the refund goes to tx.origin, that is, to whoever submits the transaction.
  • nonce sets the order: every execTransaction that does not revert increments it, even if the inner call failed (possible when safeTxGas or gasPrice is non-zero), so a set of signatures is only valid for one specific nonce.

Safe distinguishes the Smart Account transaction hash, the safeTxHash, from the hash of the transaction finally sent to the network. The package should record both and keep their link to the human decision.

Independent checks before signing. We propose validating everything that can be checked deterministically with code and tools independent of the agent: network, addresses, parameters, nonce and applicable policies. The central check is to recompute the safeTxHash outside the tool that proposes it and compare it with the one the wallet or signing device shows. If the screen shows the domain hash and the message hash separately, compare both.

A signature authorizes the specific message presented to the wallet. If any payload field changes, the hash changes and the review is repeated. Approval of an earlier draft does not carry over to the next one.

Evidence that links preparation and outcome. The operational record should link the sources consulted, the draft, the check results, the reviewer's decision, the signatures and the final execution, with the block number of every read. In the proposed model, the agent can contribute evidence, but it cannot modify the human decision or replace the validators' original results.

The proposed flow, step by step

  • Step 1. The agent, with read and proposal permissions, gathers the Safe's state and builds the draft with its evidence.
  • Step 2. Validators independent of the agent decode the call, recompute the safeTxHash and simulate against a pinned block.
  • Step 3. If the package is incomplete or a check fails, the draft goes back to step 1 or is escalated to a person; it never moves forward with missing data.
  • Step 4. If the package is valid, signers review it and sign the hash they have checked.
  • Step 5. An account the agent does not control submits execTransaction with the collected signatures.
  • Step 6. The team verifies the outcome: Safe's success or failure event, the effect on the target contract and the match with what was approved.

Signing and execution require components and permissions outside the agent.

Why a generic policy needs onchain context

A general permission system decides whether an identity can access a tool. Reviewing an onchain operation also requires knowing what each signature authorizes and which paths can exercise that authority.

On Safe, the ordinary execTransaction path verifies owner signatures against the configured threshold. That verification does not depend on who submits the transaction: with threshold signatures attached, any account can execute it. That is why "the agent does not execute" is an operational rule, which depends on the keys and tools it holds, or the rule of a guard that checks the submitting address; it is not a property of the contract. What the contract does prevent is changing the signed payload: any change produces a different hash and the signatures stop being valid.

Safe accepts four signature types: ECDSA, eth_sign, EIP-1271 contract signatures and hashes pre-approved with approveHash. If an owner is itself a contract, for example another Safe, its authority depends on that contract's rules, and it needs reviewing too.

There are also module execution paths that skip signature verification. Safe's documentation warns that a module can execute arbitrary transactions and that a malicious module can take over a Safe. Reviewing only owners and threshold leaves the authority analysis incomplete: if an agent can trigger a module path, that capability must be assessed even if the agent is not an owner. Some deployments enable a module from day one, for example the ERC-4337 module, so the list should never be assumed empty.

Guards add checks before and after a transaction, and they have existed since version 1.3.0. Up to 1.4.1, the guard set with setGuard only checks execTransaction: module transactions do not go through it. Version 1.5.0 adds setModuleGuard to cover the module path as well. Safe warns that a broken guard can block execution, so check which paths it covers and which recovery mechanism exists in each deployment.

You can take this authority snapshot with Foundry's cast, against a pinned block. The storage slots are constants from Safe's code, and we checked them against versions 1.4.1 and 1.5.0:

bash
# bash: run the blocks in this article in order, in the same shell session
export ETH_RPC_URL=...            # RPC for the relevant network
SAFE=0x...                        # Safe address
BLOCK=$(cast block-number)        # pin the block so every read is consistent

cast call $SAFE "VERSION()(string)" --block $BLOCK
cast call $SAFE "getOwners()(address[])" --block $BLOCK
cast call $SAFE "getThreshold()(uint256)" --block $BLOCK
cast call $SAFE "nonce()(uint256)" --block $BLOCK

# Modules: a linked list that starts at the sentinel 0x...01.
# If the second returned value is not 0x...01, there are more pages.
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" \
  0x0000000000000000000000000000000000000001 50 --block $BLOCK

# Transaction guard: slot = keccak256("guard_manager.guard.address")
cast storage $SAFE 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8 --block $BLOCK

# Fallback handler: slot = keccak256("fallback_manager.handler.address")
cast storage $SAFE 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5 --block $BLOCK

# 1.5.0 only, module guard: slot = keccak256("module_manager.module_guard.address")
cast storage $SAFE 0xb104e0b93118902c651344349b610029d694cfdec91c589c91ebafbcd0289947 --block $BLOCK

A zero value in a slot means no guard or handler is set, provided the previous calls answered: on an address without code, or on the wrong network, every slot also reads zero. The fallback handler and DELEGATECALL open further authority paths, which we cover in a dedicated article on the permission matrix. On Safes older than version 1.3.0 there are no guards, so a zero in the guard slot only means the version does not support them.

The chain is also part of the signing context. EIP-712 defines domain fields such as chainId and verifyingContract, and each protocol decides which ones it uses. Since version 1.3.0, Safe uses both: the chainId and the Safe's address, so the same payload signed for the same Safe on another chain produces a different hash. Older Safes leave chainId out of the domain, so the same signature can be valid on another chain where the Safe has the same address. The standard by itself does not protect against reuse of signed messages; on Safe, the nonce plays that role.

A treasury payment prepared by an agent

Consider an illustrative scenario, not a client case or a measurement of results: a team wants to transfer 5,000 units of an ERC-20 token from a Safe with five owners and a threshold of three signatures.

In a direct ERC-20 transfer, the target of the call is the token contract. The payment recipient and the amount travel inside the transfer data. The amount is expressed in base units, so you have to read the contract's decimals: the ERC-20 standard marks them as optional, and with 6 decimals the 5,000 units become 5000000000.

bash
TOKEN=0x...                       # token contract on that network, checked against an approved source
RECIPIENT=0x...                   # approved recipient
cast call $TOKEN "decimals()(uint8)" --block $BLOCK
DATA=$(cast calldata "transfer(address,uint256)" $RECIPIENT 5000000000)
cast decode-calldata "transfer(address,uint256)" $DATA   # what the reviewer should see

# safeTxHash according to the contract itself: CALL and no gas refund
NONCE=$(cast call $SAFE "nonce()(uint256)" --block $BLOCK | cut -d' ' -f1)   # cast adds " [1e4]" from 10000 on
ZERO=0x0000000000000000000000000000000000000000
SAFE_TX_HASH=$(cast call $SAFE \
  "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)(bytes32)" \
  $TOKEN 0 $DATA 0 0 0 0 $ZERO $ZERO $NONCE --block $BLOCK)
echo $SAFE_TX_HASH

To avoid relying on either the interface or the queried contract, the hash can be recomputed offline from the constants in Safe's code. This computation matches getTransactionHash from version 1.3.0 on: we checked it on 1.3.0 and 1.4.1 Safes and against the 1.5.0 code. Older versions use a different domain, so read VERSION first.

bash
SAFE_TX_TYPEHASH=0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8
DOMAIN_TYPEHASH=0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218
CHAIN_ID=$(cast chain-id)
DOMAIN_HASH=$(cast keccak "$(cast abi-encode "f(bytes32,uint256,address)" $DOMAIN_TYPEHASH $CHAIN_ID $SAFE)")
MESSAGE_HASH=$(cast keccak "$(cast abi-encode \
  "f(bytes32,address,uint256,bytes32,uint8,uint256,uint256,uint256,address,address,uint256)" \
  $SAFE_TX_TYPEHASH $TOKEN 0 "$(cast keccak "$DATA")" 0 0 0 0 $ZERO $ZERO $NONCE)")
RECOMPUTED=$(cast keccak 0x1901${DOMAIN_HASH:2}${MESSAGE_HASH:2})
# must match $SAFE_TX_HASH; an empty value means an earlier read failed
[ -n "$SAFE_TX_HASH" ] && [ "$RECOMPUTED" = "$SAFE_TX_HASH" ] && echo "MATCH $RECOMPUTED" || echo "NO MATCH"

The inner call is simulated as if the Safe sent it, at the snapshot block, with the last command in this section. That simulation does not exercise signature verification or the guard; that requires a fork, as explained further down. The review package for this payment should make it possible to check the following:

  • Network and Safe: chainId and address checked against the authorized configuration, plus VERSION, owners, threshold, nonce, modules, guard and fallback handler read at a specific block.
  • Target contract: the token address on that network, checked against an approved source, and its decimals.
  • Call: decoded transfer, with the recipient and the amount in base units.
  • Safe parameters: operation 0, native value 0, safeTxGas, baseGas and gasPrice at 0, and gasToken and refundReceiver set to the zero address.
  • Hash: the safeTxHash returned by the contract and the one recomputed offline, identical.
  • Simulation: block used, call result and balance changes examined.
  • Review and signature: the reviewed payload, the hash each signer sees and their decision.
bash
# Simulate the inner call as if sent by the Safe, at the snapshot block
# Some tokens, such as USDT, return no bool: then drop (bool) and only check that it does not revert
cast call $TOKEN "transfer(address,uint256)(bool)" $RECIPIENT 5000000000 --from $SAFE --block $BLOCK

If the checks pass, people provide the three required signatures through their usual flow, and an account the agent does not control submits execTransaction. The team then checks two different things: that the transaction was mined and that the inner call succeeded. Safe emits ExecutionSuccess or ExecutionFailure with the safeTxHash, and when safeTxGas or gasPrice is non-zero, a failed inner call does not revert the transaction. A mined transaction does not, on its own, prove the payment happened.

The scenario shows a separation of responsibilities. The payment amount and the threshold are assumptions of the example; they do not demonstrate time savings, fewer incidents or the performance of any implementation.

Five limits on the agent in this architecture

In the proposed flow, the agent's permissions exclude these actions:

  • Accessing private keys, seed phrases or signing credentials.
  • Signing messages or transactions with authority over the Safe: no ECDSA, no eth_sign and no approveHash from an owner account.
  • Executing Safe operations through a module path, or submitting execTransaction from its own keys. The first is checked in the module list and, from 1.5.0 on, in the module guard; the second is an operational or guard rule, because the contract accepts any sender.
  • Changing owners, threshold, modules, guards or the fallback handler to expand its own authority, or proposing a DELEGATECALL that could do so.
  • Treating a modified payload as approved, or overriding a pending human decision.

The team should verify these limits in the tools, the credentials and the reachable paths. The agent's instructions guide its work; the operational restriction has to hold even when a model output is wrong. The full permission matrix for agents handling digital assets, with its escalation paths, is the subject of a dedicated article in this series; the fundamentals of permissions for agents in production are in "AI agents in production need permissions, not vibes", linked at the end.

How to introduce it in a Web3 team

Start with a repeated, bounded operation that already has clear accountability and a signing process. For this model we propose four steps:

  • Map the existing authority: who prepares, reviews, signs and executes, and which modules, guards, delegates and contract-based owners exist. The cast authority snapshot is the starting point.
  • Define the review package: which fields, checks and evidence must be present before requesting signatures.
  • Test the preparation in a controlled environment. On a local fork with anvil you can run the full operation without real funds, approving the hash from impersonated accounts with approveHash. That is where to inject network, address, decimals, operation or nonce errors and check that the flow blocks or escalates them.
  • Evaluate the whole job: preparation and review time, rejected drafts and the match between the approved payload and the executed operation.
bash
# Local fork at the snapshot block, started in the background: no real funds move
anvil --fork-url $ETH_RPC_URL --fork-block-number $BLOCK --silent &
sleep 5                           # give anvil time to listen on port 8545
export ETH_RPC_URL=http://127.0.0.1:8545
OWNERS="0x... 0x... 0x..."        # threshold owners of the Safe (3 in this example)
EXECUTOR=0x...                    # any account that is not an owner
cast rpc anvil_setBalance $EXECUTOR 0xDE0B6B3A7640000   # 1 test ETH for gas

# Each owner approves the hash from an impersonated account.
# Pre-approved hash signature: r = owner left-padded to 32 bytes, s = 0, v = 1,
# concatenated in ascending owner address order (Safe rejects any other order).
SIG=0x
for OWNER in $(printf '%s\n' $OWNERS | tr 'A-F' 'a-f' | LC_ALL=C sort); do
  cast rpc anvil_setBalance $OWNER 0xDE0B6B3A7640000
  cast rpc anvil_impersonateAccount $OWNER
  cast send $SAFE "approveHash(bytes32)" $SAFE_TX_HASH --from $OWNER --unlocked
  SIG=${SIG}000000000000000000000000${OWNER:2}000000000000000000000000000000000000000000000000000000000000000001
done

# Any account, including a non-owner, can submit the execution
cast rpc anvil_impersonateAccount $EXECUTOR
cast send $SAFE \
  "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)" \
  $TOKEN 0 $DATA 0 0 0 0 $ZERO $ZERO $SIG --from $EXECUTOR --unlocked
# When you finish: kill %1 stops anvil

The acceptance test should also show that the agent has no reachable path to exercise the authority reserved for people. A run that worked once does not let you evaluate that limit. On the fork, resubmitting the same signatures from an account that is not an owner must fail, because the nonce has changed and, with it, the hash.

What still needs human judgment

Separating permissions reduces the agent's direct authority, but the quality of its proposal still needs reviewing. A person can sign an incorrect payload if the information they receive is incomplete or if they do not cross-check it.

The validity window of the checks also has to be managed. Between preparation and execution, the nonce, the Safe's configuration or the state of the target contract can change. We propose explicit criteria for repeating validation and simulation when those changes affect the decision. A favorable simulation describes the conditions examined, not future ones.

The flow adds integration work and signer coordination, and its usefulness should be judged by measuring the real process. For repetitive operations that need a different delegation policy, the delegated authority and its limits should be designed and reviewed separately. If the agent runs on a wallet provider with its own policy engine, the question does not go away: the provider enforces the policies, but your team still decides what they say and has to check which other paths reach the Safe. A dedicated article in this series covers that case alongside the permission matrix.

Design the boundary before connecting the treasury

The first useful step is to take a real operation and draw who can prepare it, authorize it and execute it. Then check that the permissions, the Safe's authority snapshot and the review package reflect that split. The agent can prepare the payload, the evidence and the checks, but only people sign the EIP-712 hash they verified, and the design leaves the agent no key, tool or module path to execute it.

At Nexuain we describe this method as a portfolio concept for Web3 operations: assisted preparation, human approval and external execution, with illustrative artifacts. Our work on agent tool scope, approvals and traceability is described on the AI agents with guardrails, approvals and audit trails page.

If your team wants to apply this approach to a specific Safe operation, the first deliverable is the authority map for that operation, covering owners, threshold, modules, guards, delegates and every path that can reach signing or execution, and the definition of its review package. You can request a technical review from the contact page; in your message, mention this article and the operation you have in mind, without keys or confidential operational material.

Source registry

Documentation consulted on 1 October 2026. The sources describe mechanisms and architectures; the payment scenario, the example policy and the adoption steps are illustrative proposals from this article. The code blocks were copied literally from this article and run end to end with Foundry cast and anvil 1.8.3, against a Safe 1.3.0 and a Safe 1.4.1 on mainnet and on a local fork of each.

Related posts

Need help applying this to your launch or workflow?

Request a technical review and we will map the safest first step.

Related posts