
Consider a serious incident in an enterprise treasury system built around autonomous Web3 agents: monitoring detects that an AI agent’s session key may have been compromised through prompt injection, or that the model has entered an abnormal loop of batch order adjustments. An operator urgently clicks “Revoke Permission” in the admin console. Within 50 milliseconds, the frontend shows “Revoked,” the database marks the agent’s session unavailable, and all subsequent API requests are blocked.
Yet 12 seconds later, the block explorer reports a new confirmed transaction: a 500 USDC transfer signed by the agent 3 seconds before the backend revoked its access has been included on-chain and settled.
Why can funds still leave the account when the backend already says permission has been revoked?
The cause is the mempool execution gap in Ethereum and EVM architectures. In ERC-4337 account abstraction, an agent does not sign an internal command that a centralized service can forcibly interrupt. It signs an independent pending user operation (UserOperation). Just before the administrator clicks revoke, that UserOperation has already passed the bundler’s off-chain prevalidation (simulateValidation) and is waiting for inclusion in a public or private mempool. Miners and validators cannot see the centralized database’s revocation state. Until the on-chain state actually changes, the in-flight transaction in the mempool remains valid for execution.
To close this dangerous window, Agent Wallet Guard implements a coordinated three-level interception architecture in production: a control-plane circuit breaker that responds within seconds, EntryPoint two-dimensional nonce preemption, and a dynamic session recheck during contract execution. This article examines that high-availability defense through actual smart contracts, PostgreSQL unique constraints, and frontend generation guards.
Threat Model and the Mempool Timing Gap
In a traditional Web2 architecture, revoking access can be as simple as blacklisting a token in Redis. In an ERC-4337 smart account, however, execution spans two distinct trust domains: off-chain and on-chain.
+-----------------------------------------------------------------------------------+
| Mempool Exposure Timeline |
+-----------------------------------------------------------------------------------+
| [T = 0.0s] Agent creates a malicious/abnormal command, signs it, and submits UserOp |
| [T = 0.5s] Bundler's off-chain simulateValidation passes; UserOp queues in mempool |
| [T = 2.0s] Operations team detects the anomaly in the console and clicks Revoke |
| -> Database marks Session Revoked (control-plane blocking takes effect) |
| =================== The Mempool Gap =============================== |
| [T = 4.0s] Miner/builder adds UserOp to handleOps batch; broadcasts to L1/L2 validators |
| [T = 5.0s] Block confirmed! EntryPoint drives the contract transfer (funds are lost) |
| [T = 7.0s] Administrator's on-chain revoke transaction finally confirms (too late) |
+-----------------------------------------------------------------------------------+
This timeline shows why database changes or ordinary on-chain transactions alone cannot catch an operation already entering the inclusion process. Blocking 100% of in-flight operations requires defenses at every checkpoint from the application layer down to the smart contract.
Comparing Three Revocation Mechanisms
| Defense | New off-chain requests | In-flight mempool transactions | Cost and latency | Safety under extreme concurrency |
|---|---|---|---|---|
| API/database blacklist only | Takes effect in milliseconds | 0% interception (broadcast UserOps will execute) | No on-chain cost, but a major gap in fund protection | Vulnerable to concurrent bypass |
| Single-nonce preemption (Tx Replacement) | Depends on on-chain confirmation | Partially effective (requires a very high priority fee) | Very high gas cost; cannot target individual sessions | Prone to nonce ordering stalls |
| Agent Wallet Guard’s three-level architecture | Takes effect in milliseconds | 100% interception (execution-time checks + 2D nonce preemption) | Zero-gas prechecks with enforced on-chain blocking | PostgreSQL atomic locks + on-chain epochs |
Designing the Three-Level Interception Architecture

