Fintech · Internal system
Tallim
Matching payments to invoices inside a payments company’s finance team: strict rules, similarity and a model each take their turn, and a person confirms what stays unclear.
- Role
- Backend, integrations, AI matching
- Timeline
- 8 weeks
- Stack
- Python · PostgreSQL · OpenAI API
What changed
- 8.7% → 1.9%
- of payments still go to manual reconciliation. The rest close with nobody touching them.
- 120 → 31 hours
- a month of the finance team’s time on reconciliation as a whole. It used to run between 110 and 130.
- 97.8%
- of automatic matches were confirmed with no correction.
- 6.4 minutes → 48 seconds
- on a single payment that reached review: confirming ready candidates instead of working the statement.
The bank infrastructure and the main ERP were left alone: the statement arrives exactly as it always did. And the model was never given the right to post or reverse an operation — it names the likely match, the critical actions stayed deterministic.
Before
Around 31,000 payments a month came through the company, and most met an invoice on rules alone. The remaining 8.7% queued up for the finance team: somebody opened the statement, read the payment reference and worked out which invoice and client it belonged to. That cost 110–130 person-hours a month, and up to 2.4% of payments were tied to the wrong thing on the first pass.
- Around 31,000 payments a month. At that volume manual work stops being counted in cases and starts being counted in percentages: any share left to people becomes a queue that never empties.
- 8.7% of payments did not match an invoice automatically. Some closed quickly; the rest went to review — an open statement, a reference read by eye, and a decision made by a person.
- Reconciliation took the finance team 110–130 person-hours a month. It could not be deferred: while a payment is unmatched, the invoice behind it still reads as unpaid.
- Up to 2.4% of payments were first tied to the wrong invoice or the wrong client. The error surfaced later, but until it did it sat in the books — and in the payer’s history, which is what their next payment was judged against.
What we built
An order of play rather than one model: exact rules first on invoice ID, amount, counterparty, date and bank details, then fuzzy matching, and only then the AI, on whatever is left.
A confidence score on every candidate: above the threshold the match is posted on its own, below it the payment goes to a review screen and a person.
The model proposes, it does not post: operations stayed with deterministic code, and that condition was set at the start rather than after the first mistake.
Outcome
Manual reconciliation did not disappear; it shrank. 8.7% of payments became 1.9%, and 120 hours a month became 31 — the hours of the whole reconciliation, not only of the payments that reach a person. What left those hours was the searching: nobody works a statement from scratch any more, they look at ready candidates with the reasoning attached and either confirm or reject — 48 seconds instead of 6.4 minutes. 97.8% of the automatic matches have needed no correction since launch. The model still cannot move money: it names the likeliest match, and everything that touches the money is done by deterministic code.
The client
A payments company. Around 31,000 payments a month pass through it, and each has to meet an invoice: until a payment is tied to one, the invoice is closed nowhere — not for the client, not in the books, and any figure in a report rests on somebody finding the match.
The client here is internal — the finance team of that same company, which was also the team doing the reconciling: the payments that matched nothing on their own were worked through by hand, from the statement and from what was known about the payer.
The brief
- What the client asked for
- Put AI on the bank statement and have it work out what a payment was for: the model reads the reference and says which invoice it belongs to. The task arrived shaped as classification — text in, invoice out.
- What the brief turned into
- Classification was not enough. A reference could carry “INV 43821”, “payment according contract 22/11”, a legal entity name, an invoice number that belongs elsewhere, a typo — or nothing of any use. Some lines match on exact fields with no model involved; others match on nothing, and no model pulls out of an empty line what was never in it. So the task became the order: rules, similarity, the AI.
Why the obvious route was closed
The reference line is written by the payer, not by any system, and the cost of misreading it is paid in money. Five conditions came out of that, and each is visible in the architecture.
- An LLM cannot be the source of truth
- For a financial operation a model’s answer is an opinion, not a fact: it cannot be audited afterwards or reproduced word for word. So the model could not sit at the front of the chain deciding for everyone. Its place was where the strict rules had already run out.
- A reference is free text
- “INV 43821”, “payment according contract 22/11”, a company name, a typo. A regular expression holds up until the first payer who writes it their own way — and the payers who write it their own way are the entire task.
- An old invoice number matches confidently
- Some payers quote a number that belongs to a different invoice. An exact rule fires on it without hesitation and points at the wrong thing, so even an ID match had to carry weight rather than deliver a verdict.
- The manual history is not ground truth
- Week one went into what people had decided: no other material shows how a payment gets tied to an invoice. But up to 2.4% of those decisions were wrong, so the history served as a source of rules and examples and never as a correct answer to measure against.
- The bank and the ERP stay where they are
- Neither was touched: the bank sends its statement in its own shape, the ERP remains the system of record. What we built sits between them — normalising what arrives and writing the result where it is expected.
How the work ran
- 01
Reading the history
Week 1What had been happening to payments before us: what the references look like, which of them matched on rules, which ones a person tied by hand and what they went on when they did. The week produced not a specification but an order of checks — what can be settled exactly, what is only similar, what is left to a model.
- 02
The reconciliation engine and normalisation
Weeks 2–3The engine on strict rules: invoice ID, amount, counterparty, date, bank details. Ahead of the rules sits the normalisation of the bank data — until the fields off a statement are brought to one shape there is nothing to compare, and a comparison fault reads as a faulty rule.
- 03
AI matching and confidence scoring
Week 4The model was wired to the remainder, to what the rules had not closed. It looks at the payment reference, the client’s history and the related documents, and it returns not one answer but a list of candidates, each with a confidence score — the number the paths part on.
- 04
The review screen
Week 5An interface for disputed payments: candidates with the reasoning for each, the payer’s history beside them, a decision in one action. The screen was measured in time rather than looks — there is nothing left to search for, only something to confirm or reject.
- 05
The ERP and the books
Week 6A match has to reach the place where accounts are kept. The ERP was not modified: the system writes into it through its API what a person used to enter by hand after reconciling — and that is the only point where any of this touches the accounting side.
- 06
Shadow mode
Week 7For a week the system ran on live data and posted nothing: it produced matches alongside the people, and the two sets were compared. What that tested was behaviour on the flow rather than accuracy on history — on the day’s own payments, next to the decision a person had made on them.
- 07
Automatic mode
Week 8Matches above the threshold began posting without anyone, the rest began arriving on the review screen. Manual reconciliation was not abolished — it became what it was meant to be: a queue of disputed cases instead of a pass over the whole flow.
Technical decisions
The stack is split along the line where strict rules give out: Python holds the pipeline and the integrations, PostgreSQL keeps payments, invoices and decisions together with the reasoning behind them, and the OpenAI API answers for a single stretch — the one where the strict rules have given out.
An order, not a choice between rules and AI
Exact rules first, then fuzzy matching, then the model. The order is not there to save money: the earlier a rule fires, the easier the result is to explain. The model receives the remainder rather than the flow — the payments where no ID turned up, the amount did not agree, and the counterparty is named differently from the invoice.
Normalisation before the first rule
Statement data is brought to one shape before a single rule is applied to it; otherwise the rule argues with the formatting rather than the payment. It is the dullest part of the pipeline and the only one whose faults damage the exact rules and the model equally.
The model gets context, not a line
What goes in is the payment reference, the client’s history and the related documents. Similar cases are found by vectors rather than by text: the embeddings live in pgvector, inside the same PostgreSQL as the payments and invoices, so a candidate and the evidence behind it never drift into two stores.
A confidence score and a threshold
Every candidate carries a number, and one comparison settles what happens to it: above the threshold the match posts itself, below it the payment lands on the review screen. The threshold is a set value rather than a property of the model — it is the dial the balance of automation and manual checking moves on.
Reviewing instead of searching
Nobody looks for an invoice any more; they check the ones offered. Candidates are ordered by confidence, and each says what backs it — the ID matched, the amount agreed, the reference resembles one from this client’s earlier payment. Hence 48 seconds against 6.4 minutes: confirming costs less than searching.
The model proposes, the code posts
The AI neither carries out nor reverses a single financial operation. It names the likeliest match, and everything that moves money or the books is done by deterministic code, on rules that can be read and replayed. The limit was accepted before the first line was written, not after the first mistake.
What stayed outside the scope
Some of this was left out by a decision taken before the start, not by what week eight ran out of.
The bank infrastructure. Neither the way a statement arrives nor what it contains was changed: the system normalises what there is and asks the bank for nothing different.
The main ERP. It stays the system of record: we write a match into it rather than move the accounting into a contour of our own.
Posting and reversing operations. The model does neither — that is deterministic code and a person, and there was never a plan to move it.
The quality of the references. The payer writes the line and the project does not touch it: an empty reference is still empty, and a person still works it out.
The remaining 1.9%. It is not an interim figure on the way to zero: the threshold was set so that a payment without enough confidence reaches a person instead of being closed on a resemblance.
The system has been running in automatic mode since week eight. The one role left to a person is the last one: a payment short of confidence is posted only once somebody has confirmed the match.
More projects
Construction
Prokta
Tender documents read for a general contractor’s commercial department: an archive of files goes in, a report of terms comes out, every line of it with a page reference.
91%
of the control sample’s critical terms the system found on its own
Python · Qdrant · OpenAI API
Restaurants
Ostera
A chain of 64 restaurants: figures from five systems in one layer, where a question about yesterday’s profit is asked in plain words and answered with the data behind it.
64 restaurants
counting on one set of definitions
NestJS · ClickHouse · OpenAI API
Contact
Send a description of the task
A reply with the scope, the timeline and a budget estimate comes within 24 hours.
The first call is 30 minutes, with no commitment on your side.