Published 2026-06-20

Obchodnirejstrik-ai: When a Better AI Model Changed the Architecture, Not Just the Prompt

An article on developing a SaaS platform for analyzing companies from public registries in the Czech Republic: from an OCR intermediary layer to working directly with PDFs, structured data, deterministic checks, and operational reliability.

I’m building obchodnirejstrik-ai as a tool for vetting a Czech company. The input is a company ID or an annual report in PDF, the output a financial, risk, and valuation report. On paper it’s simple. In reality the financial statements are scattered across the Collection of Deeds and other registries, they often have several years of data in a single document, and some of the important information isn’t in the financial statements at all.

This version of the project is already the second architecture. It’s not a frontend rewrite because of a new framework. What changed was the very assumption on which the first pipeline rested: what the system has to do before the model can reasonably work over a document.

The problem wasn’t “read the PDF”

The first version began with a separate OCR step. The PDF went into Mistral OCR, out came text and tables in Markdown, and only then did the model extract financial data into a structured output. At the time this was a reasonable compromise: annual reports are often scans, the tables are complex, and merely pulling the PDF’s text layer isn’t enough.

After OCR came looking up data from the registries, analysis by the model, risk computation, and report generation. The application had React/Vite and FastAPI, long tasks ran through Celery and Redis, and Supabase handled the data backbone, authentication, and files.

The biggest problem wasn’t one extra service. It was the whole boundary between the document and the model: another external call, more billing, another type of error, and another transformation whose quality had to be watched. The OCR layer was an infrastructural necessity, not a product advantage.

The turning point: the model took over the OCR layer too

Current multimodal models can process a PDF as a file directly in the API: alongside the text layer they also work with the visual form of the pages. That shifted the decision from “improve OCR” to “simplify the whole path.”

In the current backend the PDF is uploaded via the OpenAI Files API and goes into the Responses API as an input_file together with a precise instruction. The model returns JSON, not a finished document. I treat the specific model in the configuration as a swappable detail — what matters is the interface, the evaluation samples, and the output contract, not a marketing claim about one “latest” model.

This removed one network dependency, a custom intermediate format, and part of the latency. It doesn’t mean the model understands every statement flawlessly. It means a single system bears responsibility for understanding the document and that I can test quality through the same structured interface instead of tuning two independent services.

The pipeline today: data first, text afterward

The rebuild ended up as a monorepo: Next.js on the web, FastAPI in the API, PostgreSQL via SQLAlchemy and Alembic for reproducible migrations, Redis and ARQ for background work. The analysis doesn’t run in the HTTP request; the API creates the report, validates the input, and hands the work to a worker.

A typical deeper analysis looks like this:

  1. The user enters a company ID or uploads a PDF. If they only have a company ID, the application tries to download the relevant financial statements from the Collection of Deeds.
  2. The worker fills in company details and multi-year financial values. For a single year it may compare multiple filings: a newer document often has a more complete current column, while an older filing fills in missing cash flow or EBITDA.
  3. The PDF is processed directly by the model into structured financial data and facts from the document. The output is parsed and stored according to a canonical schema, not as free-form text.
  4. In parallel, context comes in from ARES, justice.cz, ADIS, eISIR, sanctions lists, and other public sources; depending on the report type, also ownership links, people in management, or public procurement.
  5. Python computes the valuation using EV/EBITDA multiples. The multiples are sector-sensitive, not one global coefficient for all companies.
  6. Only on top of that do the individual parts of the report, the sources, and a clear summary arise. The result can be displayed, shared via a read-only link, or exported to PDF.

This order is deliberate. I don’t want the model to first write a convincing story and then have the system look for something to back it up with. First the data foundation and the evidence are created; the narrative is only their interpretation.

Why “analyze the company” isn’t enough

The report doesn’t arise from a single free-form prompt. The model extracts data into a canonical schema and interprets verified data, but it has no authority to create financial facts or valuation assumptions. The application validates the extracted data and deterministically governs the calculations, risk flags, charts, section order, rendering, and source evidence.

The output is a fixed set of sections rendered from validated data, so it can be recreated and checked. The valuation uses centrally managed, sector-sensitive multiples and benchmarks with deterministic usage rules and guardrails. The model can neither guess them nor change them.

Where the model ends and the software begins

The most important architectural decision is the trust boundary. The model is good at reading non-uniform documents, extraction, and explaining connections. It isn’t a source of truth for things that must be reproducible or that have legal and financial impact.

That’s why I keep in code:

  • the data schema and response validation;
  • the calculations of financial ratios, valuation, and benchmarks;
  • hard risk flags, for example an active insolvency or an unreliable VAT payer;
  • the order and rendering of the report, the charts, and the record of sources;
  • access rules, credits, and an audit of model consumption per individual report.

The model can describe why a drop in margin is interesting. But it shouldn’t decide on its own whether a company meets the rule for a risk flag, nor “invent” a number for the valuation. This decomposition also greatly simplifies debugging: a wrong value is a problem of extraction or input, a wrong calculation a problem of financial logic, and a wrong display a problem of the renderer. I’m not dealing with one vague model output.

What it means to operate an AI feature

The hardest parts weren’t the prompts. It was the operational discipline around expensive and slow operations.

Each report goes through an ARQ queue instead of holding an HTTP request open for several minutes. The worker saves state and progress of the work, retries transient failures, and on a repeated run can reuse already-created report sections. When there’s a problem with one public registry, the application prefers to mark the source as unavailable rather than cancel the whole paid report — but a critical lack of input data can stop the analysis before money is spent on generation.

The AI cost isn’t just a plain line item in the provider’s dashboard. When calling the Responses API I measure the input and output tokens for a specific report. The credit system writes operations into a ledger; deduction, refunding credit on failure, and checking the balance are explicit workflows, not a side effect of a single request.

For observability I use a self-hosted service compatible with Sentry (Bugsink). It captures API errors, permanently failed jobs, and worker health checks without the error data having to leave the VPS. For a small product this is a pragmatic compromise: a smaller vendor footprint at the cost of infrastructure operation and updates being my responsibility.

What I’d do the same way today

The rebuild confirmed two things for me. First: when a model’s capability changes fundamentally, it’s sometimes more correct to rethink the system boundary than to patch the original pipeline. Mistral OCR wasn’t a mistake; it was the right step for the model capabilities of the time. It just stopped being the best architecture.

Second: a trustworthy AI application isn’t created by giving the model more freedom. It’s created by giving it precisely delimited work, a verifiable data contract, and software around it that can handle provider errors, incomplete data, and the cost of each run. That’s the part of obchodnirejstrik-ai that interests me most — not the model call itself, but the boundaries that turn it into a usable product.