A home-services company called me after a chargeback warning letter.
Their AI booking agent had been collecting deposits for about seven months. Everything looked fine on the dashboard: booking rate up 11%, no-show rate down from 22% to 9%, deposits collected comfortably into six figures. Then a customer named in the letter opened her banking app, screenshotted four separate charges from the same company on the same card, and sent them to her bank with the word "fraud" attached.
The four charges were $150, $150, $150 and $150, dated three weeks apart. One job. One furnace replacement. The customer had rescheduled three times — a work trip, a sick kid, a contractor running late. Every reschedule went through the AI agent, and every reschedule re-ran the booking flow, which collected a deposit, because collecting a deposit is what the booking flow does.
Nobody at the company had noticed, because nobody was looking at deposits per customer. They were looking at deposits per booking, and every one of those was a valid booking.
The agent's message on each of the four occasions was warm, accurate about the amount, and identical: "A $150 refundable deposit secures your slot and is applied to your final invoice." The word refundable is doing a lot of work in that sentence. The company's actual policy was: deposit is refundable if cancelled more than 48 hours out, non-refundable inside 48 hours, and always applied to the invoice on completion. The AI had learned "refundable" from a marketing page and used it as a universal comfort phrase. It also had no concept that the same job could hold more than one deposit, so it never checked whether one already existed.
By the time I got involved there were 340 jobs with duplicate holds, $47,000 in deposits sitting on customer cards for work not yet performed, and a chargeback rate that had quietly tripled. None of it was malicious. All of it came from the same design mistake: the agent was given the word "deposit" and no model of what a deposit actually is.
The real problem: "deposit" is four different things
Every payment conversation an AI agent has about a deposit collapses four distinct financial objects into one word. They have different rules, different legal weight, and different failure modes:
| Object | What it does | When money moves | Failure if confused |
|---|---|---|---|
| Hold (pre-auth) | Reserves funds on the card | Nothing moves; available credit drops | Customer sees a "charge" that isn't one, calls it fraud |
| Charge (capture) | Takes the money | Immediately | Duplicate captures, no reversal path |
| Credit (applied) | Reduces the final invoice | At invoice time | Customer pays twice, gets a refund weeks later |
| Fee (retained) | Compensates for a cancellation | On cancellation | Agent waives or imposes outside its authority |
The agent in the story was doing charge, describing hold, and promising credit semantics, all in one paragraph, for four separate events on the same job. That's not a tone problem. It's an unmodeled state machine.
The tell is a single question you can ask any AI deployment: "If a customer asks 'is my deposit refundable right now?', what does your agent answer, and how does that answer change between booking, 3 days out, 1 day out, and after the technician arrives?" If the answer is the same in all four cases, the agent is not reading policy. It's reciting a phrase.
Fix 1: Give the agent a money vocabulary and forbid the fuzzy one
The cheapest intervention, and the one that would have prevented all 340 incidents, is a hard rule set on the words the agent is allowed to use about money. It cannot say "deposit." It must say which of the four objects it means, because naming the object forces the state check.
MONEY LANGUAGE RULES
- Never use the bare word "deposit" in customer-facing text.
Say exactly one of: hold / charge / credit / cancellation fee.
- Never describe a hold as a charge. A hold is "reserved, not taken."
- Never use the word "refundable" without the window:
"refundable if you cancel more than 48 hours before your appointment."
- Before quoting any amount, read the job's existing money state.
If a hold or charge already exists for this job, quote nothing. Say:
"I can see a deposit is already on this job — let me confirm the details
with the team before taking anything else."
- Any amount above the service-type ceiling escalates to a human. No exceptions.
That third rule is the highest-leverage line here. "Refundable" without a window isn't a smaller truth — it's a promise the agent can't keep, in writing, on a record the customer will screenshot.
Fix 2: Deposit policy as data, not prose
The company's deposit policy existed. It was three paragraphs on an internal wiki, and the AI had been given a marketing page instead. This is the most common root cause I find: policy lives in prose somewhere, the agent gets the brochure.
Convert it to a lookup table the agent must consult before it speaks:
{
"deposit_policy": {
"version": "2026-10",
"source_of_truth": "billing.deposit_rules",
"by_service_type": {
"diagnostic_visit": { "amount": 0, "type": "none" },
"standard_repair": { "amount": 150, "type": "hold", "capture_on": "completion" },
"install_replacement": { "amount": 500, "type": "hold", "capture_on": "completion" },
"emergency_after_hours": { "amount": 250, "type": "charge", "capture_on": "dispatch" }
},
"refund_window_hours": 48,
"refundable_inside_window": false,
"retained_as": "cancellation_fee",
"max_open_holds_per_job": 1,
"reschedule_behavior": "reuse_existing_hold",
"hard_ceiling_per_customer": 1000,
"forbidden": [
"collecting a second deposit on an existing job",
"calling a hold refundable without stating the window",
"waiving the cancellation fee without human approval",
"quoting a deposit for a service type not in this table"
]
}
}
Three fields carry the weight:
-
max_open_holds_per_job: 1— the direct fix for the four-charge story. One job, one hold, forever, no matter how many reschedules. -
reschedule_behavior: reuse_existing_hold— reschedule is a date change on an existing money state, not a new transaction. If your agent treats reschedule as a new booking, it will duplicate charges at exactly the rate customers reschedule, which for home services is constantly. -
amount: 0, type: "none"for diagnostic visits — an explicit zero is not the same as a missing value. Agents fill missing values with plausible numbers. That's how a $150 hold appeared on a service the company had decided should be free to book.
Note that the last forbidden entry — quoting a deposit for a service type not in the table — is what stops the invention of new deposit tiers. An agent that can't find the row must escalate, not estimate.
Fix 3: One ledger row per job, with an idempotency key
The technical version of the same rule: every payment intent the agent creates must be keyed to the job, not to the conversation.
-
Idempotency key =
job_id+intent(e.g.J-88213:hold). The same key twice returns the existing record instead of creating a second one. Any payment API worth using supports this. Use it. - Money state lives on the job record, not in the conversation transcript. If the agent's only memory of a hold is "it said so earlier in the chat," a new session cannot see it — and reschedules frequently start a new session.
- The job record is the read-before-write gate. Before any payment action: read the job's money state. Non-empty means no new collection, full stop.
I've stopped treating "does the agent check before charging" as an integration detail. It's the whole control. An agent with perfect tone and no read-before-write is a very polite way to double-charge people.
Fix 4: A daily "money in flight" report
Deposits fail quietly because they're invisible between collection and reconciliation. Add one report, generated daily, five rows:
| Metric | Definition | Healthy |
|---|---|---|
| Open holds | Count and sum of uncaptured holds | Flat or falling week over week |
| Holds older than 14 days | Holds on jobs not yet completed | < 5% of open holds |
| Duplicate holds per job | Jobs with > 1 open hold | 0 |
| Hold-to-charge conversion | Holds captured ÷ holds created, trailing 30d | 80–95% |
| Refund requests citing "refundable" | Tickets containing the agent's promise | 0 |
That last row is the canary. Every one of those tickets is a written promise your agent made that your policy doesn't support. In the story above the row would have been non-zero within two weeks — seven months before the bank noticed.
Fix 5: The 12-line deposit audit card
Print this. Run it monthly. It takes about 40 minutes against a decent helpdesk export.
[ ] 1. Query: jobs with 2+ open holds. Count must be 0.
[ ] 2. Query: holds older than 14 days on incomplete jobs. List and explain each.
[ ] 3. Sample 20 transcripts containing "deposit" — did the agent name hold vs charge?
[ ] 4. Sample 20 transcripts containing "refundable" — was a window stated?
[ ] 5. Any deposit quoted for a service type absent from the policy table?
[ ] 6. Any amount collected above the per-service ceiling?
[ ] 7. Reschedule events in period: did any create a new payment intent?
[ ] 8. Cancellation fees waived: which, by whom, within authority?
[ ] 9. Chargebacks and disputes in period — do they cluster on one service type?
[ ] 10. Refund tickets citing a promise the agent made. Log every one.
[ ] 11. Policy table version live in the agent == version in billing? Diff them.
[ ] 12. Total money in flight (open holds + uncaptured charges) vs. last month.
Item 11 catches an embarrassing failure: billing updates the rules, the agent keeps serving last quarter's table, and policy and promises diverge silently. Version the table and diff it monthly.
The numbers
From deployments I've audited where AI agents handle deposits:
-
Duplicate holds are the default, not the exception. Of five deployments where the agent could collect deposits, four had duplicate holds on reschedule. The one that didn't had
max_open_holds_per_job: 1from day one. - "Refundable" without a window appears in 60–90% of deposit mentions before it's explicitly forbidden. It's the single most common unauthorized promise in payment-adjacent AI conversations — more common than pricing errors, because it feels like reassurance rather than commitment.
- Duplicate holds run 2–4% of bookings, which sounds small until you multiply by a $150–$500 hold. On the home-services company above it was $47,000 in flight. Chargeback rates on those cohorts ran 3–5x baseline — the disputes were never about the amount, they were about seeing four charges the customer couldn't explain.
- Adding read-before-write on the job record cut duplicate holds to zero in every case, without touching the model, the prompt voice, or the booking conversion rate.
What actually changed
The home-services company didn't need a new agent. It needed the agent to stop treating a job as a conversation. A job is an object with a money state that persists across reschedules, sessions, channels, and weeks. A conversation is a temporary view of it. Every payment bug I've seen in these deployments is the agent mistaking the view for the object.
Four rules, if you only take four:
- Never say "deposit." Name hold, charge, credit, or fee.
- Never say "refundable" without the window in the same sentence.
- One open hold per job, enforced by idempotency key, reused on reschedule.
- Read the job's money state before every payment action. No read, no write.
None of these make the AI sound less warm. The warmest thing an agent can do with someone's money is be exactly right about what it's doing with it.
Want the full playbook — deposit and pre-authorization policy schemas, the money-language rules, the daily in-flight report, and the audit cards I run on every deployment? Get it here: https://whop.com/agentfast-04b6/
Top comments (0)