Every time I get an invoice or a broker deposit instruction, I do the same dumb, error-prone thing: copy the IBAN, the amount, and the variable symbol into my banking app by hand. One transposed digit and the money goes to a stranger.
Czech invoices increasingly carry a QR Platba code you just scan — but a lot of them still don't (anyone invoicing from Word/Excel, foreign senders, smaller shops). So I built a small tool that turns those payment details into a scannable QR, and it grew into something I think is worth sharing.
Live: https://qr.cz-agents.dev · Code (MIT): https://github.com/martinhavel/cz-agents-mcp · npm: @czagents/payqr
What it does
- Generates SPAYD (Czech/Slovak "QR Platba") and EPC / GiroCode (the SEPA standard, huge in Germany/Austria) payment QR codes from an IBAN + amount + reference.
- Also does plain-text, Wi-Fi and vCard QR, and reads a QR back (decode + classify).
- You can type the details or drop a payment screenshot — OCR runs in your browser.
- The lazy path (in an AI client / MCP): hand it a whole invoice — PDF or photo — and the model reads the IBAN, amount and reference and generates the payment QR for you. No retyping at all, with a verify step before anything is shown.
- It's 100% client-side: nothing is uploaded, no account, no tracking, no AI, MIT.
The bits that were actually interesting to build
1. The payment formats are just strings. SPAYD is a delimited string:
SPD*1.0*ACC:CZ6508000000192000145399*AM:1250.00*CC:CZK*X-VS:1234567890
EPC/GiroCode is 12 newline-separated lines (service tag, BIC, recipient, IBAN, amount, remittance…). EUR-only, recipient name required. Once you know the spec, generating them is deterministic — no API, no key, no rounding surprises. A lot of "QR code generator" tools just wrap a remote API; this one computes everything locally, which is what makes the privacy claim real.
2. IBAN validation is a tiny party trick. mod-97: move the first 4 chars to the end, turn letters into numbers, and the whole thing mod 97 must equal 1. ~10 lines, catches most typos before a QR is ever drawn.
3. Privacy is an architecture, not a checkbox. OCR uses tesseract.js and QR decoding uses jsQR, both lazy-loaded in the browser. The image never leaves the device. I could only honestly write "your image never leaves your device" because there is literally no backend.
The MCP part (and a payment-safety gotcha)
It's also an MCP server, so in an AI client you can hand it an invoice (PDF or photo) and the model reads the IBAN/amount/reference and calls qr_payment. The instructions force a read-back step: the model echoes the extracted fields and asks you to verify before showing the QR — because a misread digit is real money gone.
Here's the non-obvious part I burned an afternoon on. The tool returns the QR as an MCP image content block:
content: [
{ type: 'image', data: base64png, mimeType: 'image/png' },
{ type: 'text', text: JSON.stringify(result) },
]
In Claude Desktop, that image is given to the model as rendered pixels — which the model cannot re-export. So when I told it "show the user the QR as a file," it couldn't access the bytes… and helpfully regenerated the QR from the payload using its own library. For a payment QR that's dangerous: the model just became the source of truth, and a single mistyped character would silently corrupt the transfer.
Two fixes:
-
Never regenerate — and if the model has no choice, make it
qr_readits own image and diff the payload character-for-character before showing it. -
Return the PNG as base64 text too (
qr_png_base64), so the model can write the exact bytes to a file with zero re-encoding.
Lesson for anyone building image-returning MCP tools: the rendered image block is for the human; if you also want the model to do something with the bytes, hand them over as text.
Try it
https://qr.cz-agents.dev — type an IBAN, or drop a screenshot. It's free and there's nothing to sign up for. I'd love feedback on EPC/GiroCode edge cases (BICs, structured vs unstructured remittance) — that's where the spec gets fiddly.

Top comments (0)