Agent Integration Guide
How an external AI agent (or any automated system) executes transactions through a BVCC Agent Wallet (BVCCAgentWalletV4). All permissions, spending limits and call policies are enforced on-chain — the agent cannot exceed them no matter what its code does.
How it works
- ●The wallet owner authorizes an agent address from the app (
/wallet/agents), signing with biometrics / WebAuthn (passkey). This callsauthorizeAgent()on the wallet with the agent’s limits. - ●The agent calls
executeAsAgent()on the wallet directly, as a normal EVM transaction — no ERC-4337 UserOp, no bundler, no WebAuthn signature needed. - ●The agent pays gas from its own EOA. The transferred funds come from the wallet.
- ●Every execution is validated against the agent’s on-chain permission set and charged a 0.15% protocol fee.
Two hard requirements for the agent account:
- ●Must be an EOA.
authorizeAgentrejects addresses with deployed code (AgentMustBeEOA). Smart-contract agents are not supported. - ●Must be authorized and not expired at execution time, and agents must not be paused (
pauseAgents()is the owner’s emergency stop).
The entrypoint: executeAsAgent
mode — ERC-7821/7579 execution mode. Use the batch mode constant:
executionData — ABI-encoded array of Execution structs:
A single call can batch multiple executions; limits are checked over the whole batch (cumulative ETH and cumulative per-token amounts).
The four call types
Each Execution item is classified on-chain and checked against the matching whitelist:
| Case | Shape | Checks applied |
|---|---|---|
| 1. ETH send | value > 0, empty callData | recipient whitelist |
| 2. ERC-20 transfer | callData starts with 0xa9059cbb (transfer(address,uint256)) | token whitelist + per-token amount cap + recipient whitelist |
| 2b. ERC-20 approve | callData starts with 0x095ea7b3 (approve(address,uint256)) | token whitelist + per-token amount cap + spender checked against recipient whitelist |
| 3. DeFi / anything else | any other callData | target in the protocol whitelist + a call policy registered for the selector |
- ●The agent can never call the wallet itself (
AgentCannotCallWallet) — owner functions stay owner-only. - ●
approveamounts count toward per-token daily/total budgets, same as transfers. This is intentionally conservative: it prevents draining via an externaltransferFromafter a large approve.
Call policies (case 3)
For a DeFi call to go through, the wallet owner must have registered a call policy for that protocol + selector. This is separate from allowedProtocols (which only whitelists the target) and is what stops a stolen agent key from redirecting funds. You don’t set policies — the owner does, in the app — but you need to know what passes:
- ●Selector must be allowed. If the owner hasn’t registered a policy for the exact
target+ selector, it reverts withSelectorNotAllowed. - ●Recipient must be the wallet. For pinned selectors (SwapRouter02, Aave Pool supply/withdraw/borrow/repay, …) the recipient /
to/onBehalfOfargument must be the agent wallet — put the wallet there, not your own address. OtherwisePinnedArgMismatch. - ●Complex calldata is validated on-chain. For the Universal Router (
execute) and Uniswap v4 PositionManager (modifyLiquidities), the call is routed to an on-chain validator that checks every recipient. Fails closed (PolicyValidationFailed) if the validator isn’t active on that chain or a recipient isn’t the wallet.
Cases 1, 2 and 2b (ETH send, token transfer, approve) are not affected by call policies — they keep using the recipient/token whitelists. Call policies only gate case 3.
Spending limits
All set per-agent in authorizeAgent. 0 always means unlimited / disabled. Checked in this order:
| Limit | Scope | Error on violation |
|---|---|---|
maxPerTxWei | ETH per single Execution item | ExceedsPerTxLimit |
tokenMaxAmounts[i] | token amount per batch (per token) | TokenBatchLimitExceeded / ExceedsTokenMaxAmount |
tokenDailyLimits[i] | token amount per UTC day | TokenDailyLimitExceeded |
tokenTotalBudgets[i] | token amount lifetime | TokenTotalBudgetExceeded |
periodBudgetWei + periodDuration | ETH per rolling period (auto-rollover when the period elapses) | PeriodBudgetExceeded |
dailyLimitWei | ETH per UTC day (resets at 00:00 UTC) | DailyLimitExceeded |
totalBudgetWei | ETH lifetime | AgentBudgetExceeded |
expiry | unix timestamp after which the agent is disabled | AgentPermissionsExpired |
totalSpentWei, periodSpentWei, per-token spent) is preserved across re-authorizations — the owner can change limits without resetting what the agent already spent.Whitelists
Max 20 entries each. Semantics differ:
| Whitelist | Empty means |
|---|---|
allowedTokens | deny all ERC-20 transfers/approves |
allowedProtocols | reverts (NoProtocolsWhitelisted) — at least one protocol required for DeFi |
allowedRecipients | any destination allowed (applies to ETH recipients, token recipients, and approve spenders when set) |
Fees
Every agent execution pays a 0.15% protocol fee to the BVCC fee wallet (0x3e3eb089169a7315a994947465ce5f5FC3A307D4), versus 0.05% for the personal smart wallet. Fee logic per call type: deducted from ETH sends, charged on top of token transfers (the wallet must hold amount + fee), and computed from balance deltas on DeFi calls.
Reading agent state
A well-behaved agent should read these before submitting and size its transaction to fit the remaining budget.
Error reference
All reverts use custom errors (4-byte selectors). The most relevant for an agent:
| Error | Cause |
|---|---|
NotAuthorizedAgent() | caller is not an active agent (never authorized, or revoked) |
AgentPermissionsExpired() | block.timestamp >= expiry |
EnforcedPause() | owner called pauseAgents() (OpenZeppelin Pausable) |
AgentCannotCallWallet() | an Execution.target is the wallet itself |
ExceedsPerTxLimit() | one item’s value > maxPerTxWei |
DailyLimitExceeded() / PeriodBudgetExceeded() / AgentBudgetExceeded() | ETH limits (day / period / lifetime) |
NoTokensWhitelisted() / TokenNotAllowed() | token transfer with empty whitelist / token not listed |
ExceedsTokenMaxAmount() / TokenBatchLimitExceeded() | per-tx/batch token cap |
TokenDailyLimitExceeded() / TokenTotalBudgetExceeded() | per-token day / lifetime budgets |
NoProtocolsWhitelisted() / ProtocolNotAllowed() | DeFi call with empty whitelist / target not listed |
AgentMustBeEOA() | the agent address carries code — V4 checks this on every execution, so an EIP-7702 delegation added later locks the agent out until removed |
TokenCallWithValue() | a transfer/approve carried native value — V4 requires zero value on token calls |
SelectorNotAllowed() | no call policy registered for this target + selector |
PinnedArgMismatch() | a pinned calldata word (recipient/spender) is not the wallet / not a whitelisted protocol |
PolicyValidationFailed() | a DEEP-policy validator denied the call, reverted, or isn’t registered/active (V3) |
CalldataTooShort() | DeFi calldata under 4 bytes — no selector to check (V3) |
RecipientNotAllowed() | destination/spender not in allowedRecipients |
Owner-side errors (OnlyWallet, InvalidAgent, AgentMustBeEOA, ArrayLengthMismatch, TooManyTokens/Protocols/Recipients, UnknownAgent, AgentNotActive, ZeroAmount) only appear when authorizing/managing agents.
Events
AgentExecution is the one to index for monitoring agent activity off-chain.
Example A — Foundry script (Solidity)
Send ETH from the agent wallet, signed by the agent EOA:
Example B — TypeScript / viem
The same call from Node.js — an ETH send plus a USDC transfer in one batch:
If the call reverts, simulate it first (client.simulateContract with the same args) — viem decodes the custom error name, which maps directly to the error reference above.