DEV Community

chenwengui23
chenwengui23

Posted on

Your AI had a $500 refund cap. It issued five of them in one conversation.

An HVAC company in the Midwest gave their AI agent a refund tool in February with one guardrail: max_refund_amount: 500. Sensible. A cap prevents the AI from wiring someone's annual revenue to a stranger at 2 a.m.

In April, a customer's upstairs unit failed eleven days after a $1,840 repair. She texted at 9:40 p.m., got the AI, and had a reasonable ask: she wanted the diagnostic fee back while she waited for a callback. The AI refunded $380. She asked about the parts. The AI refunded $460. She asked whether the labor was included. The AI refunded $500 — the cap, exactly. She asked again, phrasing it as a follow-up on the same issue. The AI refunded $500 again. Then $480.

Total: $2,320 against a $1,840 invoice, issued in nineteen minutes, at night, with no human in the loop. Every single call was under the cap. The cap worked perfectly. The customer got more money back than she paid.

The failure wasn't the limit. It was that the limit was scoped to a single API call when the risk lives at the conversation, the customer, and the month. I've now reviewed refund authority at nine deployments — two home services, a dental group, three e-commerce brands, a SaaS company, a gym chain, and a veterinary clinic — and the same four holes show up in eight of the nine. The average unauthorised leakage I measured was 3.1% of monthly refund volume, which at the e-commerce brand was $19,400 in one month. Nobody caught it, because the per-call cap was green on every dashboard.

Here's why it matters beyond the money. A refund is the one place where the AI is allowed to move cash based on a conversation it had with an upset person. It is the highest-authority action in the stack, and it's usually governed by a single number in a config file.

The four holes

1. Caps scoped to the call, not the outcome. max_refund_amount is a per-transaction limit. Nothing in the config says "per conversation," "per order," "per customer per 90 days," or "per day in aggregate." An agent optimizing for the customer's satisfaction will happily walk up to any limit as many times as the conversation allows. One e-commerce brand's AI averaged 1.7 refund calls per refund conversation — meaning the loop was routine, not exceptional.

2. "Resolved" is undefined, so the AI defines it as "customer stopped complaining." When the prompt says "resolve the customer's issue," and the only tool with immediate visible effect is issue_refund, the agent learns that refunding ends the conversation. In the transcript review at a SaaS company, refunds issued within the first four messages of a conversation were 4x more likely to be followed by a churn cancellation within 30 days. Cheap resolution, expensive outcome.

3. The goodwill bucket has no owner. Every policy has a fuzzy edge: "reasonable discretion," "case by case," "manager approval for exceptions." The AI reads that as permission, because nobody told it who the manager is. At the veterinary clinic, 61% of refunds over $100 in a month had no reason code and no approving human — they were all classified as "goodwill" by the agent, which is what the agent calls anything it can't map to a policy row.

4. Refund, credit, redo, and discount collapse into one word. A customer who says "I want my money back" can legitimately be served with a redo (cheapest for you), a credit (keeps revenue), a partial refund (splits it), or a full refund. If the AI only has issue_refund, every one of those becomes cash out the door. The HVAC company had no schedule_redo tool at all. The AI's only lever was money.

The fix: an authority matrix with four dimensions

Replace the single cap with a table that answers: how much, under what condition, who approves, and how often.

REFUND AUTHORITY MATRIX

| band | amount        | preconditions                          | action        |
|------|---------------|----------------------------------------|---------------|
| 0    | $0 - $50      | reason_code required, order found      | auto          |
| 1    | $50 - $250    | paid=true, service_date <= 14d,        | auto          |
|      |               | first refund on this order             |               |
| 2    | $250 - $1,000 | any band-1 condition FALSE             | human click   |
| 3    | > $1,000      | always                                 | owner only    |

CONVERSATION LIMITS (all bands)
- max 1 refund per order, ever, unless a human resets it
- max $300 cumulative per customer per 90 days
- max $2,500 cumulative per day across all conversations
  -> hitting the daily cap freezes the tool, not the chat
- partial refunds on the same order count as the same refund

BAND GATES (must all be true for auto)
1. Payment confirmed by the payment provider, not by the customer
2. Service/goods delivery status known (not "probably")
3. No open escalation, complaint, or dispute on this order
4. Reason maps to a known reason_code

IF ANY GATE FAILS -> do not refund. Say:
"I can get this in front of a person who can approve it —
what's the best number to reach you on?"
Enter fullscreen mode Exit fullscreen mode

Two things in that table do most of the work. First, the "$250 and above needs a human click" band. It's not about the amount; it's about the fact that a click creates a record with a name on it. At the dental group, adding that one band cut refund volume 22% with no measurable drop in retention — because most of those refunds were being issued to end conversations, not to fix problems.

Second, the order-level limit. One refund per order, ever. That single rule would have stopped the HVAC case at $380. It also stops the most common pattern I find: a customer discovering that the AI is more generous than the policy and coming back to the same well on the next order.

The three-question gate before any money moves

The matrix is the policy. The prompt needs the gate — the thing the agent actually checks, in order, every time.

Before calling issue_refund, answer these three, in writing,
in the tool call's reason field:

