Ledger AI
A personal finance workspace where every number it shows you can be checked:
upload a bank statement or a receipt, and ask questions in plain English that
are answered by SQL, not by a language model's guess.
Live demo: app.use-ledger-ai.com → Try the demo (no signup)
The demo button provisions a private, temporary account with three synthetic
accounts and ~250 transactions; it is deleted automatically when it expires.
Demo runs on synthetic data — Ledger AI connects to no real bank.
What it does, end to end
- Ingest — CSV statements (column layout auto-detected), text-layer PDFs,
and receipt photos read locally with Tesseract OCR.
- Normalize — merchants mapped to a stable key; re-uploading the same file
or row is a no-op, enforced by unique constraints.
- Categorize — your own corrections, then seeded merchant rules, then an
optional LLM; low-confidence rows go to a review queue. One correction can
be applied to every matching row, with the count shown first.
- Flag — exact and near duplicates and unusual charges (median/MAD), each
alert opening to show the statistics behind it.
- Ask — a question becomes a validated plan, compiled to a parameterized
SQL aggregate; the five steps stream over SSE, and any number in the
narration that is not in the result set causes it to be replaced by a
template. Export everything as a ZIP, or delete it with a preview.
Architecture
- web — Next.js 15 App Router, TanStack Query, Recharts; owns the session cookie.
- api — FastAPI, SQLAlchemy 2.0, Alembic; verifies a short-lived bearer token, scopes every query by
user_id.
- worker — RQ consumer for parsing, OCR, categorization, alert detection, plus the hourly maintenance sweeps.
- State — Postgres (integer-cent money, idempotency as unique constraints), Redis (queue, cache, rate limits), S3-compatible object storage.
- Hosting — five Railway services (web, api, worker, Postgres, Redis), redeployed on every push to
main.
Works fully with no API key: rules plan the question and templates write the
answer. An OpenAI key changes who proposes a plan, never who does the arithmetic.
Stack: Python 3.12 · FastAPI · PostgreSQL 16 · Redis · RQ · Tesseract · Next.js 15 · TypeScript · Docker · GitHub Actions.
Quick start
cp .env.example .env && cp apps/web/.env.example apps/web/.env.local
# set the same AUTH_SECRET (openssl rand -base64 32) in both files
make setup && make up && make migrate && make seed && make dev
Open http://localhost:3000 and press Try the demo. Full steps are below.
Tests
1,402 tests — 1,092 backend (pytest, against real Postgres and Redis) and
310 frontend (vitest) — plus ruff, mypy, eslint and tsc, migration round-trips
and Docker image builds, all in CI on every push.
make test # needs `make up` for Postgres and Redis
make lint # ruff · mypy · eslint · tsc --noEmit
Documentation
| Document | Contents |
|---|
| docs/design.md | Why it is built this way: plans not SQL, verified narration, demo isolation, rate limiting, testing strategy |
| docs/screens.md | Every screen, with what it shows and why |
| docs/architecture.md | System, processing, analysis, demo and deletion lifecycles |
| docs/data-model.md | Tables, constraints, indexes |
| docs/api.md | Every operation, what is public, and error semantics |
| docs/ai-usage.md | What the model sees, what it never sees, and every fallback |
| docs/security.md | Isolation, auth, demo expiry, OAuth linking, rate limiting, lifecycle |
| docs/deployment.md | Railway shape, variables, storage, proxy trust, health checks, rollback, backups |
| railway/README.md | Per-service deployment configuration and the settings that cannot live in it |
Running it locally
Prerequisites
Docker · Node 22+ · Python 3.12 · uv · Tesseract
(brew install tesseract)
Steps
git clone https://github.com/yds233013/ledger-ai.git && cd ledger-ai
# 1. Set the SAME secret in BOTH files — the API verifies the token the web app
# mints, so a mismatch means sign-in works and every API call then 401s.
# .env AUTH_SECRET=...
# apps/web/.env.local AUTH_SECRET=...
cp .env.example .env
cp apps/web/.env.example apps/web/.env.local
openssl rand -base64 32 # paste into both
make setup # install backend + frontend dependencies
make up # Postgres (5433), Redis (6379), MinIO (9000/9001)
make migrate # apply migrations
make seed # generate the synthetic development dataset
make dev # api :8000 · worker · web :3000
Open http://localhost:3000 and press Try the demo — no credentials needed.
To use the seeded development account instead, sign in with DEMO_USER_EMAIL /
DEMO_USER_PASSWORD from your .env.
Neither GitHub OAuth nor an OpenAI key is required. The app is fully functional
without both.
Optional: enable the AI stages
# .env
AI_ENABLED=true
OPENAI_API_KEY=sk-...
This changes who proposes a plan and who words an explanation. Numbers stay
SQL-computed and verified either way. Every response is Pydantic-validated with
a deterministic fallback on timeout, rate limit, malformed JSON, schema
violation, or a fabricated figure.
This is the beginning of the README. Read the rest at the original. Read the full README ↗