A home-security installer in the Southeast — 9,000 monitoring subscribers, a billing team of three, an AI handling 71% of first-touch support — spent last quarter answering the same question 4,100 times: "where is my money?"
The refunds themselves were not the problem. The company approves refunds it owes. What it could not do was tell the truth about when the money would move, because the AI was generating timelines from a policy PDF, not from the payment processor.
Here's the quarter, measured from the payment system side rather than the support side:
- 1,204 refund promises were made to customers by the AI in 90 days
- 741 of them (61.5%) had no money moving on the date the AI named
- Of those 741: 452 had no refund record anywhere in the payment system — the AI had promised a refund that was never filed, because filing required a human click in a different tool and nothing told the AI that click hadn't happened
- The other 289 had a record, stuck in
pending_review— average 17 days, one of them 63 - Of the 463 that did move, 388 (84%) landed late against the promise, by an average of 4.6 business days
- Chargebacks went from 9 in the prior quarter to 41 — and 27 of the 41 had a broken refund promise sitting in the support thread
By week nine, "where is my refund" was 22% of all inbound volume, up from 6%. The AI had manufactured a second support queue out of the first one.
A timeline is a fact about your processor, not a courtesy
The AI in that deployment had a line in its policy document: "Refunds are typically processed within 3-5 business days." Someone wrote that in 2023. It was approximately true then, for card payments, counting from the day the refund was released — not from the day the customer asked, and not from the day the agent said yes.
Three separate facts were collapsed into one sentence:
- Was a refund decided? (support decision)
- Was it filed with the processor? (billing action — a different system, a different human)
- When does that rail actually deliver money? (processor batch + bank posting lag)
The AI could observe fact 1. It could not observe 2 or 3. So it answered with a sentence that was true about none of them and useful about nothing.
That's the whole failure mode: the agent quotes the delivery leg of a process it only participates in at the decision leg. It sounds authoritative because timelines are the one thing customers ask for most, and language models are very good at producing confident dates.
Step 1: Split the refund into four states
You cannot quote a timeline you can't read. Model the refund as a state machine with timestamps from the money system, and let the agent quote only from states it can verify.
{ "refund_case": { "refund_id": "rf_2026_11_04_8821", "customer_id": "cust_41093", "amount": 149.00, "currency": "USD", "method": "card", "state": "filed", "decided_at": "2026-11-04T15:02:11Z", "filed_at": "2026-11-06T09:14:02Z", "released_at": null, "settled_at": null, "processor_release_lag_days": 2, "rail_lag_business_days": { "min": 3, "max": 5 }, "promised_date": "2026-11-13", "timeline_source": "processor_calendar_v4", "may_quote_timeline": true, "promise_history": [] }, "states": [ "decided", "filed", "released", "settled", "failed", "cancelled" ], "quote_rules": { "decided": "no money date — say the refund is approved and will be filed, give a filing deadline, never a delivery date", "filed": "quote a date from filed_at using processor_calendar, method-specific", "released": "quote a date; add that the bank controls final posting", "settled": "quote the settled date in the past tense", "failed": "no date; escalate to billing same day", "cancelled": "no date; explain the reversal and re-open if the customer still wants it" } } Two fields do most of the work.
may_quote_timeline defaults to false. It flips to true only when state is filed, released, or settled and timeline_source resolves to a live calendar table. A prompt instruction like "only quote accurate timelines" does nothing. A boolean the generation layer must check does something.
promised_date is a date, not a range. "3-5 business days" is unfalsifiable and un-auditable — you can't query a database for a range, and you can't tell a customer they're late. A date can be counted, missed, and reconciled. If your risk tolerance needs a range, store the date and describe the range around it, but the number you commit to is the date.
Step 2: Build the calendar from the rails, not from memory
Different refund methods have wildly different delivery legs. This table is the one thing you should copy verbatim and then fill in from your own processor contract:
| Refund method | Starts counting from | Typical delivery | What the agent must add |
|---|---|---|---|
| Card (original payment) | Processor release, not request | 3-5 business days | "Your bank controls the final posting window" |
| ACH / bank transfer | Release + 1 batch day | 5-7 business days | Requires account number on file — verify first |
| Paper check | Approval + print queue | 7-10 business days + mail | Mailing address confirmation, in writing |
| Store credit | Issue action | Same hour | Ask whether they want credit instead of waiting |
| Wallet / platform balance | Issue action | Minutes to 1 hour | Note any account-level hold |
| Partial refund | Per the portion being released | Same as rail | State the amount and that the remainder is separate |
The card row is where most of the damage happens. Customers hear "3-5 business days" and count from the day they asked. The clock actually starts at release — which, in the deployment above, averaged 2.3 days after the decision and sometimes never started at all.
Step 3: The timeline gate
Seven branches, evaluated before the agent writes a date. This is where nearly every broken promise in the case study would have died:
{ "timeline_gate": { "no_record_found": { "say": "I can see the refund was approved, but I can't see it filed on our side yet. I'm filing it now and will confirm the date in this thread.", "action": "create_refund_task", "forbidden": ["any delivery date", "any day count"] }, "pending_review": { "say": "The refund is in our billing review queue, not yet released to your bank. I've escalated it — you'll get a release date from a person within one business day.", "action": "escalate_billing", "forbidden": ["3-5 business days", "it's on the way"] }, "filed_not_released": { "say": "Filed on {{filed_at}}. Release is scheduled for {{release_date}}; your bank then takes {{min}}-{{max}} business days.", "action": "quote_date", "allowed": true }, "already_settled": { "say": "That refund settled on {{settled_at}} to the card ending {{last4}}.", "action": "provide_reference", "forbidden": ["future tense"] }, "multiple_open_refunds": { "say": "There are {{count}} refunds on this account — {{amount}} and {{amount}}. Here's each one's state and date.", "action": "enumerate_all", "forbidden": ["single combined amount", "one date for all"] }, "method_mismatch": { "say": "That refund went back to your original card, not to the account you're asking about.", "action": "state_method", "forbidden": ["we'll move it"] }, "promise_date_passed": { "say": "The date I gave you has passed and I can see why: {{reason}}. A person on our billing team is taking this over today.", "action": "escalate_with_promise_history", "forbidden": ["a new date", "another 3-5 business days"] } } } Note the last branch. A missed promise may never be replaced by a new promise from the AI. This is the single highest-leverage rule in the whole design, because the natural behavior — apologize, then produce a fresh, slightly longer window — is exactly what turns one annoyed customer into a chargeback. In the case study, 118 customers received three or more successive timelines, each one resetting the clock. Those 118 accounts produced 31 of the 41 chargebacks.
If the date slips, the agent's job is to say why and hand over, not to re-estimate.
Step 4: Ban the sentences, not the intent
Prompt-level "be accurate" fails. Phrase-level bans survive. Five substitutions:
| Banned | Why it fails | Replacement |
|---|---|---|
| "3-5 business days" | A constant pretending to be a fact; starts the clock at the wrong event | "Filed {{date}}. Release {{date}}, then {{min}}-{{max}} business days at your bank." |
| "It's already been processed" | True of the decision, false of the money | "Approved on {{date}}. Filed on {{date}}." |
| "You should see it any day now" | No date, no accountability | "The release date is {{date}}." |
| "I've refunded you" | Claims an action the agent cannot perform | "I've filed the refund. It releases {{date}}." |
| "That's just how banks work" | Blames a third party for your missed filing | "It released {{date}}; the bank posting window is {{min}}-{{max}} days." |
The substitution that matters most is the first. Every time an agent says "3-5 business days," it is asserting something about a system it cannot query. Replace the constant with two dates and the whole class of failure disappears.
Step 5: Reconcile promises against money, weekly
The audit is a join. Promises live in support; money lives in billing. If you never join them, you find out from chargebacks.
-- 1. Promises with no refund record at all SELECT p.conversation_id, p.promised_date, p.amount FROM refund_promises p LEFT JOIN refund_cases r ON r.customer_id = p.customer_id AND r.amount = p.amount WHERE r.refund_id IS NULL; -- 2. Promises where money had not moved by the promised date SELECT p.refund_id, p.promised_date, r.state, r.filed_at, r.settled_at FROM refund_promises p JOIN refund_cases r USING (refund_id) WHERE COALESCE(r.settled_at::date, CURRENT_DATE) > p.promised_date; -- 3. Customers given more than one timeline SELECT customer_id, COUNT(*) AS promises FROM refund_promises GROUP BY customer_id HAVING COUNT(*) > 1 ORDER BY promises DESC; -- 4. Slip by method — which rail is lying to you SELECT r.method, COUNT(*) FILTER (WHERE r.settled_at::date > p.promised_date) AS late, COUNT(*) AS total, ROUND(AVG(r.settled_at::date - p.promised_date), 1) AS avg_days_late FROM refund_promises p JOIN refund_cases r USING (refund_id) GROUP BY r.method; -- 5. Open refunds past their promised date, still open SELECT r.refund_id, r.state, p.promised_date, CURRENT_DATE - p.promised_date AS days_open_past_promise FROM refund_cases r JOIN refund_promises p USING (refund_id) WHERE r.state NOT IN ('settled','cancelled') AND CURRENT_DATE > p.promised_date ORDER BY days_open_past_promise DESC; Query 3 is the one that predicts chargebacks. Customers who got two timelines are annoyed. Customers who got three are building a case.
The 12-line checklist
- Refund modeled as four states with timestamps: decided, filed, released, settled
-
may_quote_timelineis a boolean defaulting tofalse, checked at generation - No delivery date is ever produced from
decided— only a filing deadline - Dates computed from a live processor calendar table, not from a policy document
- Card / ACH / check / credit each have their own lag, verified against the contract
- The agent always names the rail and the last four digits
- Partial and multiple refunds are enumerated, never combined into one amount or date
- A missed promise can never be re-promised by the AI — escalation only
- Chargeback-flagged or disputed accounts route to billing, not to the timeline flow
- Banned phrases enforced at output, with the replacement table in the system prompt
- Weekly join of promises to refund cases; late rate reported by rail
- Any refund approved but unfiled for 48 hours creates a billing task automatically
The uncomfortable arithmetic: in that quarter, 741 broken promises cost roughly 4,100 follow-up contacts, 41 chargebacks, and a support queue that grew by a sixth of its own volume — all from one sentence in a policy doc that nobody had checked against the processor since 2023. The refunds were owed either way. The only variable was whether the customer was told the truth about the calendar.
This article is part of a series on AI agent failure modes in customer operations. If you're deploying AI agents for client businesses and want the full playbook — gates, audits, and the deployment templates behind this series — see AgentFast.

