Eine QRIS-Kassen-App mit Codex entwickeln: Notizen zur Entwicklung von QRIS-EDC von März bis Juli 2026
Ich bin Mohammad Hilmi Bisri, Gründer der PT Hanz Kode Terpadu (HanzCodes) in Jombang. Nach den Datumsangaben der Projektdateien habe ich QRIS-EDC von März bis Juli 2026 entwickelt. QRIS-EDC ist eine kostenlose Kassen-App, die im Browser des Smartphones läuft und für jede Transaktion einen QRIS-Code mit dem genauen Betrag anzeigt. QRIS ist der indonesische Standard für Zahlungen per QR-Code. Den größten Teil des Codes habe ich zusammen mit OpenAI Codex (externe Website) geschrieben, einem Programmieragenten, den man laut OpenAI in ChatGPT, im Code-Editor und im Terminal nutzen kann.
Der Projektordner enthält keine Protokolle der Gespräche mit Codex. Die Darstellung der Rolle von Codex beruht deshalb auf meinen eigenen Angaben. Ablauf und technische Details habe ich den Datumsangaben der Dateien in vier Arbeitsordnern und der Git-Historie des Backends entnommen. Wenn Sie ein Geschäft führen und eine Möglichkeit suchen, QRIS-Zahlungen anzunehmen, finden Sie den Vergleich im Artikel Kostenlose QRIS-Kassen-App.
Ursprünglich war QRIS-EDC mein Beitrag zum PIDI Digdaya X Hackathon 2026 von Bank Indonesia und OJK, unter dem Arbeitsnamen Kasir Digdaya. Der Beitrag war nicht unter den besten 80. Nach dem Hackathon habe ich weiter daran gearbeitet und die App kostenlos veröffentlicht.
Ablauf der Entwicklung
| Zeitraum | Arbeiten |
|---|---|
| 1. März 2026 | Erste Version: eine HTML-Seite mit einer JavaScript-Datei und einer CSS-Datei. |
| 2. März 2026 | Aufteilung der App in mehrere Seiten: Kasse, Artikel, Berichte, QRIS-Scanner und Einstellungen. |
| 4. bis 6. März 2026 | Neuaufbau als modulare SPA (Single-Page-Anwendung). Der Ordner engine enthält die Logik für QRIS, Lagerbestand, Drucker und Synchronisierung, der Ordner pages die einzelnen Bildschirme und der Ordner lib die Speicherung und Hilfsfunktionen. Einige große Funktionen habe ich zuerst in Markdown-Dateien geplant, zum Beispiel die Berichte und die Zustimmung zur Datenübermittlung. |
| Mai 2026 | Backend mit FastAPI und einer Turso-Datenbank, betrieben auf Vercel. Das Repository enthält 33 Commits, alle vom 14. und 19. Mai. In der App habe ich WebLLM ergänzt, eine Bibliothek, mit der kleine Sprachmodelle direkt im Browser laufen. |
| Juli 2026 | Schuldenbuch für Kunden (Kasbon) und eine geführte Tour in der App. |
QRIS-EDC nutzt dieses Sprachmodell, um mündliche Bestellungen wie „dua kopi, gula kurang“ (zwei Kaffee, wenig Zucker) in Einträge im Warenkorb umzuwandeln. Die Nutzer wählen das Modell selbst, zum Beispiel eine kleine Version von Qwen oder Gemma. Beim ersten Einsatz eines Modells lädt der Browser dessen Gewichte in den Cache.
So schreibe ich Prompts
Im Mai 2026 habe ich mein Prompt-Muster in einem internen Leitfaden festgehalten:
- Der Hauptprompt gibt dem Agenten die Rolle eines senior pragmatic software engineer, der Korrektheit (correctness), Sicherheit, Wartbarkeit und eine einfache Architektur an die erste Stelle setzt. Der Agent darf keine Fakten erfinden. Fehlt ein Detail, ohne das die Arbeit nicht weitergeht, stellt der Agent eine kurze Frage.
- Der Agent erledigt eine Aufgabe, prüft sie und geht erst dann zur nächsten über. Ich verlange keine parallele Arbeit und fasse keine Änderungen, die nichts miteinander zu tun haben, in einer Anfrage zusammen.
- Bevor er Code schreibt, formt der Agent meine grobe Idee in eine technische Spezifikation um: Rolle, Ziel, Kontext, Einschränkungen, zu lesende Dateien, Reihenfolge der Arbeit, Art der Prüfung und Definition of Done. Dabei streicht der Agent auch Werbesprache, unbegründete Annahmen und Funktionen außerhalb des Umfangs.
- Hat der Plan noch Lücken, übernimmt der Agent die Rolle eines Architekten und befragt mich. Er stellt höchstens fünf Fragen, nur solche, die die Umsetzung blockieren, und begründet jede einzelne.
Den Beispielprompt unten habe ich für diesen Artikel nach diesem Muster zusammengestellt. Der ursprüngliche Prompt, mit dem ich die Funktion für den dynamischen QRIS gebaut habe, ist nicht erhalten.
Sie sind ein senior pragmatic software engineer. Setzen Sie Korrektheit, Sicherheit, Wartbarkeit und eine einfache Architektur an die erste Stelle. Erfinden Sie keine Fakten. Wenn ein Detail Sie blockiert, stellen Sie eine kurze Frage.
Ziel: Schreiben Sie in js/engine/qris.js eine Funktion generateDynamicQris, die den statischen QRIS des Geschäfts in einen dynamischen QRIS mit Betrag umwandelt, vollständig auf dem Gerät und ohne Server.
Kontext: Lesen Sie js/engine/qris.js und js/pages/pos.js, bevor Sie etwas ändern. Der Inhalt des QRIS verwendet das Format aus ID, Länge und Wert aus der Spezifikation EMVCo Merchant-Presented Mode.
Einschränkungen: Ändern Sie die Daten des Händlerkontos nicht (ID 26 bis 51). Der Betrag muss eine positive ganze Zahl sein. Fügen Sie keine Abhängigkeiten hinzu und rühren Sie keine anderen Dateien an.
Schritte: Ändern Sie den Wert von ID 01 von 11 auf 12; entfernen Sie eine vorhandene alte ID 54; fügen Sie die neue ID 54 direkt vor ID 58 ein, indem Sie die Datenstruktur auslesen, statt nach Text zu suchen; berechnen Sie die CRC-16/CCITT mit dem Polynom 0x1021 und dem Startwert 0xFFFF neu. Führen Sie einen Schritt aus, prüfen Sie ihn und machen Sie erst dann weiter.
Prüfung: Die CRC-Funktion muss für den Text 123456789 den Wert 29B1 liefern. Testen Sie außerdem mit erfundenen Daten, nicht mit dem QRIS eines echten Geschäfts.
Bericht: geänderte Dateien, Begründung, Testbefehle mit Ergebnissen, Risiken und Annahmen.
Technischer Kern: Das Smartphone erzeugt den dynamischen QRIS
Der Inhalt eines QRIS-Codes ist ein Text aus Datenobjekten (data objects). Jedes Datenobjekt besteht aus einer zweistelligen ID, einer zweistelligen Längenangabe und dem Wert. Dieses Format stammt aus der Spezifikation EMV QR Code Merchant-Presented Mode von EMVCo (externe Website). Nach der Entwicklerdokumentation von Mastercard (externe Website), die sich auf diese Spezifikation stützt, hat ID 01 den Wert 11 bei statischen und 12 bei dynamischen QR-Codes. ID 54 enthält den Transaktionsbetrag, ID 58 den Ländercode und ID 63 die CRC, die das letzte Datenobjekt sein muss.
Alle Werte in der folgenden Tabelle sind erfunden.
| ID | Bedeutung | Statischer QRIS | Dynamischer QRIS |
|---|---|---|---|
| 00 | Formatversion | 01 | 01 |
| 01 | QR-Typ | 11 | 12 |
| 26 bis 51 | Daten des Händlerkontos vom QRIS-Anbieter | Original | Unverändert |
| 54 | Betrag | Nicht vorhanden | 25000 |
| 58 | Ländercode | ID | ID |
| 59 | Name des Händlers | TOKO CONTOH | TOKO CONTOH |
| 63 | CRC | Aus dem Inhalt des statischen QR-Codes berechnet | Neu berechnet |
Die Funktion in QRIS-EDC ändert ID 01, fügt ID 54 ein und berechnet dann die CRC neu. Die folgende Skizze habe ich vereinfacht:
function buatDinamis(qris, rp) {
// "6304" und alte CRC löschen
let s = qris.slice(0, -8);
// ID 01: 11 (statisch) zu 12
s = s.replace('010211',
'010212');
const n = String(rp);
const tag54 = '54' +
String(n.length)
.padStart(2, '0') + n;
// ID 54 vor ID 58 einfügen
const i = posisiTag(s, '58');
s = s.slice(0, i) + tag54
+ s.slice(i) + '6304';
return s + crc16(s);
}
Die Funktion posisiTag liest den Text von vorn, Datenobjekt für Datenobjekt, bis sie ID 58 findet. Die Zahl 58 kann auch innerhalb anderer Werte vorkommen, etwa in der Händlernummer. Eine einfache Textsuche könnte den Betrag deshalb an der falschen Stelle einfügen. Der Originalcode entfernt außerdem eine vorhandene alte ID 54 und lehnt Beträge ab, die keine ganze Zahl in Rupiah sind.
Die CRC umfasst den gesamten Text bis einschließlich 6304, also ID 63 mit der Länge 04. QRIS-EDC verwendet CRC-16/CCITT mit dem Polynom 0x1021 und dem Startwert 0xFFFF und schreibt das Ergebnis als vierstellige Hexadezimalzahl in Großbuchstaben. Um Ihre eigene CRC-Funktion zu testen, berechnen Sie die CRC des Texts 123456789: Das Ergebnis muss 29B1 lauten.
Grenzen dieses Verfahrens
Der QR-Code aus dieser Funktion verwendet die Daten des Händlerkontos vom QRIS-Aufkleber des Geschäfts. Der QRIS-Anbieter des Geschäfts wickelt die Zahlung ab, und das Geld geht auf das Konto, das mit diesem Aufkleber verbunden ist. QRIS-EDC verwahrt kein Geld und ist nicht an das System des Anbieters angebunden, daher weiß die App nicht, ob eine Zahlung erfolgreich war. Die Kassenkraft prüft den Zahlungseingang weiterhin in der Banking-App oder E-Wallet des Geschäfts, bevor sie die Transaktion speichert. Dieser QR-Code stammt auch nicht aus dem System des QRIS-Anbieters, und ich behaupte nicht, dass die Bank Indonesia oder die ASPI dieses Verfahren genehmigt haben.
Entscheidungen, die ich selbst getroffen habe
Turso lehnt PRAGMA von SQLAlchemy ab
Das Backend nutzt SQLAlchemy mit Turso (libSQL) über das Paket sqlalchemy-libsql. Beim ersten Verbindungsaufbau führt der SQLite-Dialekt von SQLAlchemy PRAGMA read_uncommitted aus, um die Isolationsstufe der Transaktionen zu lesen. Turso lehnt diesen Befehl ab, wenn der Zugriff über das Hrana-Protokoll läuft, und die Verbindung scheitert mit ValueError 405.
Die Korrektur steht in der Git-Historie der Datei app/db.py vom 14. Mai 2026, in sieben Commits zwischen 03:54 und 04:19 Uhr WIB. Ich habe pool_pre_ping abgeschaltet und dann die Methode get_isolation_level mit einem Wrapper versehen, sodass sie bei diesem Fehler SERIALIZABLE zurückgibt, die Standard-Isolationsstufe von SQLite. Nachdem ich mehrere URL-Formate ausprobiert hatte, bin ich zum Format aus der SQLAlchemy-Dokumentation von Turso (externe Website) zurückgekehrt. Das Muster sieht so aus, mit Host und Token als Platzhaltern:
from sqlalchemy import \
create_engine
from sqlalchemy.dialects \
import sqlite
D = sqlite.base.SQLiteDialect
asli = D.get_isolation_level
def aman(self, conn):
try:
return asli(self, conn)
except Exception:
return "SERIALIZABLE"
D.get_isolation_level = aman
engine = create_engine(
"sqlite+libsql://HOST_DB"
"?secure=true",
# TOKEN aus env gelesen
connect_args={
"auth_token": TOKEN},
pool_pre_ping=False,
)
Telemetrieplan für eine Million Geräte
QRIS-EDC hat eine optionale Funktion, die Preisdaten für Grundnahrungsmittel (Sembako) zur Beobachtung der Inflation und monatliche Zusammenfassungen der Transaktionen für eine alternative Kreditbewertung übermittelt. Die Funktion ist nur aktiv, wenn die Nutzer zustimmen. Am 6. März 2026 habe ich zwei Pläne für das Backend verfasst, TELEMETRY_BE_PLAN.md und TELEMETRY_PYTHON_ENTERPRISE_PLAN.md, mit dem Ziel „1.000.000+ node“. Der Entwurf sah Apache Kafka als Warteschlange vor, dazu Redis, InfluxDB für zeitbasierte Preisdaten, PostgreSQL und Kubernetes, um die Kapazität automatisch zu erweitern.
Für die Demo beim Hackathon habe ich mich für einen viel kleineren Entwurf entschieden: einen einzigen FastAPI-Monolithen nach IMPLEMENTATION_PLAN_SERVER.md. Das Dokument bezeichnet sich selbst als MVP für Hackathon und Demo, weil sich so ein Monolith schnell bauen lässt, wenig Ressourcen braucht und später leicht auf PostgreSQL umgestellt werden kann. Redis, Celery, PostgreSQL und ein aufwendiges Modell zur Kreditbewertung stellt der Plan zurück.
Dieses Backend habe ich auf Vercel mit einer Turso-Datenbank betrieben. Die Deploy-Dokumentation des Projekts nennt die Einschränkungen der Serverless-Umgebung von Vercel, die seine Form mitbestimmt haben:
- SQLite-Dateien bleiben in den Serverless-Funktionen von Vercel nicht erhalten, deshalb nutzt die Produktionsdatenbank Turso.
- Es gibt keine Hintergrundaufgaben, die dauerhaft laufen. Das Backend bereinigt alte Daten zufällig bei etwa 2 % der Requests.
- Alembic-Migrationen sollte man vor dem Deploy von Hand ausführen, damit nicht mehrere Cold Starts gleichzeitig migrieren. Die automatische Migration beim Start der App dient nur als Rückfallebene.
Dieser Artikel ist ein Erfahrungsbericht über die Entwicklung einer App und ersetzt nicht die offiziellen Regeln für QRIS. Die Codeausschnitte oben sind vereinfachte Skizzen. Für die Annahme von QRIS-Zahlungen richten Sie sich bitte nach den Vorgaben der Bank oder des QRIS-Anbieters Ihres Geschäfts.
Quellen
- Produktseite von QRIS-EDC, HanzCodes (externe Website)
- Codex, OpenAI (externe Website)
- SQLAlchemy + Turso, Dokumentation von Turso (externe Website)
- EMV QR Code Specification for Payment Systems (EMV QRCPS) Merchant-Presented Mode, EMVCo (externe Website)
- Generate QR Code for Merchants, Mastercard Developers (externe Website)
Brauchen Sie Unterstützung?
Schildern Sie uns, was Ihre Behörde oder Ihr Unternehmen braucht. Das erste Gespräch ist kostenlos.