Aperçu visuel
Captures prises en mode démo (persona "Alex Démo", dataset synthétique — aucune donnée réelle ne sort jamais de la machine, et surtout pas vers un repo public).
Case study · finance-tracker
Une app perso qui ingère des relevés PDF, les fait catégoriser par Claude, calcule un score de santé financière, et propose une démo publique verrouillée pour montrer sans rien fuiter. Stack : React 18 + NestJS 11 + Anthropic SDK, code 100% écrit avec Claude Code.
Aperçu visuel
Captures prises en mode démo (persona "Alex Démo", dataset synthétique — aucune donnée réelle ne sort jamais de la machine, et surtout pas vers un repo public).
Pourquoi ce projet
Suivre ses finances perso à la main, c'est lourd : ouvrir chaque relevé, ressaisir chaque transaction, retrouver de tête à quelle catégorie ça appartient. L'enjeu était de voir si un LLM pouvait absorber cette corvée de catégorisation et la rendre fiable — pas juste plausible — sur des PDFs bancaires français bruts.
Sous-objectif personnel : c'était aussi un terrain pour pratiquer NestJS + React 18 + Tailwind + shadcn, hors de la zone JVM dans laquelle je tourne en prod depuis 2005. Le code a été écrit en pair-programming avec Claude Code, ChatGPT a posé le logo et les premières maquettes UX. Aucune ligne tapée en solo.
Architecture
Trois flux remarquables coexistent : l'import PDF
(synchrone, multipart, jusqu'à 12 fichiers en une fois), l'auto-sync
post-analyse qui met à jour les jauges crédit / épargne, et le
mode démo isolé qui dérive le dataDir
runtime via AsyncLocalStorage pour ne jamais croiser les
données réelles avec les fixtures publiques.
Stack & data flow
font-feature-settings: 'tnum'
global pour des chiffres tabulaires partout — non-négociable en
fintech. data/finance/statements/<YYYY-MM>.json, archivage
automatique en sous-dossier archive/<YYYY>/ pour
les années passées. tool_choice: { type: 'tool' } strict pour forcer
la sortie structurée. Phase 1 extrait les transactions brutes +
score + suggestions de récurrents ; Phase 2 catégorise chaque
transaction (catégorie / sous-catégorie / confidence). Découper en
deux appels donne un meilleur taux de réussite que tout demander
d'un coup. /api/ vers le backend
sur le même réseau Docker. PinGuard NestJS global,
Bearer token simple comparé à process.env.APP_PIN.
Mode permissif si la variable est absente. Frontend stocke le PIN
en sessionStorage (volontairement non persistant entre onglets). Le process avec Claude Code
Construit en 8 phases incrémentales (V3 livrée 2026-05-02, durcissements V3.1 le 2026-05-03), chaque phase écrite en TDD bite-sized via plans markdown que Claude Code génère puis exécute. Mon rôle : tracer la vision, valider à chaque étape, repérer ce qui cloche en testant l'app avec mes propres relevés. Le rôle de Claude : poser le code, expliquer, itérer.
tool_choice strict évite les hallucinations de format ;
la validation Zod côté NestJS bloque ce qui passe quand même. Host et, s'il matche
DEMO_FORCED_HOSTS (par défaut trycloudflare.com),
force demoMode = true et bypass PinGuard. La
UI lit /api/demo/status au boot ; si forced,
elle masque le bouton "Quitter démo" et affiche un badge "Démo
verrouillée". N'importe quel quick tunnel ad-hoc devient ainsi
public mais isolé sur la persona "Alex Démo" —
dataset synthétique 6 mois, jamais croisé avec les vraies données. 2026-08 — santé financière & IA 100% locale
Trois mois de vie réelle plus tard, l'app a changé de nature : d'un visualiseur de relevés, elle est devenue un outil de décision pour sortir d'une situation d'endettement revolving. Principe directeur de cette phase : « les chiffres en code, les mots au LLM » — aucun verdict n'est délégué à une IA, et le LLM utilisé est un modèle local (Ollama + qwen3 sur RTX 5090), donc les libellés bancaires ne quittent jamais la machine.
documentType par PDF et
route ; un relevé bancaire déposé au mauvais endroit est refusé
avec un message qui pointe la bonne page (leçon apprise : avant ce
garde-fou, un relevé Sofinco a écrasé le relevé bancaire du même
mois — restauré grâce aux snapshots automatiques pré-écriture). Pivots & lessons learned
extractMonthFromFilename
devinait mal et ratait des mois. Solution : ne reconnaître que le
pattern explicite YYYY-MM, sinon dériver la période
côté code via le nombre de jours distincts (pas le
nombre de tx) — un relevé qui chevauche deux mois vote pour celui
avec ~21 jours uniques contre ~9. archive/<YYYY>/ et régénérer depuis
l'ensemble du dossier. Les opérations destructives par
défaut, c'est presque toujours une mauvaise idée. KNOWN_LOAN_CREDITORS
(organismes REGAFI/ASF) et sans regex
NOT_A_CREDIT (PAIEMENT CB|ACHAT CB|RETRAIT…),
on créait des crédits fantômes. Leçon : un LLM qui suggère, c'est
bien. Un code qui filtre avant d'écrire, c'est mieux. window.location.reload() après écriture règle le
problème en 1 ligne. À ne pas sur-architecturer. isQuotaError() qui ne mélange plus.