cd /news/ai-agents/the-chatbot-refunded-twice-the-retry… · home › topics › ai-agents › article
[ARTICLE · art-153676] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

The Chatbot Refunded Twice. The Retry Looked Successful

A developer documented a failure pattern in AI agents that perform consequential actions through APIs, using a composite example in which a chatbot issued a duplicate $89 refund after a payment request timed out. The retry appeared successful because the provider processed the same business action twice, and the agent could not distinguish a lost response from a genuine failure. The proposed fix assigns a stable actionId generated from trusted workflow state, passes it as an idempotency key, enforces a unique database constraint, and replaces a simple SUCCESS/FAILURE/RETRY model with states including CONFIRMATION_UNKNOWN and REQUIRES_REVIEW.

by read4 min views1 publishedOct 20, 2026

The production trace looked harmless:

14:03:11  Refund requested for order 4821
14:03:12  Payment provider called
14:03:22  Request timed out
14:03:24  Agent selected retry
14:03:26  Refund completed: $89

The chatbot reported success. The customer received confirmation. No exception remained open.

Two days later, finance found two $89 refunds.

The first payment request had not failed. Only the response had failed to reach the chatbot. When the agent retried, the payment provider processed the same business action again.

The retry looked successful because it was successful.

That was the bug.

This is a simplified composite example of a failure pattern that appears whenever an AI agent can perform consequential actions through APIs.

Developers often treat a timeout as evidence that an operation failed.

It is only evidence that the client did not receive a response within the expected time.

Several things may have happened:

The chatbot cannot distinguish these outcomes from a timeout alone.

A human support agent might , check the payment dashboard, and confirm the order status. An AI agent may interpret the timeout as permission to repeat the tool call.

This is why “retry on failure” is not a complete reliability policy.

Suppose the agent calls this tool:

refundPayment({
  orderId: "4821",
  amount: 89
});

After a timeout, the model generates the same arguments and calls the tool again. The two calls look identical to us, but the payment system may see two independent refund requests.

The application should create a stable action identifier before execution:

type RefundCommand = {
  actionId: string;
  orderId: string;
  amount: number;
  requestedBy: string;
};

const command: RefundCommand = {
  actionId: "refund:4821:approved-request-7",
  orderId: "4821",
  amount: 89,
  requestedBy: "support-agent"
};

The important field is actionId.

It represents the business action, not one HTTP attempt. Every retry of that refund must reuse the same identifier.

Do not ask the language model to invent or remember this value. The surrounding application should generate it from trusted workflow state and persist it before calling the payment provider.

If the provider supports idempotency keys, pass the stable identifier with every attempt:

async function issueRefund(command: RefundCommand) {
  return paymentProvider.refund(
    {
      orderId: command.orderId,
      amount: command.amount
    },
    {
      idempotencyKey: command.actionId
    }
  );
}

The first request may complete without returning a response. When the retry arrives with the same key, the provider should return the original result instead of creating another refund.

That protects the external transaction, but the application still needs its own record:

await db.refundCommands.create({
  data: {
    actionId: command.actionId,
    orderId: command.orderId,
    amount: command.amount,
    status: "PENDING"
  }
});

Place a unique constraint on actionId.

If two workers receive the same command simultaneously, only one should be allowed to create the execution record. The other should read the existing state instead of starting another action.

If an external API does not support idempotency, a database check alone may not be sufficient. Two processes can both check before either records completion. Use a lock, serialized queue, transactional outbox, or another mechanism that guarantees one executor for that business action.

The chatbot’s original workflow understood three outcomes:

SUCCESS
FAILURE
RETRY

That was too simple.

The refund command needed states that described what the system actually knew:

PENDING
EXECUTING
CONFIRMATION_UNKNOWN
COMPLETED
REJECTED
REQUIRES_REVIEW

A timeout after sending the request should move the command to CONFIRMATION_UNKNOWN, not directly to FAILURE.

From there, the system can query the provider using the idempotency key, inspect a webhook, or send the case to a human. The chatbot should not independently decide that repeating a financial action is safe.

Most tests simulate an API rejecting a request before doing any work.

The more dangerous case is a successful operation followed by a lost response.

A useful integration test should reproduce this sequence:

Then run the test with two workers executing the same command concurrently.

The assertion that matters is not how many HTTP attempts occurred. It is how many financial transactions were created.

A 2026 Kore.ai survey of 408 enterprise IT and engineering leaders found that 79% of consequential autonomous actions required manual reversal. Among those reversals, 93% were described as costly or disruptive.

Better prompts cannot solve a missing transaction boundary.

If you are evaluating an AI chatbot development company, do not ask only whether its chatbot can process a refund. Ask what happens when the refund succeeds but its response disappears.

A reliable chatbot must know more than how to complete an action.

It must know when the action may already be complete.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-chatbot-refunded…] indexed:0 read:4min 2026-10-20 · —