Every time I receive an invoice or a deposit instruction from my broker, I do the same stupid thing: I manually copy the account number, amount, and reference into my banking app. One transposed digit and the money flies to a stranger. Czech invoices increasingly carry a QR payment code you can just scan — but far from all of them do. So one afternoon I decided to fix that.
The result is qr.cz-agents.dev — a free European payment QR code generator (Czech SPAYD and European EPC/GiroCode), and the same logic exposed as an MCP server @czagents/payqr — an interface that AI agents (like Claude) understand and can call. It's the latest piece of cz-agents — a suite of MCP servers over Czech public data (ARES, ČNB, sanctions, insolvency) that I wrote about before. This is an honest case study: what was easy, what surprised me, and one lesson I didn't see coming.
What was easy
Payment formats are just strings. SPAYD is SPD*1.0*ACC:CZ65…*AM:1250.00*CC:CZK*X-VS:…, EPC/GiroCode is twelve lines defined by the EPC069-12 standard. IBAN validation is a ten-line trick (mod-97). Generation is therefore deterministic — no AI, no external API, no rounding surprises. The entire web application runs in the browser: OCR and QR reading (tesseract.js and jsQR) process the image locally, nothing is sent anywhere. For payment details, that's fundamental — the promise "your image never leaves your device" is something I could write only because it's literally true (there is no server-side component).
What surprised me (the lesson)
I exposed the same logic as an MCP server, so that an AI agent (like Claude) could take an invoice, read the payment details from it, and generate a payment QR. And that's where a full day of iteration began.
An MCP server can return an image. But it turned out that Claude Desktop does not reliably render images returned by a tool as a call result, and the model also can't transfer a multi-kilobyte base64 string to a file without corrupting it along the way. The result: instead of a QR code, just a broken image — a few meaningless pixels in the chat. I tried debugging it with prompt instructions — pointlessly, because the problem wasn't carelessness but a principle: language models aren't built to transcribe long random strings without error, so somewhere in 2 KB of tokens they will eventually make a mistake.
The right solution wasn't "teach the model to be more careful" — it was removing the model from the critical path:
- The server self-verifies its own QR — it re-decodes the generated PNG and compares it against the original data (
self_verified). The model no longer verifies anything. - The image is offered as a short, temporary URL — not base64 to be transcribed. A URL can't be garbled.
- The shared link on the web is opaque (
?p=<id>) — no payment data in the URL, so nothing leaks into history or logs, and the link can't be tampered with.
The lesson I'm taking away: for a tool where a single mistake means money sent to the wrong place, verification belongs on the server and in the deterministic layer, not in the instructions to the model.
And it's not about QR. It's a general pattern for AI tools: the model proposes, the deterministic layer verifies and owns responsibility for correctness. The more expensive the mistake, the more decision-making belongs outside the model — in code that fails predictably.
Does it work?
A practical test: I sent an actual deposit via a QR generated by this tool. The bank read the code, pre-filled the payment, and the recipient (my broker) confirmed that the money arrived in the correct account. Not a demo — real money, where it was supposed to go.
Where it lives
The tool runs at qr.cz-agents.dev — IBAN or a screenshot, everything in the browser, free and without an account. The code is open-source (MIT) on GitHub, as part of the broader cz-agents suite.
What I'd add next: batch generation from multiple invoices at once, and saved recipient templates. For now it does exactly what it was built to do. The best things come from your own annoying problem — this one solved mine.