18 September 20262 min read← All notes

The most valuable move is sometimes nothing

A failed payment is a decision, not an error to be retried. On pricing every recovery action, including silence, and on the flattering 3× that had to go.

  • decision-systems
  • payments
  • ml

A payment fails. Most systems answer with a loop: wait, retry, wait longer, retry again. The loop never asks whether the next attempt is worth its fee, whether the customer has already been nudged twice this week, or whether the kindest and most profitable thing would be to leave the matter alone.

Reclaim begins from that unasked question. Recovery is treated as constrained optimisation, and every candidate action, a retry, a reminder, a human call, a quiet wait, is given a price before anything is done.

The rule on the wall

The whole engine rests on one line of arithmetic:

EV(a) = P(recover | s, a) x RecoverableAmount
        - InterventionCost(a) - ComputeCost(a)
        - RiskPenalty(s, a)   - ContactFatigueCost(s, a)

choose a* = argmax EV(a)

The subtle term is the one that is easy to forget. Doing nothing sits in the same list, with its own expected value, so restraint is a decision the system can choose on the evidence rather than a failure to act.

A modest model, chosen on purpose

The probability of recovery comes from a calibrated logistic regression. Gradient boosting was the fashionable alternative, so both were run on the identical held-out split, and the simpler model won: a Brier skill of 0.162 against 0.135, a ROC-AUC of 0.690 against 0.636. An expected-value engine spends probabilities, not rankings, so calibration is the property that matters, and it is the one the plain model keeps.

Attention is a budget too

Unconstrained, the policy wanted to escalate 91.6% of events to a person. No operations team staffs that. A capped daily budget, ranked by expected value, matches or beats escalating everyone, which is a polite way of saying that most escalations were never worth a human's afternoon.

The model never touches money

The language model writes the words of a reminder and nothing else. It is kept away from the payments client five independent ways: mapped types, a dependency-free interface, an ESLint boundary, an import-graph test, and a deterministic template that takes over whenever the model is unavailable. Arithmetic, idempotency and the audit trail belong to plain, property-tested TypeScript.

Idempotency itself rests on a single UNIQUE constraint rather than a distributed lock in a second datastore. One transaction inserts the event and enqueues it; duplicate webhook deliveries collapse at the database. A real kill -9 mid-batch, a restart and a lease expiry later, the ledger still settles to exactly one audit row.

The number that had to go

The first headline read 3×. It was wrong, and not by accident: the comparison behind it could not have come out the other way. Rebuilt against a baseline that could genuinely win, the honest figure on 3,042 held-out events is 1.42× the net value of retrying everything.

A result earns trust only after it has been given a fair chance to lose.

The full case study, with the architecture and the 574 tests that guard it, hangs in the Reclaim room.