# agent double-refunded a customer: refund script ran twice on retry

## TL;DR
Wrap every refund in an idempotency key derived from the payment plus the refund reason, and check for an existing refund before creating a new one. If a double refund already happened, issue no third refund; instead record the over-refund and arrange to re-collect the difference. Most refund endpoints dedupe on the key, so a crashed-and-retried script stops being a double-charge vector.

```text
agent double-refunded a customer: refund script ran twice on retry
```

## Fix it

1. **Derive one stable idempotency key per logical refund.** Compose it from payment id + refund reason + date, e.g. `refund-[payment_id]-partial-[yyyymmdd]`. Success check: re-running the script with the same inputs returns the original refund object instead of creating a new one.
2. **Query before you create.** List refunds for the charge and skip any charge that already has a refund matching your reason. Success check: a pre-flight list returns 0 unmatched charges before the create call.
3. **Persist a processed list.** Record each completed refund (charge id, refund id, timestamp) in durable storage, and read it at startup. Success check: after a mid-run crash, the restarted script skips all recorded rows.
4. **Recover an existing double refund.** Confirm both refunds posted, void nothing automatically; log the overage, notify ops, and re-invoice the extra amount rather than attempting a third adjustment. Success check: the customer ledger shows exactly one net refund after reconciliation.

## When this applies

- A refund script, cron, or agent retried and a customer shows two refunds for one payment.
- You are building an agent that issues refunds and want retry safety.

## When it doesn't

- The refund failed the first time and never posted; that is a create-retry problem, not a double refund.
- You are reconciling refund webhooks, not issuing refunds from a script.

## Compatibility

- Pattern: any agent harness (scheduled job, retry loop, multi-step checkout agent) calling Stripe Refunds, Adyen refunds, or PayPal refunds endpoints.
- Works on Stripe API 2024-06 and later (idempotency keys supported on POST /v1/refunds).

### Variant: refund posted twice after a network timeout

The create call timed out, the agent retried, and both calls actually landed. Same fix: the idempotency key turns the retry into a no-op replay of the first call.

### Variant: batch refund job restarted from zero

The whole batch re-ran because the first run crashed. Same fix plus a checkpoint file: record progress after every N refunds so a restart resumes mid-list.

## Why it happens

Refund create calls are not safe to retry bare. If the first call succeeded but its response was lost (timeout, crash, deploy), the retry is indistinguishable from a brand-new refund request, so the provider happily creates a second one.

## Edge cases

- Partial refunds: a second refund for a DIFFERENT amount is legitimate; scope your pre-flight check to the same amount and reason, not just the same charge.
- A refund that failed validation may still be retried with a corrected amount; use a new key when the amount changes, never reuse the old one.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_BuVSScCWJrGECnhbJvw9OQ
