Self-hosted bookkeeping for a small Canadian corporation: receipts and bank statements in, a reconciled, audit-ready ledger out. Bring your own vision LLM. AGPL-3.0, no telemetry.
Python
0
11 commits
updated Sep 20, 2026
Watch the demo · Quickstart · How it works · Your data · Known issues
Autonomous Accounting turns a pile of receipts, invoices and bank statements into a reconciled, categorized ledger — plus an audit binder with every source document attached. A vision LLM you choose reads the documents, a four-pass matcher pairs them with your bank lines, and anything it is not sure of is flagged for you instead of guessed. Everything runs on one machine you own: no account to create, no vendor to pay, no copy of your books anywhere but your own disk.
Two minutes twenty: one invented company's January, from eight documents and a bank CSV to a downloadable audit binder.
Watch the full demo (2:20, captioned, no sound)
Everything shown is synthetic. python scripts/gen_demo_data.py writes the same month on
your machine — see Try the demo month.
![]() | ![]() | ![]() |
|---|---|---|
| A review queue, not a black box. Exact pairs approve themselves; date gaps and cross-currency pairs wait for you. | A ledger with CRA/GIFI categories. Monthly and annual, with a GST/HST/PST summary and QuickBooks- and Xero-style CSVs. | An audit binder in one ZIP. The XLSX workbook, a self-contained HTML report, and the document behind every line. |
202; a background worker
drains the queue, so dropping half a year of documents at once cannot time out, and a
restart re-queues whatever was mid-flight.config/category_rules.yaml) locks
what it recognises, vendor clustering normalises descriptors, and only the remainder
reaches the model, under a per-run call budget.LLM_BASE_URL to a server on your own
machine or LAN and nothing leaves. An empty provider key means off; nothing silently
falls back to a paid provider, and GET /health shows where an upload would go. If the
endpoint is unreachable, jobs park in waiting_for_model and resume by themselves.The long form — backups, encryption at rest, what reaches a model — is in Honesty about data.
You need Python 3.11+, Node 20+, PostgreSQL 15+ on loopback, and any
OpenAI-compatible, vision-capable endpoint — vLLM, llama.cpp, Ollama's /v1, LM Studio.
This project hosts no model of its own.
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then fill in the three values it names
createdb autonomous_accounting # the order below is not interchangeable
psql -d autonomous_accounting -f db/local_auth_schema.sql
psql -d autonomous_accounting -f db/schema_pg.sql
python -m db.migrate
psql -d autonomous_accounting -f db/local_grants.sql
npm --prefix web ci && npm --prefix web run build
python -m uvicorn server.app:app --port 8080 # then open http://localhost:8080
Registration is closed by default. Set AUTH_ALLOW_SIGNUP=1, restart, sign up, then set it
back to 0. GET /health reports database, storage and LLM reachability.
.env values, and how to generate the secretsGenerate each secret exactly as .env.example says:
python -c "import secrets;print(secrets.token_urlsafe(48))" # AUTH_JWT_SECRET
python -c "import secrets;print(secrets.token_urlsafe(48))" # STORAGE_URL_SECRET
and set DATABASE_URL to the database you are about to create. Every other variable, with
its default, is in .env.example. AUTH_AUTOCONFIRM=1 (the default) means a new account
works immediately without any mail transport.
local_auth_schema.sql must run first: it creates the auth schema, auth.uid() and the
authenticated role, all of which schema_pg.sql needs. schema_pg.sql is the whole
schema — every table, index, trigger and row-level-security policy — as a single baseline;
db/migrations/ ships empty and python -m db.migrate therefore has nothing to do on a
fresh install, but run it anyway so anything added after the baseline is picked up.
local_grants.sql must run last: it grants on the tables the steps above created, and
without it every query fails with permission denied. Each step is idempotent, so
re-running the sequence on an existing database changes nothing. (Nothing in db/ needs a
server feature newer than PostgreSQL 13; 15 is just the oldest release still maintained
upstream.)
FastAPI serves the built bundle from the same origin. For frontend work, run the Vite dev
server instead, which proxies /api to the backend:
cd web
npm ci
npm run dev # http://localhost:5174
The reasonableness engine needs a benchmark table, which is not committed:
python scripts/ingest_ised_benchmarks.py --year 2024 --out config/ised_benchmarks.json
That downloads the Financial Performance Data CSVs published by Innovation, Science and
Economic Development Canada on the Government of Canada Open Government Portal, normalises
them into per-industry cohort cells, validates them fail-loud, and writes a large JSON file
that .gitignore excludes. The data is licensed under the Open Government Licence –
Canada; the generated file and every report built from it carry the attribution the
licence requires. No Open Government Licence data file is distributed with this repository
— you build the table yourself. See NOTICE.
Every run validates structure and plausibility. --anchor anchor.json adds an optional
check that one named cell still carries the figures you expect, so a refresh that quietly
changes what a cell means aborts instead of overwriting your table; with no anchor
supplied, no cell-specific figures are demanded.
--seed-only writes a small table of invented example figures instead, with no download —
enough for the engine and its tests to run, and labelled in its own meta as not being
industry data. Skip the step entirely and the feature stays off; nothing else is affected.
The files under tests/fixtures/ are per-parser unit fixtures; uploaded through the UI they
match nothing, because no receipt among them belongs to any bank line. To see the whole
pipeline work, generate a coherent synthetic month instead:
python scripts/gen_demo_data.py # writes data/demo/, which is gitignored
That writes one invented company's January 2026 — a 12-row CAD statement in the built-in
CSV layout, seven PDFs and one PNG — plus a README.txt saying what each file is there to
show. Then, in the app:
OWN_COMPANY_PATTERNS=example corp in .env and restart, so the invoice issued
by the demo company is read as income and can match the incoming wire.demo_bank_cad.csv as the statement.proof_of_transaction/ and index.html.What that run looks like on one model is written up under Verification gaps in
docs/backlog.md, next to the defects it found.
| Layer | What runs |
|---|---|
| API + web app | FastAPI on uvicorn, port 8080 by default; serves /api/* and the built SPA from one origin |
| Database | PostgreSQL with row-level security keyed on the signed-in user |
| File storage | A directory on local disk, served only through short-lived HMAC-signed URLs |
| Frontend | React 19 + Vite + TypeScript, Tailwind, TanStack Query, Zustand |
| Extraction | Any OpenAI-compatible, vision-capable endpoint you point LLM_BASE_URL at. Gemini and OpenAI are optional fallbacks that stay inert while their keys are empty |
| Auth | Local email + password (server/local_auth.py): HS256 access tokens, rotating refresh tokens, PBKDF2-HMAC-SHA256 hashes. Sign in with Google is optional |
| Bring your own data | The six YAML tables under config/ you are expected to edit, every record shape and status vocabulary, the built-in bank parsers (BMO, Wise, PayPal, Amazon; everything else goes through the LLM statement parser), and the minimum CSV a hand-made statement needs |
| Honesty about data | Where everything sits, what is and is not encrypted, backups, what reaches a model |
| Known issues | Open engineering items. Read Security hardening before exposing an instance beyond loopback: the defaults — one owner, loopback only, registration closed — are what this was reviewed for |
| Testing | ./scripts/gates.sh runs ruff, pytest, typecheck and build — there is no CI. How pytest isolates its database, and what the synthetic fixtures do not prove |
| Working on it with a coding agent | CLAUDE.md and AGENTS.md are the entry points; .claude/ is an optional Claude Code harness that denies agent reads of .env, data/ and logs/. Nothing in the application depends on it |
One thing a stranger will meet early: Gmail's OAuth redirect has to match what you
registered. The code builds <BASE_URL>/api/onboarding/gmail/callback, and BASE_URL
defaults to http://localhost:8080. If you serve the app anywhere else, set BASE_URL (or
GMAIL_REDIRECT_URI) before connecting Gmail, and register the same value with Google.
Gmail, PayPal and Wise gather integrations stay hidden until you configure credentials.
See CONTRIBUTING.md: synthetic fixtures only, ruff and tsc clean, one
topic per pull request, sign off your commits (git commit -s). The most useful
contribution is a statement parser or a category rule for a layout the engine does not read
yet — built from invented data, never real books.
If this saved you a bookkeeping afternoon, a star helps the next small-business owner find it.
Autonomous Accounting — self-hosted bookkeeping with LLM document extraction and bank reconciliation. Copyright (C) 2026 Autonomous Accounting contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Full text in LICENSE. Because this is the AGPL, running a modified version as a network service obliges you to offer its source to its users. Third-party attributions — including PyMuPDF, itself AGPL — are in NOTICE.
11 commits
Python
74.3%
TypeScript
23.5%
Self-hosted bookkeeping for a small Canadian corporation: receipts and bank statements in, a reconciled, audit-ready ledger out. Bring your own vision LLM. AGPL-3.0, no telemetry.
Python
0
11 commits
updated Sep 20, 2026
Watch the demo · Quickstart · How it works · Your data · Known issues
Autonomous Accounting turns a pile of receipts, invoices and bank statements into a reconciled, categorized ledger — plus an audit binder with every source document attached. A vision LLM you choose reads the documents, a four-pass matcher pairs them with your bank lines, and anything it is not sure of is flagged for you instead of guessed. Everything runs on one machine you own: no account to create, no vendor to pay, no copy of your books anywhere but your own disk.
Two minutes twenty: one invented company's January, from eight documents and a bank CSV to a downloadable audit binder.
Watch the full demo (2:20, captioned, no sound)
Everything shown is synthetic. python scripts/gen_demo_data.py writes the same month on
your machine — see Try the demo month.
![]() | ![]() | ![]() |
|---|---|---|
| A review queue, not a black box. Exact pairs approve themselves; date gaps and cross-currency pairs wait for you. | A ledger with CRA/GIFI categories. Monthly and annual, with a GST/HST/PST summary and QuickBooks- and Xero-style CSVs. | An audit binder in one ZIP. The XLSX workbook, a self-contained HTML report, and the document behind every line. |
202; a background worker
drains the queue, so dropping half a year of documents at once cannot time out, and a
restart re-queues whatever was mid-flight.config/category_rules.yaml) locks
what it recognises, vendor clustering normalises descriptors, and only the remainder
reaches the model, under a per-run call budget.LLM_BASE_URL to a server on your own
machine or LAN and nothing leaves. An empty provider key means off; nothing silently
falls back to a paid provider, and GET /health shows where an upload would go. If the
endpoint is unreachable, jobs park in waiting_for_model and resume by themselves.The long form — backups, encryption at rest, what reaches a model — is in Honesty about data.
You need Python 3.11+, Node 20+, PostgreSQL 15+ on loopback, and any
OpenAI-compatible, vision-capable endpoint — vLLM, llama.cpp, Ollama's /v1, LM Studio.
This project hosts no model of its own.
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then fill in the three values it names
createdb autonomous_accounting # the order below is not interchangeable
psql -d autonomous_accounting -f db/local_auth_schema.sql
psql -d autonomous_accounting -f db/schema_pg.sql
python -m db.migrate
psql -d autonomous_accounting -f db/local_grants.sql
npm --prefix web ci && npm --prefix web run build
python -m uvicorn server.app:app --port 8080 # then open http://localhost:8080
Registration is closed by default. Set AUTH_ALLOW_SIGNUP=1, restart, sign up, then set it
back to 0. GET /health reports database, storage and LLM reachability.
.env values, and how to generate the secretsGenerate each secret exactly as .env.example says:
python -c "import secrets;print(secrets.token_urlsafe(48))" # AUTH_JWT_SECRET
python -c "import secrets;print(secrets.token_urlsafe(48))" # STORAGE_URL_SECRET
and set DATABASE_URL to the database you are about to create. Every other variable, with
its default, is in .env.example. AUTH_AUTOCONFIRM=1 (the default) means a new account
works immediately without any mail transport.
local_auth_schema.sql must run first: it creates the auth schema, auth.uid() and the
authenticated role, all of which schema_pg.sql needs. schema_pg.sql is the whole
schema — every table, index, trigger and row-level-security policy — as a single baseline;
db/migrations/ ships empty and python -m db.migrate therefore has nothing to do on a
fresh install, but run it anyway so anything added after the baseline is picked up.
local_grants.sql must run last: it grants on the tables the steps above created, and
without it every query fails with permission denied. Each step is idempotent, so
re-running the sequence on an existing database changes nothing. (Nothing in db/ needs a
server feature newer than PostgreSQL 13; 15 is just the oldest release still maintained
upstream.)
FastAPI serves the built bundle from the same origin. For frontend work, run the Vite dev
server instead, which proxies /api to the backend:
cd web
npm ci
npm run dev # http://localhost:5174
The reasonableness engine needs a benchmark table, which is not committed:
python scripts/ingest_ised_benchmarks.py --year 2024 --out config/ised_benchmarks.json
That downloads the Financial Performance Data CSVs published by Innovation, Science and
Economic Development Canada on the Government of Canada Open Government Portal, normalises
them into per-industry cohort cells, validates them fail-loud, and writes a large JSON file
that .gitignore excludes. The data is licensed under the Open Government Licence –
Canada; the generated file and every report built from it carry the attribution the
licence requires. No Open Government Licence data file is distributed with this repository
— you build the table yourself. See NOTICE.
Every run validates structure and plausibility. --anchor anchor.json adds an optional
check that one named cell still carries the figures you expect, so a refresh that quietly
changes what a cell means aborts instead of overwriting your table; with no anchor
supplied, no cell-specific figures are demanded.
--seed-only writes a small table of invented example figures instead, with no download —
enough for the engine and its tests to run, and labelled in its own meta as not being
industry data. Skip the step entirely and the feature stays off; nothing else is affected.
The files under tests/fixtures/ are per-parser unit fixtures; uploaded through the UI they
match nothing, because no receipt among them belongs to any bank line. To see the whole
pipeline work, generate a coherent synthetic month instead:
python scripts/gen_demo_data.py # writes data/demo/, which is gitignored
That writes one invented company's January 2026 — a 12-row CAD statement in the built-in
CSV layout, seven PDFs and one PNG — plus a README.txt saying what each file is there to
show. Then, in the app:
OWN_COMPANY_PATTERNS=example corp in .env and restart, so the invoice issued
by the demo company is read as income and can match the incoming wire.demo_bank_cad.csv as the statement.proof_of_transaction/ and index.html.What that run looks like on one model is written up under Verification gaps in
docs/backlog.md, next to the defects it found.
| Layer | What runs |
|---|---|
| API + web app | FastAPI on uvicorn, port 8080 by default; serves /api/* and the built SPA from one origin |
| Database | PostgreSQL with row-level security keyed on the signed-in user |
| File storage | A directory on local disk, served only through short-lived HMAC-signed URLs |
| Frontend | React 19 + Vite + TypeScript, Tailwind, TanStack Query, Zustand |
| Extraction | Any OpenAI-compatible, vision-capable endpoint you point LLM_BASE_URL at. Gemini and OpenAI are optional fallbacks that stay inert while their keys are empty |
| Auth | Local email + password (server/local_auth.py): HS256 access tokens, rotating refresh tokens, PBKDF2-HMAC-SHA256 hashes. Sign in with Google is optional |
| Bring your own data | The six YAML tables under config/ you are expected to edit, every record shape and status vocabulary, the built-in bank parsers (BMO, Wise, PayPal, Amazon; everything else goes through the LLM statement parser), and the minimum CSV a hand-made statement needs |
| Honesty about data | Where everything sits, what is and is not encrypted, backups, what reaches a model |
| Known issues | Open engineering items. Read Security hardening before exposing an instance beyond loopback: the defaults — one owner, loopback only, registration closed — are what this was reviewed for |
| Testing | ./scripts/gates.sh runs ruff, pytest, typecheck and build — there is no CI. How pytest isolates its database, and what the synthetic fixtures do not prove |
| Working on it with a coding agent | CLAUDE.md and AGENTS.md are the entry points; .claude/ is an optional Claude Code harness that denies agent reads of .env, data/ and logs/. Nothing in the application depends on it |
One thing a stranger will meet early: Gmail's OAuth redirect has to match what you
registered. The code builds <BASE_URL>/api/onboarding/gmail/callback, and BASE_URL
defaults to http://localhost:8080. If you serve the app anywhere else, set BASE_URL (or
GMAIL_REDIRECT_URI) before connecting Gmail, and register the same value with Google.
Gmail, PayPal and Wise gather integrations stay hidden until you configure credentials.
See CONTRIBUTING.md: synthetic fixtures only, ruff and tsc clean, one
topic per pull request, sign off your commits (git commit -s). The most useful
contribution is a statement parser or a category rule for a layout the engine does not read
yet — built from invented data, never real books.
If this saved you a bookkeeping afternoon, a star helps the next small-business owner find it.
Autonomous Accounting — self-hosted bookkeeping with LLM document extraction and bank reconciliation. Copyright (C) 2026 Autonomous Accounting contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
Full text in LICENSE. Because this is the AGPL, running a modified version as a network service obliges you to offer its source to its users. Third-party attributions — including PyMuPDF, itself AGPL — are in NOTICE.
11 commits
Python
74.3%
TypeScript
23.5%