Agent Wallet Guard combines three levels of defense:
+-----------------------------------------------------------------------------------+
| Agent Wallet Guard: Three-Level Interception |
+-----------------------------------------------------------------------------------+
| Level 1: Immediate API control-plane circuit breaker (Off-Chain Gateway Token Purge) |
| - Invalidate in-memory Session Token immediately; refuse all new UserOperations |
| - Atomically mark in-flight user_operation_reservations as invalidated |
| - Return consistent blocking codes: SESSION_KEY_REVOKED / AGENT_PAUSED |
+-----------------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------------+
| Level 2: EntryPoint two-dimensional nonce preemption and invalidation |
| - ERC-4337 EntryPoint: Nonce = (uint192 key << 64) | uint64 sequence |
| - Broadcast a high-priority-fee no-op for the affected agent's key to consume sequence |
| - All old queued UserOps fail validation with AA25 invalid account nonce |
+-----------------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------------+
| Level 3: Dynamic session recheck during smart contract execution |
| - executeValidatedUserOp unpacks sessionSig again and recovers sessionKey |
| - Enforce _requireActiveSession: check for Revoked or expired state |
| - Final fallback: revert at execution even if UserOp and revoke share a block |
+-----------------------------------------------------------------------------------+
Core Implementation: Smart Contracts and Data Flow
1. On-Chain Session Revocation and Terminal State (AgentWalletGuard.sol)
AgentWalletGuard.sol strictly manages session state. Calling revokeSessionKey immediately transitions the key to the terminal state SessionStatus.Revoked; only the owner bound to the smart account may initiate it:
// From AgentWalletGuard_Contracts/src/AgentWalletGuard.sol
/// @notice Revoke a previously enabled session key. Its state becomes `Revoked`
/// (the session's terminal state), after which every
/// `validateUserOp` for this key fails.
function revokeSessionKey(address sessionKey) external onlyAccount {
SessionKey storage session = _sessions[msg.sender][sessionKey];
if (session.status == SessionStatus.None || session.installEpoch != _installEpoch[msg.sender]) {
revert SessionNotFound(msg.sender, sessionKey);
}
if (session.status == SessionStatus.Revoked) {
revert SessionAlreadyRevoked(msg.sender, sessionKey);
}
session.status = SessionStatus.Revoked;
emit SessionKeyRevoked(session.agentId, sessionKey, msg.sender);
}
2. The Critical Check: Recover and Validate the Session During Execution (executeValidatedUserOp)
Many basic ERC-4337 account implementations check the session signature only in validateUserOp, during validation. But that validation happens off-chain before the transaction is included in a block.
Agent Wallet Guard adds execution-time revalidation: immediately before calling the token contract to transfer funds, it unpacks the session envelope from userOp.signature again, recovers sessionKey, and enforces _requireActiveSession:
// From AgentWalletGuard_Contracts/src/AgentWalletGuard.sol
function executeValidatedUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external nonReentrant {
if (!isInstalled[msg.sender]) revert NotAccount();
if (userOp.sender != msg.sender) revert NotAccount();
address configuredUsdc = _accountConfig[msg.sender].usdc;
if (configuredUsdc == address(0)) revert UsdcNotConfigured();
// 1) Validate outer call shape -> (target, value, innerCallData)
(bool shapeOk, address target, uint256 value, bytes calldata innerCallData) =
_decodeOuterCallShape(userOp.callData);
if (!shapeOk) revert InvalidOuterCallShape();
// 2) Strict payment decoding: extract recipient and amount
PaymentDecoder.Decoded memory decoded =
PaymentDecoder.decode(configuredUsdc, bytes32(uint256(0)), target, value, innerCallData);
// 3) Session envelope: agentId || policyId || sessionSig
// Core defense: recover and validate the session at execution to catch revocation or expiry after validation!
bytes calldata sig = userOp.signature;
(bytes32 agentId, bytes32 policyId, bytes calldata sessionSig) = _decodeSessionEnvelope(sig);
address sessionKey;
try this._ecdsaRecoverExternal(userOpHash, sessionSig) returns (address recovered) {
sessionKey = recovered;
} catch {
revert InvalidSessionSigner();
}
if (sessionKey == address(0)) revert InvalidSessionSigner();
// Assert that Session remains Active; otherwise revert immediately
_requireActiveSession(msg.sender, sessionKey, agentId, policyId);
// 4) Validate policy and recipient allowlist ...
// 5) Compute intent hash ...
// 6) Deduct budget ...
// 7) Execute the token transfer
_executeUsdcTransfer(configuredUsdc, innerCallData);
emit AgentPaymentExecuted(agentId, intentHash, policyId, decoded.recipient, decoded.amount);
}
The consequence of this design: even when a malicious UserOp is already in the same block, if the administrator’s revokeSessionKey transaction comes first in block order, or receives earlier inclusion because of a high priority fee, the malicious UserOp that follows reads _sessions[account][sessionKey].status == Revoked at step 3. It must revert, and the token transfer never occurs.
3. Invalidating Operations Through EntryPoint 2D Nonce Preemption
Standard Ethereum transaction nonces are one-dimensional and sequential. If an address has a large transfer stuck in the mempool, it must either wait for inclusion in order or send a higher-fee transaction with the same nonce to replace it (Replace by Fee).
ERC-4337’s EntryPoint, however, introduces a two-dimensional nonce model: Nonce = (uint192 key ≪ 64) ∣ uint64 sequence
In Agent Wallet Guard, each independent agent or session type can receive its own key (for example, key = hash(agentId)). When an administrator needs to block all of an agent’s in-flight transactions urgently:
- The backend reads the smart account’s latest committed
sequencefor thatkey. - The administrator calls the smart account’s own
executeinterface directly and broadcasts a valid empty transaction with thatkeyand currentsequence, or directly increments the nonce sequence. - Once this preemptive transaction lands on-chain, every queued in-flight UserOp carrying the old
sequencefails the underlyinguserOp.nonce != currentNoncecheck when inspected by the bundler or EntryPoint, triggersAA25 invalid account nonce, and is discarded by miners across the network.
4. Persistent Database Reservation Locks and Unique Indexes (0011_erc4337_operations.sql)
On the server, PostgreSQL row-level locks and composite unique keys prevent duplicate issuance and uncontrolled concurrent execution of in-flight UserOps:
-- From AgentWalletGuard_API/migrations/0011_erc4337_operations.sql
CREATE TABLE IF NOT EXISTS user_operation_reservations (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
wallet_id TEXT NOT NULL REFERENCES wallets(id) ON DELETE RESTRICT,
account_address TEXT NOT NULL CHECK (account_address ~ '^0x[0-9a-f]{40}$'),
user_op_nonce NUMERIC(78, 0) NOT NULL,
intent_id TEXT NOT NULL,
approval_nonce NUMERIC(78, 0) NOT NULL,
status TEXT NOT NULL CHECK (status IN ('prepared','submitted','confirmed','failed','expired','invalidated')),
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Core constraint: prevent the control plane from issuing duplicate UserOp Nonces or Approval Nonces for an account
CONSTRAINT user_operation_reservations_account_nonce_key UNIQUE (account_address, user_op_nonce),
CONSTRAINT user_operation_reservations_approval_nonce_key UNIQUE (account_address, approval_nonce)
);
When access is revoked, the system uses one transaction to mark all of the agent’s in-flight reservations in prepared or submitted state as invalidated:
// From AgentWalletGuard_API/src/domain/intents.ts
export const INTENT_REASON = {
AGENT_PAUSED: BusinessErrorCode.AGENT_PAUSED,
SESSION_KEY_EXPIRED: BusinessErrorCode.SESSION_KEY_EXPIRED,
SESSION_KEY_REVOKED: BusinessErrorCode.SESSION_KEY_REVOKED,
POLICY_NOT_ACTIVE: BusinessErrorCode.POLICY_NOT_ACTIVE,
// ...
} as const;
Frontend Engineering: Preventing Stale Prompts with a Generation Guard (useOwnerAuthorization.ts)
Frontend state can also create dangerous races in user interfaces and admin consoles. For example, while revoking authorization over a slow network, an administrator might repeatedly switch between agent tabs. An unresolved promise from an old tab could then open the wrong signature request on the new page.
Agent Wallet Guard introduces a strict generation guard in the frontend:
// From AgentWalletGuard_Frontend/src/hooks/useOwnerAuthorization.ts
export function useOwnerAuthorization(agentId: string) {
const [phase, setPhase] = useState<OwnerPhase>("idle");
const busy = useRef(false);
const generation = useRef(0);
// Increment the generation when dependencies change, invalidating all earlier pending async callbacks
useEffect(() => {
const scope = generation;
scope.current++;
return () => {
scope.current++;
};
}, [agentId]);
async function confirm() {
const gen = generation.current;
// Run RPC simulation precheck
await publicClient.call({ ... });
// Critical check: stop immediately if generation changed while awaiting the precheck (navigation or cancellation)!
if (gen !== generation.current) return;
// Recheck that the wallet's actual account and network still match
if (
(await wallet.getChainId()) !== expectedChainId ||
!sameAuthorizationValue((await wallet.getAddresses())[0], op.owner)
) {
throw new AuthorizationError("contextChanged");
}
if (gen !== generation.current) return;
// Persist local recovery state before sending
localStorage.setItem(authorizationRecoveryKey(agentId), JSON.stringify(...));
// Ask the actual wallet to broadcast
const txHash = await wallet.sendTransaction({ ... });
if (gen !== generation.current) return;
// ...
}
}
Inserting if (gen !== generation.current) return; after every time-consuming await eliminates ghost prompts and stale signing requests caused by component unmounts, asynchronous delays, or tab switches. For a detailed failure analysis, see Cancel Stale Wallet Prompts with Async Generation Checks.
Four Production Deployment Checks
If you are designing an account-abstraction wallet for enterprise autonomous agents, AI customer-service agents, or quantitative trading agents, review its revocation mechanism against these rules:
- Do not rely on frontend/API revocation alone: clicking sign-out in a backend does not make funds safe. On a blockchain, assume that any broadcast UserOp may be included at any moment. Focus defenses on execution-time contract validation and nonce invalidation.
- Recheck session state in
executeValidatedUserOp: a single signature check invalidateUserOpis insufficient. RecoversessionKeyfrom the envelope immediately before transferring tokens and assert that it has not been revoked. - Use ERC-4337 two-dimensional nonces for rapid preemptive invalidation: assign each agent an independent
key; in an emergency, use a higher-sequence transaction with the same key to immediately invalidate all unauthorized operations queued in the mempool. - Add frontend generation guards to prevent stale state: introduce generation locks for every asynchronous private-key signing and authorization flow, preventing invalid signatures from being submitted after concurrent actions or cancellation.
Enterprise AI Wallet Security and Architecture Consulting
Fone builds production-grade secure account infrastructure for enterprise Web3 agents, autonomous trading bots, and decentralized applications. Our Agent Wallet Guard implementation has completed end-to-end production validation across ERC-4337 smart contracts, the BFF gateway, and the frontend review console:
- Read the full agent integration guide: AI Agent Wallet Access Before Your First Integration
- Explore the dual-checkpoint spending guard architecture: Designing Dual-Checkpoint Spending Guards for ERC-4337 AI Agents
- Learn how to prevent concurrent budget overruns: Resolving Session Key Budget Races Under Concurrent AI Agent Requests
- Book an enterprise AI agent funds security audit and custom implementation: Contact Fone Engineering