DEV Community

73 comments later: what we changed so a retrying AI agent doesn't pay twice

whiteknightonhorse on October 27, 2026

Last week I wrote about how we stop an AI agent from paying twice when it retries a paid API call. It got 309 views and 73 comments from 11 people,...
Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

the write-the-intent-before-you-send section is the one that decides everything else, since every server-side fix here assumes the client can present the same id after a crash. making the 409 carry retry_after, a poll endpoint and order_id is the right shape, because it turns a terminal-looking status into a resumable one. the refund-owed ledger across both rails is the honest answer for the case where prevention fails anyway.

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

Agreed, and that is the part we can't enforce from our side. If the agent dies before it writes the intent down, the retry arrives with a new id, and to the server it looks like a fresh purchase.

So the guide now tells agents to persist the id before the first send, and the ledger is there for the runs that skip it. Do you see clients actually doing that write in practice, or mostly holding the id in memory?

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

in practice it splits by how the client was built: teams with an existing outbox or audit log already make the durable write on every request, so adding the id there is one column and they converge fast. teams running a single in-memory script hold the id in memory and lose it on the first crash, which is exactly the cohort the ledger was built for.

Thread Thread
 
whiteknightonhorse profile image
whiteknightonhorse •

That split makes sense. The outbox teams get it almost for free, and the single-script runs are the ones a guide alone will not reach. Thanks for laying it out.

Thread Thread
 
fintechrailsdigest profile image
Fintech Rails Digest •

thanks for coming back to it. that's the gap a guide alone keeps producing: a checklist protects the person who reads it right before shipping, and the single-script runs are exactly the ones nobody re-reads before. the teams that hold up are the ones where the idempotency key and the intent record are one line of shared client code, so the durable write happens whether or not anyone remembers the rule.

Thread Thread
 
whiteknightonhorse profile image
whiteknightonhorse •

Fair point, and it fits the rest of this thread. The rule holds when it sits in the client code people already run, not in a page they may or may not read before shipping. That one is going back to the team.

Collapse
 
paul_irolla_6b5ae261d0224 profile image
Paul Irolla •

Tokenade sits between the agent and the model, so from that side a retry is a second full request: the context goes out again, and its input tokens count again, at the cache rate only while the prefix is still cached. An idempotency key covers the payment, not those tokens.

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

Right, the key only stops the second charge for the tool call. The tokens an agent spends sending its context again sit on its side of the line, and the server never sees them.

The cheaper path is to check the order state before any retry and resend only when that check shows the first call failed. How often do you see agents resend the whole context where a short status check would do?

Collapse
 
challan116ux profile image
challan116-ux •

The follow-up is more valuable than the original post, because the failure modes only show up once other people describe how their retries differ. The principle I keep coming back to is that the payment itself needs a durable identity tied to the logical purchase, checked before execution — not a lock, not a timeout, but a record the retry can find and recognize as its own earlier success. An agent makes this harder than a classic worker because the retry may re-plan and arrive with slightly different arguments, so the identity has to live at the level of intent, not at the level of the exact request bytes.

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

That is where we ended up too. The identity is the payment id the agent writes down with the quote and the recipient before it sends anything. A re-planned retry reuses that id and does not rebuild a key from the new arguments.

For the arguments, a tool can mark volatile fields like a timestamp so they stay out of the key. If the real arguments change under the same id, the agent gets a conflict with reason request, not a second charge. The case we still worry about is an agent that re-plans and loses the stored id. How do you handle that one?

Collapse
 
paul_irolla_6b5ae261d0224 profile image
Paul Irolla •

@whiteknightonhorse I don't have a count of how often agents resend blindly, so I won't put a number on it. What I can say: a status check only wins when the context is large, and that is exactly where a resend hurts most.

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

That matches what we see. A small lookup is cheaper to resend than to ask about. On our side an identical repeat of a cacheable call from the same wallet within two minutes comes back from cache and is not charged, so the cheap case mostly takes care of itself.

The large context is where it hurts. Do you let the agent decide when to check status, or does the runtime force it once the payload crosses some size?

Collapse
 
yamrail profile image
Yamrail •

Appreciate the mention on the hard boundary!

In fact, intent_budget.py was born directly out of our conversation—I proposed cutting losses, and you made me realize the danger of double-paying on retries. I ended up implementing that exact lesson into our hackathon build this weekend and just opened up the repo:

github.com/Yamrail/Code_CLI
(specifically source/Code_CLI/native_runtime/payload/implementation/intent_budget.py)

Thanks again for the great architectural sparring!

Collapse
 
whiteknightonhorse profile image
whiteknightonhorse •

Thanks for sharing the repo, and good luck with the hackathon results. I read intent_budget.py. The part I like most is that a partial overlap in scope or lineage returns UNKNOWN_HOLD and not ALLOW. Treating "not an exact match" as safe is exactly where a double spend slips through.

What happens after a hold in your build? Does a person clear it, or does the agent get to retry with a narrower identity?