Analyze receipts incl. handwritten annotations with AI analysis to auto-adjust totals and assign payers based on custom instructions.
Important
This is a personal app I built to solve a personal problem. It is open-sourced under the MIT license for learning purposes. On a best-effort basis, it aims to demonstrate sound architecture and engineering practices. It is not a product, not intended for distribution, and not meant to run out-of-the-box for others.
It is challenging for me as a U.S. citizen to open a bank account in Germany due to FACTA. So without a joint bank account, how can we best split shared households expenses?
For receipts with shared and individual items, how can we indicate that some items are to be split between persons and others not?
As part of my pivot to AI Engineering and Architecture, I decided to build my own app instead of using existing commercial apps, which has added privacy benefit.
The original total is 23.23 as per the OCR data. Handwritten annotations indicate that Julie paid, and MM owes. Two items, PUTENSTEAK (8.08) and SPEZI KROMBACHER (0.99), are circled, which defines the adjusted total as their sum: 9.07. The handwritten note 'Julie paid' clearly identifies the payer, and the annotations are unambiguous, leading to high confidence in both the adjusted total and payer determination.
- Overall: 90%
- splitAmount: 90%
| Component | Role |
|---|---|
| Nuxt | Full-stack web app (UI + API) |
| Postgres | Database for receipts, splits, and workflow state |
| SQLite | SQLite DB for @nuxt/content to serve the landing page. |
| Component | Role |
|---|---|
| Vercel | App Platform that automatically deploys my application as serverless functions. |
| Trigger.dev | Platform for fully managed workflows (agent) and orchestration. |
| Supabase | Postgres development platform with built-in features and optimizations for serverless functions. |
| Turso | Hosted libSQL for @nuxt/content, not Postgres: Nuxt Content needs the pg driver, which conflicts with the postgres-js driver Supabase's pooler requires. |
| Azure Blob Storage | Receipt photo storage (direct client uploads via SAS) |
| Azure Document Intelligence | OCR for receipts (prebuilt-receipt model) |
| Vercel AI Gateway | Provider-agnostic LLM routing (ZDR, prompt caching, fallback) |
| GPT-4o | Handwritten annotation detection (via the Gateway) |
| GPT-4o-mini | Receipt normalization & split adjustment (via the Gateway) |
Note
I chose Trigger.dev over Vercel Workflows for its developer experience and stronger feature set, for example, durable compute, remote-controlled execution.
Highlights of the AuthN / AuthZ design (see docs/SECURITY.md for detail):
- Two principal types, never blended. Human users authenticate via session (GitHub OAuth); Trigger.dev tasks authenticate via HMAC. The auth pipeline commits to one path based on request headers — a failed task token does not fall back to session auth.
- Action-scoped HMAC tokens. Each task gets only the
resource:permissionpairs it needs (e.g.expense:write,upload:read). Tasks cannot mint their own tokens; only registered orchestrators can, and only for their declared children. - Cryptographic scope binding. The token's scope is part of the signed HMAC payload, not a separate header — tampering invalidates the token. Verification uses
crypto.timingSafeEqualand enforces a server-side expiry window. - Household isolation at the query layer. Every resource read filters by the caller's household; resource-by-id reads go through a
requireAuthorizationguard that returns 404 (not 403) on mismatch to avoid leaking existence. - Sessions hold identity + authZ scope only. Domain data lives in Pinia stores / DB queries, not in the session.
- User decides what personal information to send. The app can only send user initials to AI and agents with receipts. However, the user can choose to provide that information in custom instructions for their household to increase receipt analysis and payer assignment accuracy.
Important
If you find a security issue or bug, please feel free to open an issue or email me. Cheers.
Note: offline development is not possible due to Azure, LLM, and Trigger.dev dependencies.
The .env.sample file describes which configuration is needed for the app to run. First, copy the file.
cp .env.sample .env Then open .env and fill in your own config values
Tip
You can use the scripts in this repo's Makefile to generate some of the secret values.
Finally, load the configuration.
. ./.envImportant
You need to load the env config before every command below - except for docker.
docker compose -f docker-compose.dev.yaml up -dAfter loading configuration from your custom .env, start the app.
npm run devConnect to trigger.dev (orchestrator) to use local machine as a worker/agent.
npm run trigger:devDrizzle provides a UI to explore database
npm run db:studioOpen https://local.drizzle.studio/
The docs are semi-scattered brain dumps.
| Path | Target Audience |
|---|---|
ARCHITECTURE.md |
High-level system architecture |
docs/ |
for Human understanding and reference |
docs/SECURITY.md |
Security model |
trigger/README.md |
Trigger.dev tasks — how to run and trigger them |
shared/enums/README.md |
Enum conventions |
.claude/rules/ |
Instructions for agent (always loaded) |
.claude/skills/ |
Loaded on Demand |
CLAUDE.md |
General agent instructions |
- UI to upload files (drag-and-drop, queued, direct-to-Azure)
- UI to view/edit/delete receipts
- UI for splits (via receipt view page)
-
Receipt inbox view (list + preview at(temporary solution removed)/receipts/inbox) - Real-time workflow status updates via SSE
- SSO with GitHub
- Blob SAS tokens on-demand
- Document Intelligence API — OCR via Blob URL + SAS token
- Pino structured logging with domain-based child loggers
- Field-level change history for receipts and splits (human + agent mutations)
- Deploy GPT-4o model for analyzing images for handwritten annotations
- Send API request with 1) OCR JSON and 2) Blob URL
- Process response, e.g. probability
- Trigger.dev orchestrator: OCR → annotations → normalize → split creation → adjust split
- Per-step progress tracking in
workflow_runstable - HMAC-authenticated HTTP API for tasks (no direct DB access)
- Action-scoped tokens — minimum permissions per child task