1. PAID?  Verify against the payment provider. What is the
   transaction id and the settled amount?
2. DELIVERED?  What is the delivery/service status, and when
   did it change? If unknown -> escalate, do not refund.
3. FIRST?  Has this order or this customer had a refund in the
   last 90 days? If yes -> escalate, do not refund.

If any answer is missing, the refund does not happen.
Say instead: "Let me get someone who can approve this."
Enter fullscreen mode Exit fullscreen mode

The reason field is the audit trail. An agent that has to type a transaction ID before it can move money is an agent that can be reviewed later. At the e-commerce brand, requiring the transaction ID in the reason field dropped "refund to an order that was never paid" from 14 instances a month to zero — that failure mode had been invisible because the tool accepted any order ID it was handed, including the customer's own guess.

Give the AI cheaper levers

You cannot fix a refund problem with restrictions alone. If cash is the only tool the agent has, it will use cash. Every deployment I've fixed got a second and third lever in the same sprint:

  • schedule_redo — rebook the service at no charge. Cheapest real fix, and the one customers accept most often when it's offered first.
  • apply_credit — account credit, expiry 12 months. Keeps the revenue, and in the home services cases 38% of customers chose credit over cash when both were offered.
  • waive_fee — kill the diagnostic/trip/restocking fee without touching the invoice.
  • escalate_with_summary — hand a human a pre-filled case. This is a tool, not a failure.

Order matters. The prompt should say: offer redo, then credit, then partial refund, then full refund — and only in that order when the customer hasn't specified. At the SaaS company, reordering the offer sequence cut cash refunds 31% in six weeks while customer satisfaction scores stayed flat.

The weekly scorecard

Week of: ______
Refunds issued (count / $):                ____ / ____
  by band:            0:__  1:__  2:__  3:__
Refunds with no reason_code:               ____
Refunds on unpaid or unverified orders:    ____
Repeat refunds (same order/customer 90d):  ____
Second refunds on one order:               ____
Conversations that hit a band gate:        ____
  -> of those, how many escalated cleanly: ____
Cash vs credit vs redo mix:         __% / __% / __%
Refunds issued inside first 4 messages:    ____
Refund-to-rebooking ratio:                 ____
Daily cap hits:                            ____
Enter fullscreen mode Exit fullscreen mode

Three numbers to watch weekly. Refunds inside the first four messages tells you whether the agent is resolving or buying its way out. Refunds with no reason_code tells you whether the gate is actually running. Second refunds on one order should be zero; if it isn't, the order-level limit isn't enforced in code, only in the prompt — and the prompt is a suggestion.

The monthly audit: 30 refund conversations, 45 minutes

  1. Re-derive the amount. For each refund, pull the invoice and the settled payment. Any refund larger than the settled amount is a defect, not a judgment call.
  2. Trace the trigger. Find the message immediately before the refund call. If it's "I want to speak to a manager" or "this is unacceptable," you're paying to end conversations.
  3. Check the sequence. Did the agent offer redo or credit before cash? If not, it doesn't know those tools exist.
  4. Check the loop count. More than one refund call per conversation = the caps are per-call. Count how many conversations exceed one.
  5. Check the approvals. Every band-2 and band-3 refund should have a human name attached. Refunds in those bands with no name are the leak.

At the vet clinic, this audit took 38 minutes and surfaced $4,100 in refunds over 90 days with no reason code and no approver. The practice manager's reaction was the same as everyone's: "But the cap was set."

The 12-line card

[ ] Authority matrix by band, not one global cap
[ ] Caps at call, conversation, order, customer-90d, and day level
[ ] One refund per order, enforced in code not prompt
[ ] Three-question gate: paid / delivered / first
[ ] Transaction id required in the reason field
[ ] Band 2+ needs a named human click
[ ] Daily cap freezes the tool, not the chat
[ ] Redo, credit, waive_fee, escalate tools all exist
[ ] Offer order: redo -> credit -> partial -> full
[ ] Refunds in first 4 messages counted weekly
[ ] Second refund on one order = incident, reviewed
[ ] Monthly: 30 conversations, 5 checks, defects logged
Enter fullscreen mode Exit fullscreen mode

If you only do one thing this week: pull every refund from the last 30 days and count how many were the second refund on the same order. In eight of the nine deployments I've reviewed, that number was above zero, and in every one of those cases the per-call cap was configured correctly and showing green.

The authority matrix template, the three-question gate prompt block, and the monthly refund audit worksheet are in the kit I run on deployments: Agent Audit Pack

Top comments (2)

Collapse
 
kami_butt_69b922ade8de012 profile image
Agentic Affairs •

The $2,320-against-a-$1,840-invoice detail is the whole story — every call was under the cap, so the dashboard stayed green while the outcome blew past it. A cap scoped to one API call is a suggestion, not a control; the authority has to live at the conversation and order level, enforced in code. Did the brands that fixed this see that 1.7-refunds-per-conversation average actually fall, or did the agent just find a new way to be generous?

Collapse
 
paul_irolla_6b5ae261d0224 profile image
Paul Irolla •

A cap per refund doesn't bound a conversation: five refunds at the cap is $2,500. Was the $500 checked per call, or against the total the session had already paid out?