reckon
Every payment reminder gets a reply someone has to read and act on: paid, promised, disputed, wrong person. reckon reads each one and updates the chase, escalating only the unsure ones. It sends nothing, and never marks a bill paid.
the problem
You chase an unpaid invoice. The customer writes back. Now somebody has to read that reply and work out what it actually means: a promise, a dispute, a claim it is already paid, or the wrong person entirely, and then do the right thing about it.
Reminder tools are everywhere and cheap. The reading is the part that still lands on a person, one reply at a time. That is the half reckon takes.
the tool you already run
Sending the reminder is a solved, cheap thing: QuickBooks Payments ships AI reminders inside its $85/mo plan, and Chaser lists $180/mo for firms under $5M in revenue. If you send invoices, something you already pay for is probably already sending the nudge.
reckon does not send anything, and does not replace any of that. It is a retrofit: the tool you run keeps running and keeps sending. This adds the half it does not do.
where it stops
The reminder goes out; the reply comes back to a person. Telling a real promise from a brush-off, noticing that a customer says they already paid, spotting that the message reached the wrong contact: that reading is the seam the sending tool does not cross.
For the checkable version of the gap: Chaser's features page describes outbound reminders; inbound reply handling is not listed there as of 2026-09-19. That is a statement about a published page on a date, and nothing more.
what reckon does with a reply
reads what came back
a promise to pay, a dispute, a wrong person, someone saying they already paid, or nothing that matters. it puts each reply into one or more of seven buckets and writes down why.
updates the chase
a promise pauses the chase until the date they named. a dispute stops it and hands you the thread. a wrong contact stops chasing that address. noise changes nothing.
never marks a bill paid
someone claiming they paid opens a check for you, it does not close the invoice. there is no button in the system that marks a bill paid, on purpose.
hands you the unsure ones
when it is not confident, the reply goes to a person with every reason attached, instead of guessing. that path is a normal outcome, not an error.
the numbers
Measured on a fixed set of 72 replies, scored one at a time. Nothing here is a projection, and no before/after time saving is claimed, because the old way was never timed.
The number that matters most is the one it missed: an out-of-office naming a live finance contact, read as nothing-to-do. Published rather than rounded away. The technical track breaks every class down with its counts.
the stack
one reply, six stages, shared across every build
Only classify spends. decide chooses what happens, and nothing a reply says can widen what act is allowed to do.
how the judgment is shaped
The classifier asks seven separate yes/no questions, one per class, not one pick-one question across seven labels. Six of the 72 replies genuinely carry two classes at once, paying part of a bill while disputing the rest, claiming payment while offering to reissue. A pick-one answer manufactures a wrong answer on every one of them; one judgment per class lets two things be true.
Each judgment is a probability, and a probability is not an action. The gate turns each one into a band: below 0.50 the class is ignored, in the middle it is reviewed by a person, and only at or above the class's threshold does it act unattended.
one probability per class, three bands
dispute and claimed_payment sit higher, because acting wrongly on them costs the most: standing down a chase on a bill that was not paid, or ignoring someone who is arguing.
Dates and amounts are components plus code, not extraction. Only 4 of the 12 promise replies contain a numeral; the rest say “by Friday”, “in the next cycle”, “before month end”. The model names the kind of time reference; code does the calendar arithmetic against a fixed ledger date. When nothing fixes a date, nothing is invented: the promise is recorded dateless and a person sets one. A guessed date silently resumes a chase.
The decision is plain TypeScript. No model chooses what happens, because reply text is untrusted and adversarial steering is a documented weakness of the model, so every side effect sits behind both a probability gate and a closed action enum. Because only the probabilities are bought, moving the sandbox's confidence threshold costs nothing: the judgment is unchanged and only the policy re-runs.
what breaks, and what happens
- Idempotency on every side-effecting action, keyed on the reply, the invoice and the action, so a retry cannot fire the same effect twice.
- Retry with backoff on every model call; an exhausted retry is written to the audit log as a failed result, never dropped into a silent success.
- An audit row per decision: the seven probabilities, the asserted classes, the action, the tie-break rule that fired, and the reason.
- A hard monthly spend cap. Reaching it degrades the demo to a committed recorded run behind a visible notice, never an error page.
- Escalation is a normal outcome. Below threshold, ambiguous, or nothing confident: a person gets it with the invoice, the full reply and every probability attached.
the numbers, per class
Measured on a fixed test set of 72 replies, run 2026-09-19, model jev-1.13.0. There is deliberately no single accuracy figure: the class distribution is imbalanced on purpose, to look like a real chasing inbox, and one headline number would flatter the system on exactly the rare, expensive classes that matter. Ordinary and hard subsets are scored apart. Every threshold was swept on the ordinary 51 only, never on the hard 21 it is reported against.
| class | n | precision | recall |
|---|---|---|---|
| dispute | 5 | 100% (5/5) | 100% (5/5) |
| claimed_payment | 6 | 100% (6/6) | 100% (6/6) |
| promise_to_pay | 10 | 100% (10/10) | 100% (10/10) |
| partial | 2 | 100% (2/2) | 100% (2/2) |
| question | 10 | 100% (10/10) | 100% (10/10) |
| wrong_contact | 5 | 100% (4/4) | 80% (4/5) |
| noise | 13 | 93% (13/14) | 100% (13/13) |
| class | n | precision | recall |
|---|---|---|---|
| dispute | 1 | 50% (1/2) | 100% (1/1) |
| claimed_payment | 4 | 100% (2/2) | 50% (2/4) |
| promise_to_pay | 2 | 100% (1/1) | 50% (1/2) |
| partial | 4 | 100% (4/4) | 100% (4/4) |
| question | 2 | 100% (1/1) | 50% (1/2) |
| wrong_contact | 3 | 100% (2/2) | 67% (2/3) |
| noise | 5 | 100% (4/4) | 80% (4/5) |
On the six replies that carry two classes, five are scored for whether both were asserted; the sixth pairs a class with noise, which earns no secondary credit. Both classes were asserted on four of those five, and the leading class was right on all five. Cost was 0.0084¢ per reply, median machine time 685 ms.
the code
The whole decision is deterministic code over the probabilities: a claim of payment pauses the chase and opens a reconciliation item for a person, and never closes the invoice, because no action in the enum marks a bill paid. The action space is closed by construction, so nothing a reply says can widen it. The full pipeline, the policy, the tie-break rules and the committed run artifact are in the public repo, linked in the demo header above.
Reading every invoice reply by hand? I'll tell you in 15 minutes whether a system can safely take it off you.
see if reckon fitsfree · 15 min · “no, it can’t” is a real answer