Skip to content
d3n.blog
Go back

Paperless-ngx Run by AI Agents: My Invoices Get Read, Filed and Matched to My Bank

A self-hosted Paperless-ngx where a vision model reads every page and AI agents file documents, attach receipts to bank transactions and answer questions, plus a 12-model benchmark.

Lire cet article en français

Two-ink cartoon: in the middle, two small robots hold up a big sign with the Paperless-ngx logo above an open filing cabinet; on the left I smile at my desk in front of my laptop, a coffee and a small scanner beside me with a sheet going through, while documents, an email and invoices fly in; around them, other robots read a page through a magnifying glass, stamp documents and clip a receipt to a bank card

An invoice shows up: a PDF in an email, a photo of a receipt, now and then a sheet of paper I feed to the scanner. A minute later a vision model has read the supplier, the date and the amount. Then an agent finds the matching transaction on my company’s bank account, shows me the match, and once I say yes it attaches the invoice to that transaction and links it to its entry in my books. I didn’t rename a file, open the banking app or type an amount.

My archive holds 890 documents: payslips, birth and marriage certificates, medical reports, contracts, expense receipts, company invoices. I no longer browse it. One agent files what comes in, another answers questions about what’s already there, and each one can touch only what it needs.

Nobody files anything, on purpose

Paperless-ngx is a self-hosted document archive: it extracts the text from PDFs and images and keeps them searchable. Mine runs in my homelab, reachable only over Tailscale, with no port open to the internet.

