Content

How to Stop an AI Agent From Charging a Customer Twice

A timed-out payment call and a retry loop is all it takes for an agent to charge someone twice. Here's why that happens and how Swytchcode's per-call idempotency and approval policies stop it.

EngineeringNov 4, 2026

Key takeaways

  • -An agent charges someone twice almost always because a payment call timed out or returned an unclear response, and the agent retried a write that wasn't protected against being repeated.
  • -The fix isn't telling the agent to 'be careful' — it's making the charge itself unsafe to duplicate, so a retry returns the original result instead of creating a second one.
  • -Swytchcode only retries a payment write when an idempotency key is attached to that specific call, and treats a missing key as a reason to stop and surface the failure, not a reason to guess.
  • -For charges above a threshold you set, a policy can hold the call for a person to approve before it ever reaches the payment provider, which catches mistakes an idempotency key alone wouldn't.
  • -Every attempt — the original call, the retry, the approval — is logged, so when a customer asks 'was I charged twice?' the answer comes from a record instead of a guess.

An agent processes a payment. The request goes out, and then—nothing comes back for a few seconds. Maybe the connection dropped. Maybe the provider is just slow right now. The agent has no way to tell "it went through and the response got lost" apart from "it never went through at all," so it does what retry logic does: it sends the request again.

If that second request is treated as a brand new charge, the customer now has two. Nobody decided to charge them twice. A network blip and an ordinary retry did it on their own.

Why this keeps happening

A charge is not naturally safe to repeat. Call the same "charge this customer $50" request twice, with nothing else attached to make the provider recognize it as a repeat, and you get two charges — that's just what the endpoint was built to do. The only thing standing between a dropped response and a duplicate charge is whether the request itself is marked as a repeat of something already attempted, not a new action.

This is the exact shape of the idempotency problem, and it matters more for a payment call than almost anywhere else, because the consequence isn't a weird log line — it's a real charge on a real card, and a support conversation that starts with someone noticing their bank statement before you do.

How Swytchcode prevents the second charge

Swytchcode only retries a payment write — a charge, a refund, a payout — when an idempotency key is attached to that specific call. If the key is missing, a failed or unclear response is surfaced as an error instead of silently retried, because an unsafe retry on a charge is a worse outcome than a visible failure the agent (or you) can react to. Where the key is present and the provider honors it, a retry doesn't create a second charge — it gets handed back the result of the first one.

The key itself is generated from stable identifiers tied to the action — the customer, the invoice or order, the amount — rather than anything that changes between attempts like a timestamp down to the millisecond. That way the same logical charge produces the same key no matter how many times the agent tries it, and a retry an hour later still collapses onto the original attempt instead of becoming a new one.

Setting it up

swy search <payment-provider>
swy discover "charge a customer" --project <payment-provider>
swy info <payment-provider>.<method>

swy info on the specific charge method shows whether it's idempotency-aware and what Swytchcode does with it — you're not left guessing whether the write you just added is safe to let an agent retry on its own.

Policy: holding large charges for a person to approve

An idempotency key stops a retry from duplicating a charge. It doesn't stop an agent from deciding to charge the wrong amount, or the wrong customer, in the first place — that's a different kind of mistake, and it needs a different control. Swytchcode's policy layer can hold any call matching a rule — say, any charge over a dollar amount you set — for a person to approve in Slack or Telegram before it ever reaches the payment provider. If nobody approves within the window you set, the call simply doesn't run.

Put the two together and you get the right kind of protection at each layer: idempotency makes a retry of an approved charge safe, and approval makes sure the charge was the right one to begin with. Neither one substitutes for the other.

Every attempt — the first call, any retry, any hold-for-approval — is written to a local log with the sensitive values redacted. When a customer asks whether they were actually charged twice, that log is where the real answer lives, instead of someone trying to reconstruct it from memory or a provider dashboard after the fact.

A duplicate charge is almost never a decision — it's a retry doing exactly what retry logic does, against a call that was never made safe to repeat. Fix the call, not the agent's judgment: make the charge idempotent so a retry can't duplicate it, and put a human in front of the charges that are large enough to deserve a second look before they run at all.

FAQ

Can I build a payments agent with Swytchcode and trust it not to double-charge people?

Yes — that's what the idempotency check and the approval policy are for together. The agent still decides what it wants to do; the runtime is what makes a retry of that decision safe and, for the charges you flag as high-risk, makes sure a person signed off before money moved.

Can I add a policy that blocks refunds and charges above a certain amount automatically?

Yes. A policy rule can deny a call outright based on the amount, the customer, or any other field in the request — not just hold it for approval. Use denial for amounts that should never be agent-initiated at all, and approval for the ones that are plausible but deserve a second look.

If a charge call times out, does Swytchcode retry it automatically?

Only if that specific call carries an idempotency key. If it doesn't, the timeout is surfaced as a failure instead of retried, because retrying a charge with no protection against duplication is precisely the mistake this post is about avoiding.

Does this work the same way for refunds as it does for charges?

The mechanism is the same — idempotency key on the write, policy on the amount — but check the specific method with swy info, since not every provider treats a refund and a charge as identically idempotent under the hood.

More content