PDF invoice in, valid German e-invoice (XRechnung) out. NVIDIA Nemotron proposes, the official KoSIT validator decides, and every value has to be on the page or computed.
Live demo: https://verdict-678224659019.europe-west1.run.app
Built for the Nebius × NVIDIA Global AI Hackathon (Best Apps and Agents). Runs on Nebius Token Factory.
Germany is moving B2B invoicing from PDF to structured e-invoices (EN 16931). Every business has to be able to receive them since January 2025, and has to send them from 2027 or 2028 depending on size. Public-sector buyers already require XRechnung today. Most small businesses still produce PDFs.
The obvious shortcut is to let a language model read the PDF and write the XML. For an invoice, "probably correct" is not good enough: a model that guesses a missing IBAN, copies the buyer's city into the seller's address, or adds up a total itself produces a file that may even pass validation and still be wrong.
Drop a PDF. Every run ends in exactly one of three stamps:
| Stamp | Meaning |
|---|---|
| Verified | Every field is quoted from the page or computed. The official KoSIT validator accepted the XRechnung. Download it as XML, as a ZUGFeRD PDF/A-3 with the same XML embedded, and with a verifiable audit log. |
| Returned | A fact the XRechnung needs is not on the document (for example the Leitweg-ID or the IBAN). Verdict asks for exactly that fact instead of inventing it, then validates again. |
| Rejected | The validator says no and nothing on the page can fix it. Verdict explains the rule in plain English and makes nothing up. |
The demo has a gallery of nine recorded runs on synthetic invoices that replay without spending model budget, plus live upload of your own text-based PDF.
flowchart TD
pdf([PDF invoice, text layer]) --> extract["<b>Nemotron 3 Super</b><br/>reads the page and quotes every field"]
extract --> checks
subgraph core ["Deterministic core: no model, no guessing"]
checks["<b>Check</b><br/>quote is on the page · normalize codes<br/>shape and role rules · no borrowed lines"]
compute["<b>Compute</b><br/>amounts with decimal math · line cross-check<br/>deterministic CII builder"]
checks --> compute
end
compute --> kosit{{"<b>Official KoSIT validator</b><br/>XRechnung 3.0.2"}}
kosit -- accepted --> verified(["<b>VERIFIED</b><br/>XRechnung + audit log"])
kosit -- rule violations --> repair["<b>Nemotron 3 Ultra</b><br/>proposes patches with quotes"]
repair --> guard["<b>Patch guard</b><br/>drops patches without proof"]
guard -- "patched, max 3 rounds" --> compute
guard -- "fact not on the page" --> returned(["<b>RETURNED</b><br/>asks the user for that fact"])
guard -- "nothing on the page can fix it" --> rejected(["<b>REJECTED</b><br/>rule explained by Nemotron + Tavily"])
returned -. "user answers" .-> compute
classDef model fill:#e8efe9,stroke:#2f5d4a,color:#1c1f22
classDef judge fill:#fbfbf8,stroke:#1c1f22,stroke-width:2px,color:#1c1f22
classDef ok fill:#e7e4f6,stroke:#4536a8,stroke-width:2px,color:#4536a8
classDef ask fill:#f7eec4,stroke:#5a4708,stroke-width:2px,color:#5a4708
classDef no fill:#f6e3df,stroke:#c2382b,stroke-width:2px,color:#c2382b
class extract,repair model
class kosit judge
class verified ok
class returned ask
class rejected no
style core fill:#f2f3ee,stroke:#b6b9ae,color:#1c1f22
Every step is appended to a SHA-256 hash chain (RFC 8785 canonical JSON). Model events record the model ID, prompt hash, tokens and cost, never invoice contents. pnpm audit:verify detects any change to the log or the output file.
| Step | Model | How it is used |
|---|---|---|
| Extraction | nvidia/nemotron-3-super-120b-a12b |
JSON-schema constrained decoding, reasoning off, one quote per field and one row quote per line item |
| Repair | nvidia/Nemotron-3-Ultra-550b-a55b |
Maps validator rule violations to evidence-backed JSON patches, or marks them as not solvable from the document |
| Explanations | nvidia/nemotron-3-super-120b-a12b |
Explains a validator rule outside the local table in plain English, using Tavily search results as sources; cached per rule |
All calls go through one client that counts tokens, prices every call from pinned prices, and enforces a daily budget. A typical invoice costs about 0.3 to 0.4 US cents in model calls and takes about 7 seconds.
We tested the smaller NVIDIA-Nemotron-3-Nano-30B-A3B for explanations first. It confused the payee with the buyer, so explanations use Super.
Ground truth comes from the public KoSIT XRechnung 3.0.2 test suite (19 invoices inside the MVP scope; synthetic data, placeholders filled with invented names and addresses). Each invoice is rendered as a PDF in several layouts; the corpus build is byte-reproducible and checked in CI.
- clean: three regular layouts
- noisy: shuffled blocks, plain number format, ISO dates
- mutated: 14 document-level defects (missing Leitweg-ID, IBAN, contact data, VAT IDs or electronic addresses, unit words instead of codes, € instead of EUR, wrong printed totals, VAT exemption without a reason, …) with the outcome a correct pipeline must reach
- holdout: four invoices kept out of all development until a single run on 2026-10-05
| Set | Documents | Valid XRechnung | Outcome as expected | Field accuracy | Invented fields |
|---|---|---|---|---|---|
| clean | 45 | 42 | 42 | 99.1 % | 7 |
| noisy | 30 | 25 | 25 | 99.1 % | 3 |
| mutated | 28 | 8 | 27 | 99.2 % | 5 |
| holdout clean | 12 | 8 | 8 | 98.0 % | 8 |
| holdout noisy | 8 | 6 | 6 | 96.7 % | 2 |
How to read it
- No valid XRechnung with a wrong amount in any set, holdout included: in every document Verdict output as valid, the grand total and amount due match the ground truth. This is the failure that matters most, because the validator cannot catch it.
- Documents that are not valid ended as Returned: Verdict asked for facts it could not find, mostly the seller contact block in the compact layout. A missed fact costs the user one question; it never becomes an invented value. In the mutated set, the expected outcome is often Returned or Rejected on purpose, so few documents end valid there.
- Invented fields counts values that are not in the ground truth at all, including in documents that were returned. Inside valid outputs only two kinds remain: the VAT exemption label kept as reason text next to its code (for category O the rule itself asks for the text "not subject to VAT"), and an account holder name equal to the seller name. They are counted, not hidden.
- The holdout rows are reported exactly as measured on commit
2555b8d. The holdout showed field-level mistakes in three valid outputs (a scheme note left in an electronic address, a country code read as an address line); those and four more guards (VAT ID read as seller identifier, party name or e-mail address used as buyer reference, a country used as city, the other party's address block used during repair) were added afterwards and are measured on the development sets only. The holdout was not rerun. - LLM output varies between runs; single documents flip between Verified and Returned from run to run. Full reports:
docs/benchmark/.
Run it yourself: pnpm eval --set clean|noisy|mutated --pipeline agent (the holdout is locked behind EVAL_ALLOW_HOLDOUT=1).
- Text-based PDFs only. Token Factory offers no Nemotron vision model, so scanned invoices are refused with a clear message.
- Commercial invoices (type 380) in EUR, payment by SEPA credit transfer or none. No document-level allowances or charges, prepayments or rounding amounts.
- Output is XRechnung (UN/CEFACT CII) and, on request, ZUGFeRD 2 (profile XRECHNUNG): a PDF/A-3 rendered from the validated XML with that XML embedded byte for byte. The ZUGFeRD file is built with Mustang only after KoSIT accepts the XML, and Mustang validates the result before it is handed out. The visual PDF is a standard rendering of the data, not a copy of the original layout.
- The demo is a demo: one instance, a per-IP rate limit and a daily model budget. When the budget is used up, the gallery keeps working.
- Nothing is stored. The PDF lives in memory for the duration of the request; results and the audit log go back to the browser.
- The validator runs in the same container on localhost only, protected by a per-boot secret.
- Secrets come from the environment (Secret Manager in production). gitleaks runs in CI and as a pre-commit hook.
- Test data is synthetic. No real invoices, no personal data.
apps/web Next.js app: upload, live run stream (SSE), routing slip, gallery
packages/core semantic model (zod), evidence check, normalization, derivation,
CII builder, patch guard, agent loop, explanations, audit chain
services/verifier Java 21 service around the KoSIT validator 1.6.3 and Mustang 2.26.0 for ZUGFeRD
(both pinned, checksum-verified)
eval/ corpus builder, mutations, benchmark harness
deploy/ one-container image (app + validator) and the Cloud Run deploy script
docs/ PRD and SRS (German), benchmark reports
Requirements: Node 22+, pnpm, Docker. No local Java needed; the validator builds in Docker.
cp .env.example .env # add NEBIUS_API_KEY and a random VERIFIER_SECRET (16+ chars)
pnpm install
docker compose up -d --build verifier
pnpm testsuite:fetch # pinned KoSIT XRechnung test suite, checksum-verified
pnpm verifier:check # all CII suite instances valid, mutations rejected with rule IDs
pnpm corpus:build && pnpm zugferd:check # ZUGFeRD files pass Mustang, a plain PDF does not
pnpm --filter @verdict/web devTests: pnpm typecheck && pnpm test. Deploy: deploy/cloud-run.sh (one Cloud Run service; see the script header).
Apache License 2.0. Third-party components (KoSIT Validator, KoSIT XRechnung configuration and test suite, Mustangproject) are used under their own licenses.