logo

Idempotency and Retries for Onchain Agent Actions

A timeout tells you the response did not arrive, not that the transaction failed. An agent must check chain state before resubmitting, bind each intent to an identifier it can look up, and manage nonces explicitly. Blind retries are how agents pay twice, and the second payment usually succeeds.

The ambiguity that causes double spends

An agent submits a transaction and does not get a receipt. Three worlds are consistent with that observation and they require opposite responses.

The transaction never reached the network, in which case retrying is correct.

The transaction is pending, in which case retrying may produce a second transaction that also lands.

The transaction confirmed and only the response was lost, in which case retrying is a straightforward duplicate payment.

The naive retry, resubmitting on timeout, is right in exactly one of those three cases and expensive in the other two. Worse, in agent systems the retry is often automatic and rapid, so the duplicate arrives before anyone can intervene.

The rule that resolves all three: a retry begins with a query, not a send. Determine what actually happened before deciding what to do, and treat every timeout as unknown rather than as failure.

Bind intent to an identifier

To check what happened, you need something to check for. That means every action an agent takes should carry an identifier that exists before submission and can be resolved afterwards.

In a payment-for-request flow, that identifier is naturally the request identifier from the payment terms: the agent records that it intends to pay for request X, submits, and can later ask whether a payment for X exists.

The records that make this work are unglamorous. Before submitting, write the intent locally with its identifier and the parameters. After confirmation, write the result. In between, the record is in an unknown state, and that is exactly the state a retry needs to be able to read.

A system without this cannot recover from ambiguity at all. It knows only that a call failed, has no way to ask whether the underlying action happened, and its only options are to retry blindly or to stop and involve a human, which for an unattended agent means stopping.

Nonces decide replace versus duplicate

A resubmission using the same nonce replaces the pending transaction. A resubmission with a new nonce creates a second one. That single detail decides whether a retry is a correction or a duplication, which is why agents that transact should manage nonces explicitly rather than letting each submission request a fresh one from a node that may not know about the pending transaction.

Retry discipline

Query first. Resolve the identifier. If the action already happened, adopt that result and continue.

Distinguish stuck from lost. A transaction that is pending but underpriced needs a fee bump using the same nonce, which replaces it. A transaction that never reached the network needs a fresh submission. These are different actions and confusing them is how duplicates appear.

Bound the attempts. Every retry loop needs a maximum and an escalation path. Unbounded retries against a persistent failure are how a small problem becomes a large bill, and in an agent that also holds spending authority, the bill is literal.

Back off. Immediate retries against a congested network reproduce the condition that caused the failure.

Set a confirmation policy. Decide how many confirmations count as done for your value at risk, and do not let the agent act on an unconfirmed transaction as though it were final. Acting early is a separate failure mode that produces correct-looking behavior on top of an outcome that may still change.

Making the failure survivable

Retry discipline reduces the chance of a duplicate. It does not eliminate it, because distributed systems and unattended software will eventually produce a case nobody enumerated.

Which is the argument for bounding the exposure underneath. If the agent spends from a bounded float, a retry bug costs part of the float rather than everything reachable. The worst version of this failure, an agent looping on a payment with unrestricted authority, is prevented by the ceiling rather than by the correctness of your retry logic.

CryptoCadet provides that bound: a non-custodial USDC rail on Base where the agent spends from a session-key bounded float with custody in the OS keychain, supporting x402-style pay-per-call and ERC-20 subscriptions. Fast finality also shortens the ambiguous window itself, which is the period during which these bugs have room to occur.

The order that works is: bound the float, bind intents to identifiers, query before resubmitting, manage nonces explicitly, and cap retries. The first item makes the rest debuggable rather than catastrophic while you get them right.

Frequently asked questions

How should an agent retry a failed onchain transaction?
By querying first. A timeout means the response did not arrive, not that the transaction failed, so the agent must resolve what actually happened before deciding. If the action already occurred, adopt that result. If it is pending but underpriced, replace it using the same nonce. Only a genuinely absent transaction warrants a fresh submission.
Why do agents double spend?
Because they treat a timeout as a failure and resubmit. The transaction may have been pending or already confirmed, so the resubmission becomes a second real payment. Automated retries make this fast and repeatable, which is why the duplicate usually arrives before anyone notices the first one succeeded.
What role do nonces play in agent retries?
They determine whether a resubmission replaces a pending transaction or creates an additional one. Reusing the nonce replaces, which is what you want for a fee bump on a stuck transaction. Requesting a fresh nonce duplicates. Agents that transact should manage nonces explicitly rather than trusting a node's view of pending state.
How many confirmations should an agent wait for?
Enough for the value at risk, decided in advance rather than per transaction. The important part is that the agent does not act on an unconfirmed transaction as though it were final, since that produces correct-looking downstream behavior built on an outcome that can still change.