Skip to content
megapromotingLet's talk

Solutions · Financial services

An agent that classifies money movements by choosing from a closed list, not by writing a sentence that someone then interprets.

In a financial organisation, a useful agent does not give advice and does not approve anything: it reads documents and transactions, places them into a category from a fixed set, says how confident it is and is allowed to answer “I do not know”. The rest — the decision, the approval, the policy — remains with people.

An offer, with conditionsWe do not write “delivered”, because we do not have an agent of this kind put into production at a client in the financial sector. What we do have, and show exactly as such, is our own ingestion pipeline running on our data: five Python modules, 1,526 lines, which read bank notifications from e-mail and PDF statements, classify each transaction with tools and write it into PostgreSQL with deduplication, plus four automation flows for webhooks from three payment processors. The real state, said in the same sentence: the repository is not under git, the `tests/` and `webhook-server/` directories are empty, and approximate counterparty-name matching uses an n-gram heuristic marked in code as “to be replaced with an embedding service before production”. The financial-platform part in production — request flow, contract, payment schedule, audit — is a separate work, with its own evidence, and there we write “delivered”.

Requests for AI agents in financial services almost always come in the form of “to answer clients’ questions about our products”. It is a legitimate request, but that is not where time is saved. Time is lost elsewhere: someone opens dozens of bank notifications and statements every day, looks at a description like a string of capitals with a code and decides whether it is a client payment, a salary, a tax, an instalment to a lender or an operating expense. The work is repetitive, dull and, precisely for that reason, full of mistakes.

A language model is good at it, with one condition: it must not be asked to write a free-text answer. If you ask it for a sentence, you will parse it yourself, and you will parse it wrongly. In our pipeline, the model does not write: it receives seven tools and must call exactly one — client payment, salary, tax, payment to creditor, operating expense, bank fee or unknown. Each tool requires, in addition to the counterparty name or category, a confidence score between 0 and 1. “Unknown” requires a written reason. The output is a structure, not an opinion.

The second rule is that nothing goes in twice. Each transaction extracted from a notification or a statement receives an external identifier calculated as the first 16 characters of a SHA-256 fingerprint over its fields, the column is unique in the table, and insertion is done with `ON CONFLICT (external_id) DO NOTHING`. The practical consequence: the mailbox can be reread as many times as you want, and the balance does not double. Without this rule, any financial ingestion pipeline ends up, by the third week, producing duplicates that someone cleans up manually.

What such an agent does not do, and why it is important to say this on the page: it does not approve a payment, does not make a credit decision, does not give investment recommendations and does not answer the client on behalf of the institution. The confidence score it returns is the model’s own assessment of itself, not a measurement of accuracy made by us — that distinction is the difference between a tool and an illusion of control.

What it covers

What changes concretely in financial services

The document goes in only once, even if we read it ten times

Notifications are read from the mailbox via IMAP, statements from PDF. Each transaction receives an external identifier from a SHA-256 fingerprint over its fields, the `external_id` column is `NOT NULL UNIQUE` in the table, and insertion is done with `ON CONFLICT (external_id) DO NOTHING`, with a separate log for the inserted row and for the one skipped as a duplicate. Rereading the same mailbox does not produce any duplicate.

Classification is done through tools, with a closed list of results

Seven tools, not free text: client payment, salary, tax, payment to creditor, operating expense, bank fee, unknown. The model must call one only. The first six require, in addition to name or category, a confidence score between 0.0 and 1.0, declared in the tool schema; the seventh requires a reason. The model used is declared in code, not hidden in configuration.

“Unknown” is a legitimate output, with reason

A classifier that is not allowed to say “I do not know” will misclassify, with high confidence, exactly the unusual transactions — that is, the ones that matter. That is why the unknown tool is available just like the others and requires a written reason, which ends up in the row. Unknown rows are the human’s work list, not a system failure.

Approximate matching of counterparty names, with its real state

