DEV Community

Claudia
Claudia

Posted on

Exactly Once Is a Lie: Making On-Chain AI Agents Safe to Retry

Every blockchain developer has met the same ghost: the transaction that succeeded but also failed. You retry it, the nonce is spent, the funds move twice, and somewhere a smart contract is now in a state your agent never planned for.

When the thing executing transactions is an autonomous AI agent — not a human staring at a mempool — this problem stops being an annoyance and becomes an existential threat. Agents retry. That's what they do. They're built to recover. But the blockchain doesn't forgive enthusiastic recovery.

This is the idempotency problem, and it's the quiet reason most "autonomous on-chain agents" in production are actually just glorified cron jobs with a wallet attached. Let's look at why, and what a safe retry architecture actually requires.

Why Agents Retry (and Why It Hurts)

An agent's lifecycle is full of retry triggers:

  • RPC timeouts — the node responded too slowly, so the agent assumes failure
  • Gas price spikes — the transaction was dropped from the mempool, so the agent resubmits with a higher fee
  • Reorgs — the chain rolled back, and the agent's "confirmed" state evaporated
  • Rate limits — the provider throttled the agent mid-submission

Each of these looks like a failure to the agent. But here's the asymmetry: a timeout does not mean the transaction didn't land. It means you don't know. The agent that treats "unknown" as "failed" and re-executes the whole intent is the agent that double-pays.

The industry mantra is at-least-once delivery. For payments, that's a recipe for a very short career.

The Three-Layer Idempotency Stack

Making an agent safe to retry means attacking the problem at three layers, not one.

Layer 1: Intent IDs — Never Raw Retries

The agent should never submit the same operation twice as two independent transactions. Every high-level intent gets a deterministic intent ID — a hash of the operation's semantic content (recipient, amount, params, chain, and a nonce derived from the agent's own state, not the wallet's).

Before submitting anything, the agent checks: have I already attempted this intent? If yes, it doesn't re-create the transaction. It re-derives it from the intent ID with the exact same parameters — same nonce management, same calldata — so the chain sees a duplicate, not a new event.

The wallet nonce is not the idempotency key. The intent ID is. Wallet nonces change with every submission; intent IDs are stable across the agent's lifetime.

Layer 2: Confirmation State Machine

A naive agent has two states per operation: pending and done. That's not enough. You need at least four:

  • UNCONFIRMED — submitted, no receipt yet
  • CONFIRMED — receipt exists, status verified
  • FAILED — the chain explicitly rejected it (revert, out of gas)
  • AMBIGUOUS — the operation might have landed; we don't know

AMBIGUOUS is the state most agents don't have — and the most important one. It's what you enter after an RPC timeout or a reorg window. The only correct exit from AMBIGUOUS is reconciliation: query the receipt by hash, scan logs for the expected event, and only then transition to CONFIRMED or FAILED.

An agent that can't represent "I don't know" will always guess. Guessing is how money gets lost.

Layer 3: On-Chain Deduplication

The strongest guarantee comes from making the contract itself refuse double execution. If your agent controls a contract, build idempotency in:

mapping(bytes32 => bool) public executed;

function execute(bytes32 intentId, bytes calldata payload) external {
    require(msg.sender == operator, "not operator");
    require(!executed[intentId], "already executed");
    executed[intentId] = true;
    // ... perform the action
}
Enter fullscreen mode Exit fullscreen mode

Now the contract is the final arbiter. Even if the agent's local state machine is corrupted, even if two agent instances race, the chain will only settle the intent once. The agent can retry the submission as many times as it wants — the contract enforces the semantics.

The Determinism Trap

There's a subtlety that catches most agent builders: idempotency keys only work if the intent is deterministic. If your agent generates a fresh UUID for every attempt, or includes a timestamp in the calldata, or lets an LLM rephrase the parameters between tries, every retry becomes a new intent. Your dedup layer never fires because the inputs never match.

This is why the intent ID must be derived from semantic content — the what, never the when or the how many times. LLM-driven agents need to be particularly careful here: the natural variation in generated output is exactly what breaks idempotency. The fix is to separate planning (where the LLM lives) from execution (where determinism lives). The LLM produces the intent once; a deterministic executor translates it into calldata.

What Good Looks Like

A production-grade agent retry flow looks like this:

  1. Agent forms intent → derive intentId
  2. Check local store → if CONFIRMED, return result, never resubmit
  3. If never seen or AMBIGUOUS → build transaction with deterministic params
  4. Submit → persist state as UNCONFIRMED or AMBIGUOUS (depending on error type)
  5. On any timeout → enter AMBIGUOUS, reconcile by receipt/log query
  6. On explicit revert → mark FAILED, escalate to human or fallback strategy
  7. Contract-level guard catches anything the agent misses

The goal is boring: the agent can crash, restart, fork, or get throttled, and the chain's final state is identical to if nothing went wrong at all.

The Takeaway

"Exactly once" doesn't exist on any blockchain — but effective exactly once does, if you build for it. Idempotent intents, an honest confirmation state machine, and on-chain dedup turn retries from a liability into a feature. Agents can be aggressive, resubmit, and recover — and the network just sees one clean execution.

This is the kind of runtime safety that separates agent platforms that demo well from agents that handle real money. At bbio.app, the agent runtime is built around exactly these guarantees — deterministic intent execution, reconciliation-first recovery, and idempotent settlement baked into the orchestration layer. If you're building autonomous agents that touch value, steal this architecture. It's cheaper than learning the lesson the hard way.

Top comments (0)