cd /news/ai-tools/i-built-an-autonomous-accounting-too… · home topics ai-tools article
[ARTICLE · art-136545] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

I built an autonomous accounting tool to let AI do my taxes

A developer released Autonomous Accounting, an open-source tool that uses a user-selected vision LLM to convert receipts, invoices and bank statements into a reconciled, categorized ledger plus a downloadable audit binder. The tool runs entirely on one machine, requires Python 3.11+, Node 20+ and PostgreSQL 15+, and supports any OpenAI-compatible vision endpoint including vLLM, llama.cpp, Ollama's /v1 and LM Studio; it hosts no model of its own. Reconciliation uses four matching passes, uncertain items are flagged for human review rather than guessed, and documents are sent only to the endpoint the user configures via LLM_BASE_URL.

read10 min views1 publishedSep 22, 2026
I built an autonomous accounting tool to let AI do my taxes
Image: Michielbdejong (auto-discovered)

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)

<sub>Everything shown is synthetic. python scripts/gen_demo_data.py writes the same month on your machine — see Try the demo month.</sub>

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

  • 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 answer202 ; 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. SetLLM_BASE_URL to a server on your own machine or LAN and nothing leaves. An empty provider key meansoff ; nothing silently falls back to a paid provider, andGET /health shows where an upload would go. If the endpoint is unreachable, jobs park inwaiting_for_model and resume by themselves.

  • No telemetry, no analytics, no webfonts. 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: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.

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

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

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:5174

Peer-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.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:

  1. Set OWN_COMPANY_PATTERNS=example corp in.env and restart, so the invoiceissued by the demo company is read as income and can match the incoming wire.
  2. Sign up, choose Manitoba as the province (the demo documents charge GST and RST), and uploaddemo_bank_cad.csv as the statement.
  3. 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.
  4. 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 inReview , 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.
  5. Approve the pending pairs in Review , then download the audit binder fromReports → January 2026 and openproof_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.
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 pointLLM_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. Howpytest isolates its database, and what the synthetic fixtures do not prove | | Working on it with a coding agent | CLAUDE.md andAGENTS.md are the entry points;.claude/ is an optional Claude Code harness that denies agent reads of.env ,data/ andlogs/ . 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.

── more in #ai-tools 4 stories · sorted by recency
── more on @autonomous accounting 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-an-autonomou…] indexed:0 read:10min 2026-09-22 ·