The same company appears in extracts under three spellings. Above classification there is a cosine similarity match between vectors. We state exactly what those vectors are today: a character n-gram heuristic, written in code with the comment that for production it must be replaced with a real call to an embedding service. It works for spelling variations; it is not a semantic match.

Webhooks from payment processors go into the same table

Four automation flows, one for each of three payment processors and one for daily reconciliation, with 7 to 10 nodes each. The architecture idea is that different sources — email, PDF, webhook — do not produce three tables that must then be reconciled, but rows in the same table, with the same deduplication rule.

Model consumption stays under budget per key, not per invoice

Calls go through our own gateway at `api.megapromoting.com/v1`, with 44 configured models, each with a token cost and context limit. The project key has a model allowlist, budget and period, can be rotated while preserving history, and consumption is consolidated daily by user × key × model. The detail that surprises everyone: reasoning models produce internal steps that the tracking system cannot see, but the provider charges for them — so the gross budget per key is set below the desired ceiling.

What can run without the data leaving the machine

For steps that do not require a large model — extracting fields from a known format, name matching, consistency checks — no external provider is needed; these are ordinary code. The model is called only for the classification step. When your policy forbids data leaving, the discussion is what exactly is sent to the model, not whether AI is used: the transaction description can be sent without account identifiers.

What an agent of this kind does not do and will not do

It does not approve a payment and does not execute a transfer. It does not make the credit decision — the rules, thresholds and approval are the institution’s policy. It does not give investment advice and does not answer the client on your behalf without a separate approval path. And it does not guarantee accuracy: the confidence score is the model’s output about itself, not an independent measurement.

Traseul

How a request passes through the system.

01

We set the closed list of outputs before any code

The first delivery is not an agent, but a list: the categories it is allowed to classify into, what mandatory fields each requires, and what “unknown” means for you. If the list cannot be written on one page, the task is not suitable for an agent — and it is cheaper to find that out now.

02

We build ingestion and deduplication, without a model

Reading from email, PDF or webhook, field extraction, calculating the external identifier and writing to the database are done and checked without any model call. We deliver a pipeline that ingests correctly and does not duplicate. If this step is not solid, a model on top of it only produces more convincing errors.

03

We add classification and measure it on your cases

On top of the working pipeline we add the classification tools and run them on a set of your real transactions, with the correct result set by the person doing the work today. We deliver the table with agreements and disagreements, not a statement about accuracy. The threshold at which the row goes to a person is chosen from that table.

04

We set the budget, the log and the gates

Project key with model allowlist and budget, daily consolidation of consumption, log with the tool called and the score, and a gate that sends everything below the threshold or unknown to a person. We deliver the accesses, the deployment document and the written list of what the agent does on its own and what it does not.

1E-mail, PDF sauwebhook2the extraction of fields3SHA-256 fingerprint4scriere cu ONCONFLICT DO NOTHING5classification through onefrom seven tools, withscorthe rows“unknown” to the person
The path, in 6 steps

The data

What we touch, where it lives, and how long it stays

The rules differ from one industry to another. These are the ones that apply in financial services.

What kind of data it actually touches
Money movements, with date, amount, currency, description and counterparty. Some counterparties are natural persons — a salary bears the employee’s name — so the table contains employees’ personal data, not just commercial data. That changes who is allowed to open the table.
What reaches the model and what stays at home
The model receives the transaction description text and the amount. Account identifiers, IBANs and the rest of the fields have no place in the request for the classification to work, so they can be left out of it. The rule we apply generally: what is not necessary for that step is not sent, because what does not leave cannot be retained by anyone.
Where the extracted data is stored
PostgreSQL, with a unique `external_id` and an index on it. The database remains yours, on your infrastructure or on one we operate for you. There is no shared repository across clients, because there is no technical reason for it to exist.
The trace of the automated decision
Each row keeps which tool was called, with what score and, where applicable, the reason for ‘unknown’. It is the minimum needed so that six months later someone can answer the question ‘why was this transaction put into operating expenses’ — with an answer, not a shrug.
The retention period, which here you do not choose on your own
Financial documents have retention periods set by accounting and tax legislation, not by our preference or yours. In a financial project the period is taken from those rules, written into the processing register and only then is deletion implemented. The reverse order — implement first, check afterwards — produces systems that delete what should have been kept.

