Ledgerline checks whether payment records from different systems agree with each other. Upload CSV exports, for example an ERP ledger, a payment gateway export and a bank statement, and ask it to reconcile. It tells you what matched, what didn't, and why. Every figure points back to a row in one of your files.
You don't have to describe your files first. Ledgerline works out from the data which columns link the files, which column holds the amount, and which file comes first in the flow of money.
┌──────────────┐ ┌───────────────────┐ ┌────────────────┐
│ erp_ledger │─────▶│ gateway_razorpayx │─────▶│ bank_hdfc │
│ 9 of 10 rows │ │ 10 of 10 rows │ │ 9 of 10 rows │
└──────────────┘ └───────────────────┘ └────────────────┘
ORD-1002 erp and gateway differ by 50.00 exception
ORD-1008 two gateway rows for one capture exception
ORD-1006 never reached the gateway unmatched
- The AI never does the maths. All matching and adding up happens in SQL. The AI agent decides what to run and explains the results. It never produces a number itself.
- Settings are measured from the data, not hard-coded. Join columns, amount columns and the normal settlement delay are all worked out from the files you upload.
- Nothing is auto-approved without a second check. Before any match is
marked
accepted, a separate verifier re-reads the original rows and redoes the sums. - Unmatched rows are normal. A bank statement has fees and balance lines that no order will explain. They are reported plainly, not treated as errors.
You need Python 3.12 with uv, Node.js, and an
OpenRouter API key.
# backend (http://127.0.0.1:8000)
cd backend
uv sync
cp .env.example .env # then add your OPENROUTER_API_KEY
uv run uvicorn app.main:app --reload
# frontend (http://localhost:5173, sends /api calls to the backend)
cd frontend
npm install
npm run devOn Windows, ./dev.ps1 starts both.
Then upload CSVs on /upload and ask the agent to reconcile. /report shows
the close report, which you can download as a single HTML file.
For demo data, run uv run python ../demo/load.py from backend/. It wipes the
database and loads the three files in demo/data/.
The frontend goes on Vercel and the backend on Render, both on free plans.
Render's free plan puts the backend to sleep after 15 idle minutes, and waking
it takes up to a minute. To hide most of that wait, the frontend pings
/api/health as soon as any page loads (state/BackendContext.tsx). While the
backend wakes, it shows a small "starting the demo server" notice, and data
loads once the backend answers.
Backend: Render. The Dockerfile runs the backend with DEMO_MODE=true,
which:
- loads the sample files on startup, and again after a reset;
- limits chat to 8 turns per visitor per hour and 150 per day in total
(
DEMO_CHAT_PER_IP_PER_HOUR,DEMO_CHAT_PER_DAY); - refuses uploads over 2 MB (
DEMO_MAX_UPLOAD_BYTES).
The database lives inside the container, so it starts fresh every time the backend wakes up.
- In Render, choose New → Blueprint and pick this repo. It reads
render.yaml. - Fill in
OPENROUTER_API_KEY,MODEL_ORCHESTRATOR(a cheap model that supports tool calling), andCORS_ORIGINS, your Vercel URL as a JSON list:["https://ledgerline.vercel.app"].
Frontend: Vercel.
- Import the repo and set Root Directory to
frontend. Vercel detects Vite. - Add the environment variable
VITE_API_URLwith your Render URL, e.g.https://ledgerline-api.onrender.com. It's read at build time, so redeploy after changing it.
Both redeploy on every push to main.
There's no login. Set a spending limit on your OpenRouter key before sharing the link.
To run the backend container locally:
docker build -t ledgerline .
docker run -p 7860:7860 -e OPENROUTER_API_KEY=... ledgerlineIt also serves the built frontend, so http://localhost:7860 is the whole app.
| Layer | Choice |
|---|---|
| Backend | FastAPI + uvicorn, Python 3.12, managed with uv |
| Database | SQLite (WAL mode). Each uploaded CSV becomes its own table |
| Agent | Google ADK LlmAgent; models through LiteLLM → OpenRouter |
| Streaming | Vercel AI SDK "UI Message Stream" over server-sent events |
| Frontend | Vite 8, React 19, TypeScript, Tailwind 4, @ai-sdk/react |
| Doc | Read it for |
|---|---|
| Architecture | How the pieces fit, what happens in a chat turn, the data model |
| Rule engine | How a match is found, checked and approved, step by step |
| Agent and tools | The six tools the agent can call, and its limits |
| Limitations | What Ledgerline doesn't check, and known bugs |
cd backend
uv run python -m tests.test_duplicates
uv run python -m tests.test_embedded_keys
uv run python -m tests.test_translator_reasoning
uv run python -m tests.test_release_gateNo test runner is installed, so each file runs itself. They also work under
pytest if you add it. Lint with uv run ruff check app tests, and type-check
the frontend with npm run typecheck.
The matching pipeline can be scored against labelled test data ("fixtures"):
cd backend
uv run python -m app.eval --fixture <dir>
uv run python -m app.eval --fixture <dir> --json > eval.json
uv run python -m app.eval --fixture <dir> --max-false-auto-match 0.03 --min-recall 0.95
uv run python -m app.eval --fixture <dir> --agent # runs a real agent turn; costs moneyA fixture is a folder of CSVs plus an expected_reconciliation.csv that says
what the right answer is. Without --agent, no AI model is used. The harness
loads the files into a temporary database, runs the automatic matching, and
compares the result with the expected answer.
The most important number is the false auto-match rate: of the matches approved without a person, how many were wrong. A missed match costs someone an afternoon. A wrong match puts a false figure in front of the person who signs off on the books.
The fixtures are not in this repository, so you need your own to run the evaluation. The last recorded result was 97–100% accuracy with a 0–2.9% false auto-match rate across eleven fixtures.
backend/app/
main.py, config.py app setup and settings (.env)
api/ REST routes and the /api/chat stream
core/ the matching pipeline (no AI here)
ingest.py CSV → its own ds_<id> table
duplicates.py mark repeated rows
discovery.py find columns whose values overlap across files
embedded.py find references hidden inside text, e.g. a narration
automatch.py decide join shape and amount column, build matching SQL
matching.py turn SQL results into proposals; the approval gate
verify.py the independent second check
timing.py learn the normal settlement delay
spine.py decide which file the money flow starts from
transactions.py group matches into end-to-end chains
report.py the close report
sqlguard.py run the agent's SQL read-only
agents/ the AI agent, its six tools, and its callbacks
streaming/ turn agent events into the stream the browser reads
db/ connection, migration runner, migrations/
eval/ the evaluation harness
frontend/src/
routes/ Home · Upload · Workspace · Report
components/ chat panel, matched/source side panels, chain view
state/ React context for datasets, proposals and chat
types/stream.ts TypeScript copy of streaming/protocol.py
demo/ demo data and a script for a live walkthrough