Agentic Trading Lab
Talk to agents, and test trading ideas. Try AI trading agents on real market data with no code required.
AI translation
Haushaltsbuch mit KI: Kontoauszüge importieren, per Regeln und Claude kategorisieren, Tagesbudget und Assistent. React Router 7, Drizzle, Postgres, Vercel.
Translation pending
FinanceTypeScript · SwiftGitHub @benschuetzeOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Ein Haushaltsbuch, das zuerst die Frage beantwortet, die man wirklich hat: Wie viel kann ich heute ausgeben? Kontoauszüge kommen per Bankanbindung oder Datei, Regeln und ein LLM kategorisieren, ein KI-Assistent rechnet mit echten Zahlen. Was die KI nicht sicher weiß, wird nicht geraten, sondern landet in „prüfen“.
| Demo | haushaltsbuch-demo.vercel.app: erfundene Daten, ohne Login, zum Ausprobieren: Import (mit Beispieldateien), Regeln, Budgets, Ziele, Prüfen. Jede Nacht und per Knopf zurück auf die Beispiele. Die Beispielmonate wandern mit dem Kalender mit (die Dateien sind für Oktober 2026 geschrieben, der Import verschiebt sie um die seither vergangenen Monate). Der Assistent antwortet dort mit dem echten Claude-Modell (begrenzte Zahl Fragen pro Tag). |
| Code | github.com/benschuetze/haushaltsbuch |
Frag zuerst
heute_verfuegbar, leisten, wann_leisten, naechste_zahlungen, buchungen_suchen, abos_und_fixkosten …) mit echten Zahlen aus der Datenbank. Er kann nichts ändern; höchstens 60 Fragen am Tag.Weniger Arbeit
Überblick und Motivation
Design: „E-Ink-Kassenbuch“, Papier und Tinte wie ein E-Paper-Display, eine Akzentfarbe (Kugelschreiber-Blau) für die eine wichtige Zahl, doppelt unterstrichen wie eine Summe in der Buchhaltung. Am Handy eine Tab-Leiste mit der Frage in der Mitte.
flowchart LR
subgraph Eingang
CSV[CSV / CAMT.053 / MT940] --> Detect[Format erkennen<br/>Bankprofil oder gelerntes Format]
Bank[Enable Banking<br/>PSD2, täglicher Cron] --> Norm
Detect --> Norm[NormalizedRow<br/>Zod, Cent, Datum ohne Zeitzone]
end
Norm --> Dedupe[Dedupe-Hash] --> Transfers[Umbuchungen verknüpfen]
Transfers --> Rules[Eigene Regeln] --> Known[Bekannte Händler] --> LLM[Claude Haiku<br/>nur 4 Felder, IBAN geschwärzt]
LLM -->|Konfidenz ≥ 0,7| DB[(Postgres<br/>Neon Frankfurt)]
LLM -->|unsicher| Review[„prüfen“] --> DB
DB --> Loaders[React-Router-Loader<br/>Tagesbudget, Monat, Abos, Ziele]
DB --> Agent[Assistent<br/>13 lesende Werkzeuge, Tageslimit]
Loaders --> UI[UI, serverseitig gerendert]
Agent --> UI
Alles läuft in einer React-Router-App auf Vercel: Loader und Actions sind der Server, app/lib enthält die Logik ohne UI. Datenzugriff gibt es nur dort, nie in Komponenten.
| Entscheidung | Warum |
|---|---|
Geld als Integer in Cent, Parsing nur in money.ts | Floats runden falsch (0,1 + 0,2). Jeder Betrag kommt als Text aus der Bank und wird ohne parseFloat gelesen. |
| Das Günstigste zuerst: Regeln, Händlerliste, dann LLM | Jeder Modellaufruf kostet Geld und ist nicht deterministisch. Was eine Regel weiß, fragt man kein Modell. |
| Unsicher heißt „prüfen“, nie geraten | Eine falsche Kategorie mit hoher Konfidenz sieht niemand. Eine Frage kostet den Nutzer zwei Sekunden. |
manual schlägt alles, in der SQL-Bedingung | Eine Korrektur des Nutzers darf kein späterer Import und keine neue Regel überschreiben, auch nicht durch einen Bug im App-Code. |
| Tagesbudget statt Kontostand | CSV-Exporte enthalten selten einen Saldo. Die App rechnet pro Gehaltsperiode: was kam, was ging, was noch fällig ist. |
| Assistent nur mit lesenden Werkzeugen | Ein Sprachmodell soll rechnen lassen, nicht selbst rechnen, und nichts ändern können. Jede Zahl in der Antwort kommt aus einem Werkzeug. |
| Tageslimits atomar in der Datenbank | Ein UPDATE … FOR UPDATE statt Zählen im Speicher: Serverless-Instanzen teilen keinen Speicher, zwei gleichzeitige Anfragen können das Limit nicht überholen. |
| Unbekannte CSV-Formate lernen statt nur feste Profile | Es gibt Hunderte Banken. Die Zuordnung der Spalten sieht das Modell nur als Spaltennamen und maskierte Muster und wird pro Format gespeichert. |
| Import ohne Datenbank-Transaktion, dafür wiederholbar | Der Treiber neon-http kennt keine interaktiven Transaktionen. Statt eines zweiten Treibers (WebSocket-Pool) nur für Schreibvorgänge ist jeder Schritt wiederholbar: Buchungen sind per Dedupe-Hash idempotent, der Import trägt einen Status (running/done/failed), und der nächste Import holt Buchungen abgebrochener Importe nach. Ein Test bricht nach dem ersten Block ab und prüft, dass danach nichts ohne Kategorie oder „prüfen“ bleibt und nichts doppelt ist. |
| Erwartungswerte von Hand nachgerechnet | Ein Test, der seine Erwartung aus dem App-Code holt, prüft nichts. beispieldaten/erwartet.json ist unabhängig. |
schema.ts ($inferSelect, $inferInsert), es gibt keine doppelten Interfaces.+types/…), Loader-Daten sind in der Komponente typisiert. tsc --noEmit im strikten Modus ist Teil von pnpm check und der CI.Gemessen mit pnpm eval am 12.10.2026, Modell claude-haiku-5-5, 203 erfundene und von Hand beschriftete Buchungen (evals/kategorisierung.json). 91 davon entscheidet die eingebaute Händlerliste ohne Modell, 112 gehen an das Modell.
| Kennzahl | erster Lauf | mit Beschreibungen und fester Liste | zusätzlich eigene Beispiele |
|---|---|---|---|
| Trefferquote gesamt | 93,6 % | 99,0 % | 99,0 % |
| Trefferquote nur Modell | 88,4 % | 98,2 % | 98,2 % |
| Anteil „prüfen“ | 13,8 % | 9,4 % | 9,4 % |
| Falsch mit Konfidenz ≥ 0,7 (stille Fehler) | 7 | 1 | 0 |
| Tokens pro Buchung ans Modell | 158 | 216 | 231 |
Im ersten Lauf sah das Modell nur Kategorienamen und verlor die meisten Punkte an Grenzen: Kletterhalle und Tanzschule als „Freizeit“ statt „Hobbys“, Kindergeld als „Kind & Kita“ statt „Einkommen“. Seitdem bekommt jede Standardkategorie eine Zeile Beschreibung, und das Antwortschema lässt nur die vorhandenen Kategorienamen zu.
Eigene Beispiele: Bis zu fünf Buchungen, die der Haushalt selbst eingeordnet oder bestätigt hat und die der neuen ähneln, gehen als Beispiele mit (dieselben vier Felder, IBANs geschwärzt). Im Eval stehen dafür die jeweils anderen Fälle, nie die eigenen. Ergebnis: gleiche Trefferquote, der letzte stille Fehler (Laufschuhe als „Kleidung“ statt „Hobbys“) landet stattdessen in „prüfen“, 7 % mehr Tokens. Das ist ein einzelner Fall und kann Zufall sein.
Einschränkung: Beschreibungen und Beispiele sind nach dem ersten Lauf auf demselben Set entstanden. Die späteren Zahlen sind deshalb optimistischer als eine Messung auf neuen Daten; neue Fälle kommen ins Set, bevor der Prompt wieder angepasst wird.
Schwelle 0,7: Ohne Beispiele bleibt bei 0,7 ein stiller Fehler und 9,4 % gehen nach „prüfen“; 0,8 hätte ihn für 12,3 % „prüfen“ vermieden, 0,9 für 19,2 %. Antworten mit Konfidenz 0,7–0,9 stimmten zu 95 %, ab 0,9 zu 100 %. Die vollständigen Auswertungen je Kategorie, Kalibrierung und Verwechslungen: evals/ergebnis.md, evals/ergebnis-beispiele.md, erster Lauf in evals/ergebnis-erster-lauf.md.
Datei/Bank ──► Bankprofil ──► Dedupe ──► Umbuchungen ──► Regeln ──► LLM ──► Konfidenz ≥ 0,7 ? ──► fertig
(Zod) (Hash) verknüpfen (Priorität) (nur 4 Felder) │
└──► „prüfen“
Korrektur in der UI ──► manuell (nie wieder überschrieben) ──► optional Regel ──► alle passenden Buchungen neu
WHERE-Klausel des Updates in der Datenbank ab, nicht nur der App-Code.Permissions-Policy (Mikrofon nur für die Spracheingabe), noindex.DEMO_READ_ONLY=1 ist sie wieder schreibgeschützt, dann blockiert eine Middleware jeden Schreibzugriff.event (z. B. categorize_llm_failed, request_failed, review_share_high). In Vercel unter Logs nach event filtern. Bankdaten (iban, raw, purpose, counterparty) und Header werden geschwärzt, falls Code sie versehentlich mitgibt.SENTRY_DSN gehen Warnungen und Fehler an Sentry, nur serverseitig, ohne Anfrage, Cookies, IP und Buchungsinhalte. Ohne DSN wird das SDK nicht einmal geladen./bank/sync, 5 Uhr) prüft, welcher Anteil der Buchungen der letzten 7 Tage, die beim Modell lagen, noch in „prüfen“ wartet. Über 50 % (ab 10 Buchungen) schreibt er review_share_high als Warnung, die in Sentry als eigenes Issue mit E-Mail ankommt. Begründung der Schwelle in review-alarm.ts: Ein ausgefallenes Modell (Key abgelaufen, Modell-ID abgeschaltet) schickt sonst still alles in „prüfen“.POST /api/capture bucht eine diktierte Barzahlung ins Konto „Bar“ (z. B. aus einem iOS-Kurzbefehl). Eingeschaltet wird sie mit CAPTURE_TOKEN (mindestens 32 Zeichen, openssl rand -hex 32); ohne Token antwortet der Endpunkt mit 503, in der Demo mit 403.
curl -X POST https://<host>/api/capture \
-H "Authorization: Bearer $CAPTURE_TOKEN" -H "Content-Type: application/json" \
-d '{"text": "hab 7 Euro für Döner gezahlt", "key": "<einmalige-id>"}'
key macht den Aufruf wiederholbar: derselbe Schlüssel bucht nur einmal, die Antwort ist dann HTTP 200 mit der ersten Buchung (neu: 201).capture_text erhalten.accounts · imports · categories · transactions · rules, Details in PLAN.md und app/lib/db/schema.ts. Das Drizzle-Schema ist die einzige Quelle für Typen, Zod prüft alle Eingänge (Formulare, CSV-Zeilen, LLM-Antworten).
React Router 7 (Framework-Modus) · TypeScript strict · Drizzle ORM · Neon Postgres · Vercel · Vercel AI SDK mit Claude · Tailwind · shadcn/ui · Vitest · PGlite · Playwright · axe-core · Biome · GitHub Actions
Voraussetzung: Node 22+ und pnpm (corepack enable). Ein lokales Postgres ist nicht nötig, PGlite übernimmt.
pnpm install
cp .env.example .env # APP_PASSWORD und SESSION_SECRET setzen, AI_MOCK=1 ohne API-Key
pnpm db:local # Datenbank in .pglite/, migriert und mit Standardkategorien
pnpm dev # http://localhost:5173
| Befehl | Was |
|---|---|
pnpm test | Unit- und Datenbanktests mit PGlite im Speicher, ohne Netz und ohne LLM |
pnpm test:e2e | Playwright auf Desktop (Chromium) und iPhone (WebKit), mit Barrierefreiheits-Check (axe) |
pnpm eval | 203 erfundene, von Hand beschriftete Buchungen durch den echten Ablauf (Händlerliste, dann Modell): Trefferquote gesamt und nur Modell, Anteil „prüfen“, sichere Fehler. Die kostenlose Stufe prüft schon pnpm test |
Die Erwartungswerte für die Demo-Daten stehen in beispieldaten/erwartet.json. Sie sind von Hand nachgerechnet und nicht mit dem App-Code erzeugt.
Die wichtigste Zahl im Eval ist „falsch mit Konfidenz ≥ 0,7“. Diese Fehler sieht der Nutzer nicht, weil sie nicht in „prüfen“ landen.
beispieldaten/banken/.public/logos/ sind die App-Icons der Institute von ihren offiziellen Websites (als PNG neu kodiert) und Marken der jeweiligen Inhaber. Sie dienen nur dazu, das Institut zu erkennen.Heute ist jede Instanz ein Haushalt mit einem Passwort. Für viele Haushalte in einer Datenbank:
household_id auf allen fachlichen Tabellen (accounts, imports, transactions, categories, rules, budgets, insights, savings_goals, bank_connections, bank_formats …). Eindeutigkeiten gelten dann pro Haushalt: Kategoriename, Kontoschlüssel und der Dedupe-Hash bekommen household_id in den Unique-Index, sonst blockiert ein gleicher Import im Nachbarhaushalt die eigene Buchung.app/lib bekommt die ID als Pflichtparameter, damit eine vergessene Einschränkung schon beim Typecheck auffällt.app.household_id, Policies filtern jede Tabelle danach. Ein Bug im App-Code gibt dann trotzdem keine fremden Buchungen heraus.Das Projekt ist mit Claude Code als primärem Werkzeug entstanden. CLAUDE.md legt Stack, Regeln und Befehle fest, .claude/skills/ enthält die wiederkehrenden Abläufe:
| Skill | Wofür |
|---|---|
verify | Tests schreiben, ausführen und die UI im Browser prüfen, bei jeder Änderung |
db-change | Schema ändern, Migration erzeugen und auf DROP prüfen |
bank-profile | Neues CSV-Format einer Bank unterstützen |
categorizer-eval | Prompt oder Modell ändern und vorher/nachher messen |
e-ink-design | Neue UI im E-Ink-Look bauen |
ship | Commit, CI, Preview und Produktion prüfen |
Grundsätze: Jeder Fehler bekommt zuerst einen Test, der ihn zeigt. Tests beschreiben das Versprechen an den Nutzer, nicht die Implementierung. Nichts gilt als fertig, was nicht gelaufen und gesehen wurde.
Tempo: Vom ersten Commit (9.10.2026) bis zur Fassung hier vergingen drei Tage, mit gut 50 Pull Requests. Gesteuert wurde über kurze Aufträge, oft vom Handy; jeder PR lief durch pnpm check (Typecheck, Lint, Unit- und Datenbanktests, Playwright auf Desktop und iPhone) und die CI, bevor er gemergt wurde. Live-Änderungen wurden danach auf der Demo nachgeprüft.
Wo der Agent falsch lag:
| Fehler | Wie es auffiel | Was daraus wurde |
|---|---|---|
| Die vom Agenten gebaute Händlerliste ordnete die Rente der „Deutschen Rentenversicherung“ als „Versicherungen“ ein: Das Wort im Namen passte, dass es ein Eingang war, prüfte die Liste nicht. | Ein neuer Test lässt das Eval-Set bei jedem pnpm test gegen die Händlerliste laufen; er schlug beim ersten Lauf an. | Händlerliste nur noch für Ausgaben, bereits falsch einsortierte Eingänge gehen nach „prüfen“; die Händlerlisten-Prüfung ist fester Teil von pnpm test und im Skill categorizer-eval beschrieben. |
| Auf die Frage „100 € mehr sparen?“ empfahl der Assistent einen Dauerauftrag aufs Tagesgeld, obwohl dort schon 300 € im Monat hingingen. Alle Zahlen stimmten, der Rat nicht. | Beim Nachrechnen der Antworten für das Demo-Video gegen die Datenbank. | Prompt-Regel: vor einem Sparrat die kommenden Zahlungen nachsehen und auf einem laufenden Auftrag aufbauen. Mit dem echten Modell geprüft: Die Antwort nennt jetzt die laufenden 300 € und schlägt 400 € vor. |
| Auf dem iPhone erschien eine Frage doppelt, und die Ladeanzeige blieb hängen. Die Playwright-Tests auf iPhone (WebKit) waren grün, weil sie nur einmal abschickten. | Beim Ausprobieren auf einem echten iPhone. | Ursache: iOS schickt beim Diktieren plus „Los“-Taste zweimal ab, der zweite Request brach den ersten ab. Der Test schickt jetzt doppelt ab und war ohne Fix rot. Seitdem gilt: Ein gemeldeter UI-Fehler bekommt zuerst einen Test, der ohne den Fix fehlschlägt. |