When we let agents run claims end to end — FNOL, reserve, settle, close — four money defects surfaced in our own claims product within the first few weeks. None of them was an AI failure. All four were bugs that had been sitting in the software, waiting for something patient enough to walk every path.
Here they are, with what each looked like in the ledger and how to detect it in yours.
1. Reserve leakage on closure
What happened: a claim was settled and closed. The settlement posted correctly. The outstanding reserve was never released, so it stayed on the books as an open liability against a closed claim.
Why it survived: closing and reserve release were separate operations in separate service methods, and the closure path called only one of them. Every individual operation was correct. Every test passed, because the tests tested the operations.
flowchart LR
subgraph expected["Expected"]
A1["Settle $58k"] --> B1["Release reserve
$250k to $0"] --> C1["Close claim"]
end
subgraph actual["What happened"]
A2["Settle $58k"] --> C2["Close claim"]
R2["Reserve still $250k
on a closed claim"]
end
style B1 fill:#e2f5f9,stroke:#0e97b0
style R2 fill:#fbede3,stroke:#b4531b
The detection query. This one is worth running against any claims system today:
-- Closed claims still carrying a reserve. Should be empty.
SELECT claim_ref, closed_at, outstanding_reserve
FROM claims
WHERE status = 'closed'
AND outstanding_reserve > 0
ORDER BY outstanding_reserve DESC;The fix: closure now requires the reserve movement as part of the same transaction. You cannot close a claim without passing the release, in the same way you cannot perform a write without a decision object.
2. The settlement that was never written
What happened: a settlement was approved, the payment was issued, the claim was marked settled — and no settlement record was created. The money left. The row describing why never existed.
Why it survived: the settlement entity was written by a code path that had been refactored into a helper, and one caller stopped invoking it. The claim status update and the payment were on the main path; the record of the settlement itself was on the branch that got orphaned.
This is the most dangerous of the four, because from the outside everything looks complete. The claimant was paid. The claim shows settled. Only when an auditor asks "on what terms was this settled, and who agreed them" does the absence appear.
-- Claims marked settled with no settlement record.
SELECT c.claim_ref, c.settled_at, c.settled_amount
FROM claims c
LEFT JOIN settlements s ON s.claim_id = c.id
WHERE c.status = 'settled'
AND s.id IS NULL;3. Release before pay
What happened: the reserve was released before the payment was confirmed. For a window of time the claim showed no outstanding liability and no completed payment — the money was in neither place.
Why it matters: if the payment then fails — a rejected bank detail, a hold, a downstream error — the claim is left with no reserve and no payment, which means it has silently disappeared from your outstanding liabilities. Nobody is watching a claim that looks finished.
The pattern to look for: ordering. Release after confirmation, never before. If the payment is asynchronous, the reserve stays until the confirmation arrives, and a reconciliation job catches anything that never confirms.
Any pair of operations where one reduces a liability and the other creates an asset must be ordered so that a failure between them leaves the liability standing. The safe ordering is always the one that fails conservatively — an unreleased reserve is a reporting nuisance; a released reserve with no payment is a claim that has vanished.
4. The duplicate payment
What happened: the same settlement was paid twice. A retry after a timeout produced a second payment because the operation was not idempotent.
Why an agent found it and a human had not: this is the clearest example of why running agents against your own systems is worth doing. A human adjuster who clicks Pay and sees a spinner will wait, then refresh, then check whether it went through, and then call someone. An agent gets a timeout and does the correct engineering thing — it retries.
The retry was correct behaviour. The endpoint was not idempotent. The combination paid twice.
# The fix: an idempotency key derived from the business facts,
# not from the request. A retry of the same settlement is the
# same key and returns the original result rather than paying again.
idempotency_key = f"settle:{claim_ref}:{settlement_id}"
# The payment table has a unique constraint on it, so the second
# attempt fails at the database rather than relying on a check
# that could race.An agent will find every non-idempotent endpoint you have, because it retries exactly the way your documentation says it should.
What the four have in common
None of them is a model failure. The agents reasoned fine. Every defect lived in the software between the decision and the system of record — the plumbing, not the intelligence.
All four are also invisible to a model-call log. The calls succeeded. The tools returned 200. The run looked clean end to end. What made them visible was a ledger where money movements sit next to each other and can be checked for the relationships that must hold:
- A closed claim has no outstanding reserve.
- A settled claim has exactly one settlement record.
- A released reserve has a confirmed payment.
- A settlement has exactly one payment.
Each of those is a one-line query against a business event ledger and impossible to express against a trace store, because a trace store has no concept of a reserve.
The four queries, as a standing check
We now run these as scheduled assertions rather than as an investigation. Any non-empty result raises an incident.
| Assertion | Expected | Catches |
|---|---|---|
| Closed claims with reserve > 0 | Empty | Reserve leakage |
| Settled claims with no settlement row | Empty | Orphaned code path |
| Released reserves with no confirmed payment after 24h | Empty | Release before pay, failed payments |
| Settlements with more than one payment | Empty | Non-idempotent retry |
If you run a claims system, these four take about an hour to write and we would be genuinely surprised if all four came back empty the first time.
What this looks like in InsightLense
The four relationships at the end of this post are now standing monitors. InsightLense runs them on a schedule and raises an incident on any non-empty result, so these defects cannot recur silently.
| Assertion | Expected | Found | Exposure | Status |
|---|---|---|---|---|
| Closed claims with reserve > 0 | 0 rows | 0 | — | passing |
| Settled with no settlement row | 0 rows | 0 | — | passing |
| Released reserve, no payment 24h | 0 rows | 3 | $412,000 | incident |
| Settlement with > 1 payment | 0 rows | 0 | — | passing |
An assertion is worth more than a fix here. Each of these bugs was individually fixed in an afternoon; what stops the class returning is that InsightLense holds the invariant, in a place that is independent of the claims code, and complains when it stops being true.
The uncomfortable conclusion
These bugs were all older than the agents. They had been in the product through human use, because humans navigate software forgivingly — they wait instead of retrying, they notice a spinner, they work around a broken path without reporting it, and they carry in their heads the knowledge that a closed claim ought to have its reserve released.
Put something patient, literal and tireless in front of the same software and it walks every path exactly as specified. That is uncomfortable and it is the point. The carrier was built as a conformance harness, and this is what it was for.
Where do your agents already act on real records?
Tell us that, and what you would need to prove about those actions to an auditor. We will set up a hands-on walkthrough within two weeks.
Request a demo →