A case

An ingestion pipeline that can be restarted without fear

The situation

Bank notifications and monthly statements arrived in a mailbox and were read by a person, who categorised them. The obvious first version — a script that reads the mailbox and writes to the database — has a problem that only becomes visible in the third week: on any restart or re-read, the same transactions come in again.

What we built

I wrote five modules, 1,526 lines of Python: two IMAP notification parsers, a PDF statement extractor, the tool-based classifier and writing to PostgreSQL. The central rule is the external identifier — a 16-character prefix from a SHA-256 fingerprint over the transaction fields — with the unique column in the table and `ON CONFLICT DO NOTHING` insertion. Classification does not return text: the model calls one of seven tools, with a mandatory confidence score, and ‘unknown’ requires a reason.

What came out

The mailbox can be re-read at any time, and rows already processed are skipped and noted as duplicate in the log. Transactions the model cannot classify appear as a work list, with the reason next to them, instead of being pushed into a category so that it looks like everything worked.

What the case does not say

It is an internal pipeline, not a system delivered to a financial client: it is not under git, the test directories are empty, and approximate name matching uses an n-gram heuristic that the code explicitly marks as provisional. I described it here because it shows the method, not because it is a product.

Questions

What someone in financial services asks

Does the agent approve payments or loans?

No, and it is not a technical limitation we will overcome in the next version. Approval is a decision with legal consequences, which belongs to an authorised person in the institution. The agent prepares: it classifies, completes, flags what does not fit. A system that approves on its own should be able to answer to an auditor, and a confidence score is not an answer.

How do I know it classified correctly?

Not from the confidence score — that is the model’s assessment of itself, not a measurement. It is known from the comparison with the decisions of the person who does the work today, on a set of real transactions, made before anything is switched on in automatic mode. The result of that comparison is a table we deliver, including the rows where the agent was wrong.

What happens when it is not sure?

It calls the unknown tool and writes the reason. The row goes into the human work list, not into a randomly chosen category just to make it seem as if the system worked. A classifier without a “I do not know” output fails exactly where failure is costly: on unusual transactions.

If we read the same notification twice, does the amount double?

No. Each transaction has an external identifier calculated as a fingerprint over its fields, the column is unique, and insertion uses `ON CONFLICT (external_id) DO NOTHING`. The skipped row is noted in the log as a duplicate, so it is visible that it was read again.

Do our data go to a model provider?

For the classification step, yes: the transaction description and amount go to the chosen model, through our gateway, with a project key, a whitelist of models and a budget. The rest of the steps — reading, extraction, deduplication, name matching — are ordinary code and do not leave anywhere. What is allowed to go out is set in writing in advance, not discovered from logs afterwards.

Can you read PDF statements, not just e-mail notifications?

Yes, it is one of the pipeline modules. What must be understood is that a PDF statement is a fragile format: it is read according to the structure produced by that bank, and when the bank changes the template, the module must be adjusted. That is why it is written with checks that fail loudly, not that guess.

Why does this page say “proposal” and not “delivered”?

Because our rule requires at least two in-house implementations before we write “delivered”, and the pipeline described here runs on our data, not in production at a client in the financial sector. It also has limitations we name: it is not under git, it has no tests, and the name matching uses a heuristic marked in code as provisional. When we can cite two client implementations, we will change the word.

What do we need from our side for the work to start?

Three things: the closed list of categories it is allowed to classify into, a set of real transactions with the correct answer given by the person who does the work today, and the written decision on which fields are allowed to leave your network. Without the third point we do not start, because it is the only one that cannot be fixed later.

What would you want to work better?

Tell us about your process. Together we decide what is worth building, what we can connect, and how we check the result.

Let's talk