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.

- Extraction. Receipts, invoices and PDF bank statements are rasterised and read by a

vision LLM into structured JSON (vendor, date, amount, tax, currency), with totals

cross-checked against line items. Uploads queue and answer 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.

- Reconciliation. Four passes over the unmatched set: strict same-currency, FX-relaxed inside a configurable band, an aggressive pass for splits and installments, and an LLM pass that links across accounts.

- Categorization. A deterministic regex pre-pass (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.

- Reports. Monthly and annual ledgers, a GST/HST/PST tax summary, a PDF report, QuickBooks- and Xero-style CSVs, a shareable read-only link, the ZIP audit binder — and an optional "reasonableness" pass that compares your expense ratios against published Canadian industry data.

- It flags; it does not guess. A wrong number in a tax filing is worse than a blank. Bank fees, interest and internal transfers are excluded rather than force-matched.

- It never sums across currencies. A USD invoice paid in CAD is an FX match for review, never an "exact" one.

- Your documents go only where you point them. Set LLM_BASE_URLto 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, andGET /healthshows where an upload would go. If the endpoint is unreachable, jobs park inwaiting_for_modeland resume by themselves.

- No telemetry, no analytics, no webfonts. Loading a page fetches nothing from anyone else. The log files on your disk are the only record.

- Row-level security on every table, keyed on the signed-in user and enforced from the catalogue by a test.

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:8080Registration 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.

The three .env values, and how to generate the secrets

Generate 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_SECRETand 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.

Why the schema files run in that order

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.)

Developing the frontend with the Vite dev server

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:5174Peer-benchmark data (optional, one command)

The reasonableness engine needs a benchmark table, which is not committed:

python scripts/ingest_ised_benchmarks.py --year 2024 --out config/ised_benchmarks.jsonThat 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 gitignoredThat 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:

- Set OWN_COMPANY_PATTERNS=example corpin.envand restart, so the invoice issued by the demo company is read as income and can match the incoming wire.

- Sign up, choose Manitoba as the province (the demo documents charge GST and RST),

and upload demo_bank_cad.csvas the statement.

- Upload all eight documents at once. They queue; each takes roughly a minute on a 27B-class local vision model, less on a hosted one.

- Press Match Receipts. Expect exact same-day pairs to be auto-approved without a model call, a six-day date gap and a USD invoice paid in CAD to land in Review, the monthly fee, interest and inter-account transfer to be marked as needing no receipt, two bank lines left asking for a document, and a cash receipt left unmatched.

- Approve the pending pairs in Review, then download the audit binder from

Reports → January 2026 and open proof_of_transaction/andindex.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.

- Not a hosted service. Nothing to sign up for. You run it or it does not run.

- Not something you buy. The software takes no payment. Every signed-in account has full access to every feature.

- Not supported. No warranty, no SLA, no security response commitment, no promise that an upgrade will not want a manual migration. See the licence.

- Not a filing service. It produces a ledger and reports for a Canadian small corporation, and models GST/HST/PST only. It files nothing with the CRA and it is not accounting advice. Check the numbers before you use them.

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.