Overview: four ways in (the scanner, which tries a scan every ten seconds, a dropped folder, an agent uploading over the API, a phone photo of a receipt) lead into Paperless-ngx, where Tesseract OCR makes the document searchable in seconds and a vision model then re-reads it one page per call and files it in the archive; from there, three main uses (questions to a read-only agent, the bank and the books where each receipt is attached and each entry linked to its record, manuals read by the homelab's agents) and, dashed, me in the browser, rarely

The first thing I did was turn off Paperless’s built-in classifier. It learns from documents you have already filed, which works for a supplier you’ve seen fifty times. Faced with an issuer it has never seen, it guesses, and nothing takes the guess back. A personal archive is full of one-off issuers: a town hall, a notary, a doctor’s office.

A vision model does the filing instead. It re-reads each document a minute after it arrives, page by page, and picks the type, the tags and the issuer from the lists that already exist. The agents never use the web interface: they talk to Paperless through its API or an MCP server, with permissions cut down to what each one needs.

Four ways in, none of them involves sorting

The scanner: trying to scan is the paper sensor

My scanner, a Brother DS-640, is plugged into the server over USB. It has no paper sensor I can read and no status button, so the scan attempt itself is the sensor.

A shell script, lab-autofeed (the entry point of the scanservjs container), runs scanimage every ten seconds, in color at 300 dpi, with --AutoDocumentSize, --AutoDeskew and --SkipBlankPage:

  • Empty feeder: the SANE driver answers “out of documents” and the script waits ten seconds.
  • A sheet in the feeder: the scan goes to a temporary ._scan_…pdf, which Paperless ignores, then gets renamed once it’s complete, so a half-written file is never ingested.
  • Scanner held by the web UI: the driver answers Device busy and the script waits fifteen seconds. Paper jams are written to the log.
  • USB sleep: when the scanner drops off the bus and comes back under a new number, the script finds it again and asks scanimage -L for its new name.

A file named autofeed.pause stops the loop. The one physical catch: after a power cut, someone has to wake the scanner by hand. Day to day, I slide a sheet in, it gets pulled through within ten seconds, and it lands in the archive.

The phone: the Android app over a private network

For a receipt or a letter I get while I’m out, the Paperless Android app connects over Tailscale with an API token. When a receipt has no printed date, the vision script takes the moment the photo was shot from its EXIF data (exif_capture_date).

A dropped folder, and the agents’ API

For a batch of old files, I drop the whole directory tree into the folder Paperless watches: every subfolder becomes a tag, and duplicates are refused. For company paperwork, the agents upload straight through the REST API.

Two reads: a fast index, then a faithful one

Tesseract, so search works right away

As soon as a document arrives, Tesseract extracts the raw text in French, English and Arabic, and the document is searchable within seconds. But Tesseract doesn’t understand structure. On handwriting, a crumpled receipt or a poor scan it produces noise: good enough to find a keyword, not good enough to pull out fields you can trust.

The vision model, in the background, one page at a time

When the Paperless container starts, /custom-cont-init.d/50-vision.sh launches a loop in the background as the paperless user:

while :; do python3 /opt/vision/paperless-vision.py --pending --apply; sleep 60; done

The script is 894 lines of Python and works directly through Paperless’s Django ORM, not the REST API. --pending picks the documents that have no vision custom field yet, or that were read under an older version of the instructions. It reads four pages in parallel and one document at a time.

The rule that matters most fits on one line: one page per call, never a whole document. On a 28-page bank statement, sending the whole file made the model condense: it returned 43 characters per page, against 2,568 for Tesseract. It summarized without saying so, to cope with the load.

A 28-page bank statement sent in one call comes back as 43 characters per page, a summary nobody announced; sent one page per call, 28 calls with 4 pages in parallel return the full text, true to the layout, then one more call reads page 1 and the transcript to pick the type, issuer, tags and fields; a note says Tesseract read 2,568 characters per page on the same statement

So the script splits the PDF: 28 calls, four at a time, each transcribing one page with google/gemini-3.1-flash-lite through OpenRouter. It’s also the model behind Paperless’s own AI features, where gemini-2.5-flash-lite used to hang on one of the calls.

The transcription prompt runs at temperature 0, asks for strict JSON ({"texte": "..."}), and is written in French because the archive is. It carries four instructions:

  1. Everything, faithfully: render all the readable content of the page in French, in full, not a summary. A page in another language is translated.
  2. Data copied, never rewritten: numbers, dates, amounts, IDs, references and proper names are reproduced exactly, neither translated nor reworded. Nothing is invented, and [illisible] marks what can’t be read.
  3. The page’s own layout: running text stays running text, a table stays a table, a form keeps its label-value pairs. Never join what the page keeps apart, never split what it keeps together, and leave anything ambiguous on its own. On a blood test with 49 values, without this clause none of the numbers kept their line neighbors across 20 runs; with it, 68% did.
  4. Blank pages: answer exactly {"texte": "[page vierge]"} for a page with no text, and never return empty text for a page with ink on it.

Blank-page detection also relies on counting non-white pixels:

  • Under 0.1% ink, the page is declared blank without any API call.
  • Over 2% ink, a “blank” answer from the model is rejected and the page is read again.
  • After three “blank” answers in a row on an inked page, the script accepts it: a photo, a stamp, a background with no text.

A second prompt gets the first page and the full transcript and fills in the type, the issuer, the person the document is about and the date it was issued. It picks from the existing lists or returns null; choosing the nearest match is forbidden. A field I’ve edited by hand in the UI is never overwritten again. The script gives up on a document after three failures, and stops for the day once spending passes $10.

The archive became a database

Paperless lets you attach custom fields to documents. I have sixteen, and eight of them carry most of the automation:

FieldWhat it holds
montantthe total of an invoice, a receipt, a payslip, a statement (614 documents)
referenceinvoice, contract or case number, copied verbatim (709)
echeance, periodewhat the issue date doesn’t say: valid until when, for which month (170 due dates)
signe, cachetwhether a signature or an official stamp is on the page, as the vision model sees it
qualitepages worth scanning again
ocr_ecartnumbers Tesseract saw that are missing from the vision model’s transcript

The field names are French because I am. montant is the amount, echeance the due date, signe “signed”.

ocr_ecart is the safety net I rely on most. A model’s worst failure isn’t an obvious hallucination; it’s a correct number on the wrong line. The script collects the numbers Tesseract saw and checks them against the vision transcript: when an amount the OCR caught is missing from the model’s text, the gap goes into ocr_ecart and the document is flagged for a look.

The paper trail of my company

I work as a freelancer through my own one-person company. A dedicated Git repository holds the agent’s instructions and scripts. Everything I receive (invoices, receipts, contracts, screenshots of emails) goes unsorted into an inbox folder, 00_A_TRAITER/ (“to process”).

The paper trail: I drop everything unsorted into the inbox; the agent looks at every file; a document (invoice, receipt, contract) is filed in Paperless and then linked to the ledger; a screenshot is information rather than a record, so it updates the project notes and nothing is filed; a duplicate already in Paperless goes to the rejected folder with the reason; an unclear file, unreadable or unrelated, becomes a question for me; every processed file moves to a folder for its month and leaves a line in the journal

The agent decides what each file is before doing anything:

  • A record worth keeping goes into Paperless.
  • A screenshot updates the project’s notes and isn’t archived.
  • A duplicate goes to 02_REJETE/ (“already in Paperless, doc 355”).
  • Anything unclear turns into a question for me.

Each processed file moves to 01_TRAITE/<month>/ and gets a line in a journal once Paperless has confirmed it stored it.

From a document to a receipt on the bank transaction, over MCP

An invoice's path: as a PDF, a photo or a scan, it enters Paperless, which makes it searchable, then the vision model, through OpenRouter, reads the supplier, date and amount; the agent lists Qonto transactions missing a receipt over MCP and matches them (same amount, within ten days, supplier named in the document); with my OK on the whole plan, it attaches the receipt to the transaction over MCP, sets the category from my written rules, and the Beancount entry is linked to the record, one click away in Fava; a note says sending a transfer, paying or deleting is mine alone, in the bank's app

The company banks with Qonto, a European business bank that ships an official MCP server. The agent lists the transactions that still lack a receipt and matches each document on three criteria: the same amount, a date within ten days, and the supplier’s name appearing in the document. That last one matters more than it looks. Without it, a €180 invoice would happily attach itself to any unrelated €180 direct debit.

The agent shows me its full plan and waits for my OK. Once I give it, it asks Qonto for a temporary upload URL (valid fifteen minutes), uploads the file, attaches it to the transaction and sets the accounting category from rules I wrote. The agent is not allowed to send a transfer, pay a supplier or delete anything. That’s a written instruction, not a technical lock, and those actions stay mine, in the banking app, where the agent only hands me a direct link.

Paperless as the backbone of the Beancount books

Paperless is the single source for every record. The 113 invoices that only existed in Qonto were pulled back into it. Each entry in my Beancount ledger carries the Paperless link to its record (199 entries linked), one click away in Fava, Beancount’s web interface.

The monthly check lists the transactions that have no record. Expense reports become mechanical: of 143 restaurant receipts, the ones for two people or more go to business meals, a receipt for one is my own meal as the company’s director. Mileage allowances are computed from the logged trips and the government’s per-kilometer rate. The expense report is a query over records that were already checked.

Personal documents: asking instead of digging

For my personal papers, the agent combines custom fields rather than running a plain full-text search:

  • “Is my copy of the lease signed?” It filters on the bail (lease) tag, checks signe, and links the signed copy without opening a PDF.
  • “What expires in the next three months?” A query on echeance lists insurance, warranties and subscriptions.
  • “How much did I pay for electricity this year, month by month?” It sums montant per month into a table and warns me if ocr_ecart flagged a mismatch.
  • “Put together a rental application: last three payslips, last tax notice and an ID.” It picks the documents and merges them into one PDF with Gotenberg, which already runs alongside Paperless.
  • “Find my last blood test.” The table keeps its alignment, so the values can be compared year to year.

The homelab benefits too: the UPS’s manual, its datasheet and its declaration of conformity are tagged homelab in Paperless, and that’s where the maintenance agents go to read them.

Who may touch what

An agent that handles ID documents and tax returns needs clear walls around it.

Who can do what: the vision model, every minute, can read every page and fill in the title, type and fields, but cannot create a tag or an issuer or touch a field I corrected; the search agent, over MCP, can search, read, count and answer with links, but cannot write anything because its token is read-only; the filing agent, over the API, can upload a document and edit one with my OK, but cannot delete my documents or administer Paperless; a note says "with my OK" is a written instruction to the agent while "cannot" is enforced in code

The agents have two dedicated accounts, neither of them an admin. The question-answering agent goes through the MCP server (paperless-mcp) with a strictly read-only token: Paperless refuses every write. The filing agent can create and edit documents but not delete them. The ban on deleting is a Paperless permission; asking me before an edit is a written instruction.

One trap worth knowing: a document that isn’t shared with an account is invisible to it, and Paperless returns no 403. The agent just sees fewer results and can get a total wrong without noticing. So an automation rule shares every new document with the agents’ accounts the moment it arrives.

Twelve models on the same pages: accuracy and real cost

To check my choice, I put google/gemini-3.1-flash-lite up against eleven other models on OpenRouter, with the same instructions, one page per call. The test set was eleven pages from the archive (receipts, invoices, datasheets): 132 calls for $0.86.

Accuracy is measured on numbers. A number that at least half the models read is treated as correct, each model is scored on how many of these consensus numbers it recovers, and a number only one model saw counts as invented.

ModelNumbers recoveredInventedCost per 1,000 pagesSeconds per page
google/gemini-3-flash-preview99.1%1$3.063.4
google/gemini-3.1-pro-preview98.6%0$11.197.0
anthropic/claude-sonnet-5.598.2%0$14.814.9
google/gemini-3.1-flash-lite (my pick)97.3%3$1.502.7
anthropic/claude-haiku-5.597.3%2$1.396.2
openai/gpt-6-luna-2026092296.4%5$1.0510.1
qwen/qwen3-vl-235b-a22b-instruct95.9%3$1.499.1
qwen/qwen3-vl-30b-a3b-instruct92.8%4$0.9511.2
google/gemini-2.5-flash-lite92.3%7$0.562.0
google/gemini-3.5-flash-lite91.0%3$2.082.0
google/gemini-3.5-flash90.5%2$39.8014.7
mistralai/mistral-small-3.2-24b-instruct77.9%6$0.5010.9

Scatter plot of the benchmark: cost per 1,000 pages on a log scale from $0.50 to $40 across, share of numbers recovered up the side; gemini-3.1-flash-lite, my pick, in orange at 97.3% for $1.50, in the tight cluster of models around one dollar with claude-haiku-5.5, gpt-6-luna and qwen3-vl-235b; at the top, gemini-3-flash-preview reaches 99.1% for $3, gemini-3.1-pro and claude-sonnet-5.5 pass 98% for $11 to $15; gemini-3.5-flash costs almost $40 for 90.5%, and mistral-small-3.2, the cheapest, drops to 77.9%

The top of the table is tight: between gemini-3.1-flash-lite at $1.50 per 1,000 pages and Claude Sonnet at $14.81, accuracy moves by about one point for ten times the price. Newer doesn’t mean better either: gemini-3.5-flash-lite does worse than 3.1, and gemini-3.5-flash is both the most expensive model in the set and one of the least accurate.

The worst failures weren’t misread numbers; they were generation loops. On the UPS manual, mistral-small filled its answer with tabs until it hit the 8,000-token limit, without returning a single number. On the second page of an Amazon invoice, almost blank, gemini-3.5-flash produced line break after line break, and billed me for them. That’s why my script caps the length of an answer and retries truncated pages.

On this set, gemini-3-flash-preview comes out first: 99.1% for $3 per 1,000 pages, an extra cost that barely registers for a personal archive. Eleven pages is a small set, with no handwriting and no hard photos, so I read it as a signal rather than a final ranking.

What it costs to run, and where my data goes

In real use, counting retries, blank pages and the filing call, reading the whole archive (890 documents, 2,318 pages) costs about $5, a little over $2 per 1,000 A4 pages. The $10 daily cap keeps a runaway loop from billing all night.

The trade-off is explicit: every page leaves my network as an image, ID documents included, to be read by OpenRouter and the model behind it. The files, the PostgreSQL database and the indexes stay home, but the reading depends on an outside API.

To keep a document out of that flow, I set its vision field by hand before the script gets to it, and the script treats it as already read. I haven’t yet run a local vision model through the same benchmark; until I do, the remote API is my compromise.

From filing cabinet to something I can ask

Manual filing is gone: I drop a document in, and the archive answers questions with links back to the original records.

A human still needs to look at the money. A number on the wrong line looks just as plausible as the right one. The next parts of this chain, the bank side with Qonto’s MCP server and the Beancount books, will get their own posts. The monthly activity report already has one: Activity Reports for Freelancers, Filled In by an AI Agent over MCP.

Cover illustration generated with Gemini 3.1 Flash Image, through OpenRouter; the Paperless-ngx logo belongs to the project.



Related posts

Follow new posts by RSS