diff --git a/crates/ledger-cli/src/main.rs b/crates/ledger-cli/src/main.rs index de388d6..fcf2cbd 100644 --- a/crates/ledger-cli/src/main.rs +++ b/crates/ledger-cli/src/main.rs @@ -117,6 +117,49 @@ anything. Keep it conversational; ask a few things at a time. migration done. 7. Tell them about `just gui`, then commit with `just backup`. +## Receipts from email + +If the human has connected their email (a Gmail or Outlook connector, or a +browser extension with their mail open), find receipts there yourself: +search for the vendor and amount around the transaction date, then file +one file per receipt — the PDF attachment if there is one; otherwise save +the message and render it to PDF. Never file a raw `.eml`. Ask before +downloading anything, and never type a password or account number into a +form. + +## Switching from QuickBooks, Xero, or a spreadsheet + +1. Ask the human to export, from the old system, for every year they want + to bring over: the chart of accounts, the general journal (or general + ledger), and the trial balance, profit and loss, and balance sheet. CSV + or XLSX. Receipts usually live as attachments; export those too. +2. Set up the chart (`books.import_chart_csv` or account by account), then + rebuild the history as entries — or, if they only want a fresh start, one + opening entry from the old trial balance at the cutover date. +3. Diff your trial balance, P&L, balance sheet, and cash flow against the + old system's reports for the same dates, **to the cent**, and show the + human the comparison. Not done until every number matches. +4. Keep the old system read-only until a full month has been kept in + wanderbooks and closed cleanly. Then they can cancel it. + +Bank feeds aren't automatic here: import the bank's CSV export +(`books.import_bank_csv`, drafts to review) or read their statements. + +## Working with the CPA + +- At year end (or whenever the CPA asks), build the package with + `books.export_cpa_package`: every statement as PDF and XLSX, the tax + summary, the general ledger and journal, a QuickBooks Desktop IIF + journal, the 1099 worksheet, and every receipt with an index. Save it + where the human can send it. +- Before that: every expense and revenue account has a tax category + (`books.tax_categories`), the period's entries all have receipts, and + `books.verify` is clean. +- A CPA who wants to look around can get a read-only copy of this + directory (it's a git repository: push it to a private remote and give + them read access) and open it with `wanderbooks gui`, or `ledger-gui + --data-dir books-data`. Nothing they do there can change the books. + ## Ongoing - Every transaction: draft, preview, confirm with the human, post, attach @@ -125,6 +168,9 @@ anything. Keep it conversational; ask a few things at a time. lately." Offer it; don't make the human ask. - Reports are pure functions of the journal. If a number looks wrong, the journal is the evidence — read it, don't guess. +- Integrity: `books.verify` checks the journal's hash chain, the database + against the journal (rebuilding it if it drifted), and every stored + file. Run it when a number looks wrong or the human asks. - Back up: `just backup` after every session that posted entries. The git history of this directory is the archive; `books-data/journal.jsonl` is the system of record and the SQLite database is disposable. diff --git a/crates/ledger-gui/src/app.rs b/crates/ledger-gui/src/app.rs index 7650e61..86323b5 100644 --- a/crates/ledger-gui/src/app.rs +++ b/crates/ledger-gui/src/app.rs @@ -376,7 +376,7 @@ fn live_status(ui: &mut Ui, b: &Books, since_update: f64, live: bool) { let (ok, text, tip) = match &b.integrity.disk { DiskCheck::Matches => (true, "Database matches journal".to_string(), "The server's SQLite database matches a fresh projection of the journal, every table, row for row.".to_string()), DiskCheck::Behind { applied, journal } => (true, "Database catching up".to_string(), format!("The database has applied {applied} of {journal} journal events; the server applies the rest right after writing them.")), - DiskCheck::Missing => (true, "Database will rebuild".to_string(), "No database file yet; the server builds it from the journal when it starts.".to_string()), + DiskCheck::Missing => (true, "Read-only copy".to_string(), "No server database here: a copy of the books (a CPA's, say), or the server hasn't run yet. The journal and every file are verified either way.".to_string()), DiskCheck::Differs(why) => (false, "Database differs".to_string(), format!("{why}. The server rebuilds it from the journal (books.verify, or on its next start).")), DiskCheck::Unreadable(why) => (false, "Database unreadable".to_string(), why.clone()), }; diff --git a/crates/ledger-mcp/src/server.rs b/crates/ledger-mcp/src/server.rs index 21cba84..91ae204 100644 --- a/crates/ledger-mcp/src/server.rs +++ b/crates/ledger-mcp/src/server.rs @@ -3328,7 +3328,7 @@ impl BooksServer { #[tool( name = "books.export_cpa_package", - description = "Build the CPA handoff package as a zip (base64): the profit and loss, balance sheet, cash flow, statement of changes in members' equity (multi-member LLCs), and trial balance, each as PDF and XLSX; plus the general ledger (by account) and general journal (entry by entry) as XLSX. cash_accounts (e.g. [\"1000\"]) is required: the cash flow statement reconciles to them. as_of defaults to today; period reports cover the fiscal year to date.", + description = "Build the CPA handoff package as a zip (base64): the profit and loss, balance sheet, cash flow, statement of changes in members' equity (multi-member LLCs), tax summary, and trial balance, each as PDF and XLSX; the general ledger (by account) and general journal (entry by entry) as XLSX; the journal as IIF (imports into QuickBooks Desktop); the 1099 worksheet as CSV; every receipt behind the year's entries (receipts/, with an index.csv tying each file to its entry) and the company's documents (documents/), unless include_receipts is false; and a README with the journal's verified hash. cash_accounts (e.g. [\"1000\"]) is required: the cash flow statement reconciles to them. as_of defaults to today; period reports cover the fiscal year to date.", annotations(read_only_hint = true) )] async fn export_cpa_package( @@ -3418,7 +3418,107 @@ impl BooksServer { "general-journal.xlsx".into(), xlsx(XlsxReport::GeneralJournal(ledger, fy, as_of))?, )); + // 1099 worksheet for the calendar year of as_of. + let year = chrono::Datelike::year(&as_of); + let r1099 = ledger_core::vendor_1099_report(ledger, year, None); + let mut w = String::from("vendor,form,w9_on_file,paid,reportable\n"); + for v in &r1099.vendors { + w.push_str(&format!( + "\"{}\",{},{},{},{}\n", + v.name.replace('"', "\"\""), + v.form.map(|f| f.label()).unwrap_or(""), + v.tin_on_file, + money(v.paid), + v.reportable + )); + } + files.push((format!("1099-worksheet-{year}.csv"), w.into_bytes())); + // The evidence: every current receipt on the year's posted entries, + // and the company's documents, with an index tying them together. + let mut receipt_count = 0; + if p.include_receipts { + let clean = |s: &str| -> String { + s.chars() + .map(|c| { + if c.is_alphanumeric() || ".-_ ".contains(c) { + c + } else { + '_' + } + }) + .collect() + }; + let mut index = String::from("entry_id,date,description,amount,file,sha256\n"); + for a in store.list_attachments(None).map_err(|e| e.to_string())? { + if a.withdrawn_at.is_some() { + continue; + } + let bytes = store.attachment_bytes(a.id).map_err(|e| e.to_string())?; + match a.entry_id.and_then(|e| ledger.get_entry(e)) { + Some(e) + if e.status() == ledger_core::EntryStatus::Posted + && e.date() >= fy + && e.date() <= as_of => + { + let name = format!( + "receipts/{}-entry-{}-{}", + e.date(), + e.id(), + clean(&a.file_name) + ); + let amount: Decimal = e + .lines() + .iter() + .filter(|l| l.side == Side::Debit) + .map(|l| l.amount.amount) + .sum(); + index.push_str(&format!( + "{},{},\"{}\",{},{},{}\n", + e.id(), + e.date(), + e.description().replace('"', "\"\""), + money(amount), + name, + a.blob_sha + )); + files.push((name, bytes)); + receipt_count += 1; + } + None if a.entry_id.is_none() && a.category != "receipts" => { + files.push(( + format!("documents/{}/{}", clean(&a.category), clean(&a.file_name)), + bytes, + )); + } + _ => {} + } + } + files.push(("receipts/index.csv".into(), index.into_bytes())); + } + let seq = store.journal_seq(); drop(store); + files.push(( + "general-journal.iif".into(), + self.export_iif()?.into_bytes(), + )); + let readme = format!( + "{name} — books package for the CPA\n\nFiscal year to date: {fy_start} through {as_of_s}. {basis} basis.\nKept in wanderbooks: double-entry books from an append-only, hash-chained journal ({seq} events, verified).\n\nStatements (PDF and XLSX): profit and loss, balance sheet, cash flow, {members}tax summary, trial balance.\nLedgers (XLSX): general ledger by account; general journal entry by entry.\ngeneral-journal.iif: every posted entry, for import into QuickBooks Desktop.\n1099-worksheet-{year}.csv: cash paid per vendor marked for a 1099, against the year's threshold.\n{receipts}\nThe tax summary maps accounts to return categories; it's a worksheet, not a filing. Questions go to the owners, who can ask their bookkeeping agent to trace any number to its entries and receipts.\n", + name = self.company.name, + basis = self.company.basis, + members = if self.company.members.is_empty() { + "" + } else { + "members' equity, " + }, + receipts = if p.include_receipts { + format!( + "receipts/: the {receipt_count} receipts behind the period's entries; index.csv ties each to its entry, date, amount, and SHA-256.\ndocuments/: the company's formation, legal, and tax documents.\n" + ) + } else { + String::new() + }, + ); + files.insert(0, ("README.txt".into(), readme.into_bytes())); // Zip it. let mut zip_buf = Vec::new(); diff --git a/crates/ledger-mcp/src/types.rs b/crates/ledger-mcp/src/types.rs index 4346bf1..b37423e 100644 --- a/crates/ledger-mcp/src/types.rs +++ b/crates/ledger-mcp/src/types.rs @@ -1917,6 +1917,10 @@ pub struct ExportCpaPackageParams { /// empty: liabilities are operating working capital. #[serde(default)] pub financing_accounts: Vec, + /// Include every receipt for the year's entries and the company's + /// documents (default true). + #[serde(default = "default_true")] + pub include_receipts: bool, } /// Output of `books.export_cpa_package`. diff --git a/crates/ledger-mcp/tests/mcp_stdio.rs b/crates/ledger-mcp/tests/mcp_stdio.rs index af03050..e22d36c 100644 --- a/crates/ledger-mcp/tests/mcp_stdio.rs +++ b/crates/ledger-mcp/tests/mcp_stdio.rs @@ -871,13 +871,18 @@ fn mcp_statement_pdfs_and_cpa_package_over_stdio() { .map(|i| zip.by_index(i).unwrap().name().unwrap().to_string()) .collect(); names.sort(); + let (receipts, rest): (Vec<&String>, Vec<&String>) = + names.iter().partition(|n| n.starts_with("receipts/")); assert_eq!( - names, + rest, [ + "1099-worksheet-2026.csv", + "README.txt", "balance-sheet.pdf", "balance-sheet.xlsx", "cash-flow.pdf", "cash-flow.xlsx", + "general-journal.iif", "general-journal.xlsx", "general-ledger.xlsx", "profit-and-loss.pdf", @@ -888,9 +893,36 @@ fn mcp_statement_pdfs_and_cpa_package_over_stdio() { "trial-balance.xlsx" ] ); + // Every receipt behind the period's entries, plus an index tying them + // to their entries. + assert!(receipts.iter().any(|n| *n == "receipts/index.csv")); + let files = receipts.len() - 1; + assert!(files > 0, "receipts included"); + let mut index = String::new(); + std::io::Read::read_to_string(&mut zip.by_name("receipts/index.csv").unwrap(), &mut index) + .unwrap(); + assert_eq!( + index.lines().count(), + files + 1, + "one index row per receipt" + ); + let mut readme = String::new(); + std::io::Read::read_to_string(&mut zip.by_name("README.txt").unwrap(), &mut readme).unwrap(); + assert!(readme.contains("hash-chained journal"), "{readme}"); let mut cf = Vec::new(); std::io::Read::read_to_end(&mut zip.by_name("cash-flow.pdf").unwrap(), &mut cf).unwrap(); assert!(cf.starts_with(b"%PDF")); + // Without receipts, a lighter package. + let light = mcp.call( + "books.export_cpa_package", + json!({"as_of": "2026-10-31", "cash_accounts": ["1000"], "include_receipts": false}), + ); + let light = zip::ZipArchive::new(std::io::Cursor::new( + b64.decode(light["content_base64"].as_str().unwrap()) + .unwrap(), + )) + .unwrap(); + assert_eq!(light.len(), rest.len()); } #[test] diff --git a/plan/STATUS.md b/plan/STATUS.md index 4a5c9f8..f7b51f7 100644 --- a/plan/STATUS.md +++ b/plan/STATUS.md @@ -285,3 +285,14 @@ interface is MCP, not a cloned GUI. The first storage + MCP slice is done directory fsync off Unix), .exe-aware binary lookup, installer OS and C-compiler checks with WANDERBOOKS_REPO, the website (site/, `just site`, `just publish`), and a repo justfile (check, build, install, demo). +- 2026-10-12: The CPA story. The CPA package now also carries every + receipt behind the period's entries (receipts/, with index.csv tying each + to its entry, date, amount, and SHA-256), the company's documents, the + journal as IIF (QuickBooks Desktop), the 1099 worksheet, and a README + with the verified journal size; include_receipts=false for a light one. + The scaffolded AGENTS.md teaches the bookkeeper to find receipts in + email, migrate from QuickBooks/Xero/spreadsheets (match every statement + to the cent; keep the old system read-only for a month), and work with + the CPA (package, read-only clone + GUI). The site leads with owning + your books and an AI bookkeeper: example prompts, a QuickBooks + comparison, an honest "what it doesn't do yet", the CPA section, FAQ. diff --git a/site/index.html b/site/index.html index 427a9c6..98a76c4 100644 --- a/site/index.html +++ b/site/index.html @@ -3,8 +3,8 @@ -wanderbooks — bookkeeping you talk to - +wanderbooks — own your books, hire an AI bookkeeper + @@ -12,17 +12,17 @@ :root { --bg: #f6f6f4; --surface: #fff; --border: #e4e4e0; --text: #1b1b1f; --muted: #67676f; --faint: #9c9ca4; --accent: #4f46e5; --accent-soft: #eeeefd; - --code-bg: #1b1b1f; --code-text: #ececee; --positive: #15803d; + --code-bg: #1b1b1f; --code-text: #ececee; --positive: #15803d; --negative: #b91c1c; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #111214; --surface: #191a1d; --border: #2a2b30; --text: #ececee; --muted: #a3a3ac; --faint: #6e6e78; --accent: #8b93fa; --accent-soft: #242545; - --code-bg: #0b0b0d; --code-text: #ececee; --positive: #4ade80; + --code-bg: #0b0b0d; --code-text: #ececee; --positive: #4ade80; --negative: #f87171; } } * { box-sizing: border-box; } -html { -webkit-text-size-adjust: 100%; } +html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; } body { margin: 0; background: var(--bg); color: var(--text); font: 400 17px/1.6 Inter, system-ui, -apple-system, "Segoe UI", sans-serif; @@ -33,11 +33,14 @@ a:hover { text-decoration: underline; } .wrap { max-width: 1120px; margin: 0 auto; padding: 0 24px; } header { display: flex; align-items: center; justify-content: space-between; padding: 22px 0; } .brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 17px; color: var(--text); } +.brand:hover { text-decoration: none; } .brand .mark { width: 28px; height: 28px; border-radius: 8px; background: var(--accent); color: #fff; display: grid; place-items: center; font-weight: 700; } nav a { color: var(--muted); margin-left: 22px; font-size: 15px; } -.hero { padding: 56px 0 36px; max-width: 820px; } -h1 { font-size: clamp(38px, 6vw, 60px); line-height: 1.05; letter-spacing: -0.03em; margin: 0 0 20px; font-weight: 700; } -.lede { font-size: clamp(18px, 2.2vw, 21px); color: var(--muted); margin: 0 0 30px; max-width: 700px; } +.hero { padding: 48px 0 32px; max-width: 840px; } +.eyebrow { color: var(--accent); font-weight: 600; font-size: 14px; letter-spacing: .05em; text-transform: uppercase; margin-bottom: 14px; } +h1 { font-size: clamp(38px, 6vw, 62px); line-height: 1.04; letter-spacing: -0.03em; margin: 0 0 20px; font-weight: 700; } +.lede { font-size: clamp(18px, 2.2vw, 21px); color: var(--muted); margin: 0 0 30px; max-width: 720px; } +.lede b { color: var(--text); font-weight: 600; } .install { background: var(--code-bg); color: var(--code-text); border-radius: 12px; padding: 16px 18px; display: flex; gap: 14px; align-items: flex-start; font: 500 14.5px/1.55 ui-monospace, "SF Mono", Menlo, Consolas, monospace; } .install code { flex: 1; min-width: 0; white-space: normal; overflow-wrap: anywhere; } .install .prompt { color: #8b93fa; user-select: none; } @@ -49,27 +52,49 @@ h1 { font-size: clamp(38px, 6vw, 60px); line-height: 1.05; letter-spacing: -0.03 .shot.dark { display: none; } @media (prefers-color-scheme: dark) { .shot.light { display: none; } .shot.dark { display: block; } } .caption { color: var(--faint); font-size: 14px; text-align: center; margin: 10px 0 0; } -section { padding: 64px 0 8px; } -h2 { font-size: clamp(26px, 3.4vw, 34px); letter-spacing: -0.02em; line-height: 1.15; margin: 0 0 14px; } -.sub { color: var(--muted); max-width: 720px; margin: 0 0 32px; } +section { padding: 72px 0 8px; } +h2 { font-size: clamp(27px, 3.6vw, 38px); letter-spacing: -0.02em; line-height: 1.12; margin: 0 0 14px; } +.sub { color: var(--muted); max-width: 740px; margin: 0 0 32px; font-size: 18px; } .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; } +.grid.two-col { grid-template-columns: repeat(2, 1fr); } .card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 22px; } .card h3 { margin: 0 0 8px; font-size: 18px; } .card p { margin: 0; color: var(--muted); font-size: 15.5px; } .card .k { color: var(--accent); font-weight: 600; font-size: 13px; letter-spacing: .04em; text-transform: uppercase; margin-bottom: 8px; } -.two { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: center; } +.ask { display: flex; flex-direction: column; gap: 10px; } +.ask .say { font-size: 16.5px; font-weight: 500; color: var(--text); line-height: 1.45; } +.ask .say::before { content: "“"; color: var(--accent); font-weight: 700; margin-right: 1px; } +.ask .say::after { content: "”"; color: var(--accent); font-weight: 700; } +.ask p { font-size: 14.5px; } +.two { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: center; } .two .shot { margin: 0; } -ul.ticks { list-style: none; padding: 0; margin: 0; } -ul.ticks li { padding: 7px 0 7px 28px; position: relative; color: var(--muted); } -ul.ticks li b { color: var(--text); font-weight: 600; } +ul.ticks, ul.crosses { list-style: none; padding: 0; margin: 0; } +ul.ticks li, ul.crosses li { padding: 7px 0 7px 28px; position: relative; color: var(--muted); } +ul.ticks li b, ul.crosses li b { color: var(--text); font-weight: 600; } ul.ticks li::before { content: "✓"; position: absolute; left: 0; color: var(--positive); font-weight: 700; } -.platforms { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; } -.platforms .card p code { font-size: 13.5px; } +ul.crosses li::before { content: "–"; position: absolute; left: 4px; color: var(--faint); font-weight: 700; } +table.vs { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; font-size: 15.5px; } +table.vs th, table.vs td { text-align: left; padding: 14px 18px; vertical-align: top; border-bottom: 1px solid var(--border); } +table.vs tr:last-child td { border-bottom: 0; } +table.vs th { font-size: 13px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 600; background: var(--bg); } +table.vs td:first-child { color: var(--muted); width: 22%; font-weight: 500; } +table.vs td:nth-child(2) { color: var(--text); width: 42%; } +table.vs td:nth-child(3) { color: var(--muted); } +.callout { border-left: 3px solid var(--accent); padding: 4px 0 4px 18px; margin: 28px 0 0; color: var(--muted); font-size: 17px; max-width: 760px; } +.callout b { color: var(--text); } +details { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; margin-bottom: 12px; } +details summary { cursor: pointer; font-weight: 600; font-size: 16.5px; list-style: none; } +details summary::-webkit-details-marker { display: none; } +details summary::after { content: "+"; float: right; color: var(--faint); font-weight: 400; } +details[open] summary::after { content: "–"; } +details p { color: var(--muted); margin: 12px 0 0; font-size: 15.5px; } code.inline { font: 500 14px ui-monospace, Menlo, Consolas, monospace; background: var(--accent-soft); padding: 2px 6px; border-radius: 6px; color: var(--text); } -footer { padding: 64px 0 48px; color: var(--faint); font-size: 14px; display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; } +footer { padding: 72px 0 48px; color: var(--faint); font-size: 14px; display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; } @media (max-width: 860px) { - .grid, .platforms, .two { grid-template-columns: 1fr; } + .grid, .grid.two-col, .two { grid-template-columns: 1fr; } nav { display: none; } + table.vs td:first-child { width: auto; } + table.vs th, table.vs td { padding: 12px; } } @@ -78,73 +103,145 @@ footer { padding: 64px 0 48px; color: var(--faint); font-size: 14px; display: fl
wwanderbooks
-

Bookkeeping you talk to.

-

wanderbooks is real double-entry accounting run by an AI bookkeeper. You tell it what happened and hand it receipts; it keeps the books, balanced to the cent. You watch every number, live, and click through any of them to the receipt behind it.

-
+
Stop renting your books
+

Own your books. Hire an AI bookkeeper.

+

wanderbooks is real double-entry accounting that lives in a folder you own, kept by an AI bookkeeper you talk to. It finds your receipts, records and categorizes everything, fixes its mistakes the way an accountant would, and hands your CPA a clean package. No subscription. No lock-in. Every number traceable to its receipt.

+
$ curl -fsSL https://git.swaits.com/swaits/wanderbooks/raw/branch/main/install.sh | sh -s -- ~/Books/YourCompany
-

One line: installs everything, makes your books, and starts your bookkeeper. Linux and macOS; Windows via WSL2.

+

One line installs everything, makes your books, and starts your bookkeeper. Linux and macOS; Windows via WSL2. Free and open source; you bring Claude.

The wanderbooks overview: cash, what you owe, spending, revenue, runway, a cash chart, and what needs attention
The wanderbooks overview in dark mode
-

The overview of a demo company. It leads with the answer, then the evidence.

+

The desktop view of a demo company. It leads with the answer, then the evidence.

-
-

You talk. It keeps the books. You see everything.

-

No forms, no categories to hunt for, no month-end scramble. The bookkeeper works through an accounting engine that refuses to be wrong, and the books are always open in front of you.

+
+

Just ask.

+

Your bookkeeper is Claude, working through an accounting engine that won't let it be wrong. Talk to it the way you'd talk to a good bookkeeper. It does the work, shows you the result, and waits for your OK before anything is posted.

-
Talk

Your bookkeeper is an AI agent

“Record the Apple developer fee, the receipt's in my email.” It drafts the entry, shows you the preview, attaches the receipt, and posts when you say so. Claude Code, via MCP.

-
See

A desktop view of everything

Cash and runway, profit by month, every transaction with its rendered receipt, statements an accountant would sign off on. Changes the agent makes appear in milliseconds.

-
Own

Your books are files you keep

An append-only journal, your receipts, and a config file, in a folder under git. No subscription, no lock-in, no cloud you don't control.

+
Find the receipt for the $106.38 Apple charge in my email and record it.

Searches your inbox, files the receipt as a PDF, drafts the entry with the right accounts, and shows you the preview.

+
Here's last month's bank export. Categorize everything.

Imports the transactions as drafts, proposes an account for each from how you've booked them before, and asks about the ones it isn't sure of.

+
The domain renewal went to the wrong account. Fix it.

Reverses the entry and records it correctly. Nothing is ever erased; the history shows exactly what changed and why.

+
Why did software spending jump in March?

Traces the number to the entries and receipts behind it, and tells you what's new.

+
Can we afford to hire someone at $6,000 a month?

Works it out from your actual cash, burn, and what's coming in, and shows its math.

+
We paid the designer $2,400 this year. Does she need a 1099?

Checks the threshold for the year, whether payments were by card, and whether her W-9 is on file.

+
Close September.

Checks for missing receipts and unposted drafts, reconciles to your bank statement, posts depreciation and amortization, and closes the period.

+
Get Krista everything she needs for our tax return.

Builds the CPA package: every statement, the ledgers, the tax summary, the 1099 worksheet, and every receipt, indexed.

+
Move us off QuickBooks.

Rebuilds your history from QuickBooks' exports and checks every statement against QuickBooks, to the cent, before you cancel.

+
+
+ +
+

Why not QuickBooks Online?

+

QuickBooks rents you access to your own financial history, raises the rent every year, and makes leaving painful. You still do the bookkeeping yourself, in forms. wanderbooks turns that around.

+ + + + + + + + +
wanderbooksQuickBooks Online
Where your books liveA folder on your computer, under git. Plain, documented files.Intuit's servers. You get access while you pay.
What it costsNothing. Open source, forever. You bring your Claude plan.A monthly subscription per company, and the price keeps rising.
Who does the bookkeepingAn AI bookkeeper: receipts, entries, categories, fixes, closes.You do, in screens and forms. Or you pay someone.
ReceiptsRequired over a threshold you set, enforced on every entry, shown right beside it.Optional attachments.
Audit trailEvery change is a hash-chained event; nothing is ever edited or erased.An audit log inside their app.
LeavingYour files are the books. Standard exports any time, including QuickBooks formats.Export what you can, and hope.
+

Can it really replace QuickBooks? For most small businesses, yes. Here's exactly what you get, and what you don't (yet).

+
+
+

What it does

+
    +
  • Real double-entry books: chart of accounts, journal, general ledger, trial balance.
  • +
  • Statements: P&L, balance sheet, cash flow, owners' equity; by month, quarter, or year; with comparisons.
  • +
  • Invoices and bills, with receivables and payables aging.
  • +
  • Tax-ready: tax categories for Schedule C, 1065, 1120-S, and 1120; a 1099 worksheet.
  • +
  • The rest of a real business: fixed assets and depreciation, prepaid expenses and deferred revenue, recurring entries, platform payouts, classes, foreign-currency lines, period close, bank reconciliation.
  • +
+
+
+

What it doesn't (yet)

+
    +
  • Automatic bank feeds. Hand it your bank's CSV export or statements; it does the rest.
  • +
  • Payroll processing. It records payroll you run elsewhere (Gusto, ADP), taxes and all.
  • +
  • Sending invoices and taking payments. It tracks them; your payment processor collects.
  • +
  • Logins for your team. Others get a read-only copy (below) or their own session with the bookkeeper.
  • +
  • Point-and-click setup. Today you run one command in a terminal. After that it's plain English.
  • +
+
+
+
+ +
+
+
+

“But my CPA works in QuickBooks.”

+

Your CPA doesn't need QuickBooks. They need your numbers, the evidence behind them, and files their software can read. wanderbooks gives them more than a QuickBooks login ever did.

+
    +
  • One package, ready to send: P&L, balance sheet, cash flow, equity, and trial balance as PDF and Excel; the general ledger and journal; the tax summary.
  • +
  • Every receipt behind the year's entries, indexed by entry, date, and amount.
  • +
  • QuickBooks-compatible: the journal as an IIF file that imports into QuickBooks Desktop, if that's where their workflow lives.
  • +
  • The 1099 worksheet, with W-9 status.
  • +
  • A live, read-only view if they want to look around: share the books folder privately and they open it in the desktop view. Nothing they do can change your books.
  • +
+
+
A profit and loss statement in wanderbooks with interval columns and drill-down
+
A transaction with its lines, its rendered receipt, and its history

Every number goes all the way down.

-

Click an amount on a statement and you get the transactions behind it, for exactly those dates. Click a transaction and you get its lines, the receipt itself, and every event in its history.

+

The desktop view is read-only and live: whatever your bookkeeper posts appears in a fraction of a second. Click any amount on a statement for the transactions behind it. Click a transaction for its lines, the receipt itself, and every event in its history.

    -
  • Statements an accountant expects: profit and loss, balance sheet, cash flow, owners' equity, trial balance, general ledger.
  • -
  • By month, quarter, or year, compared with the prior period, each line as a share of income.
  • -
  • Tax-ready: tax categories by return (Schedule C, 1065, 1120-S, 1120), a 1099 worksheet, receivables and payables aging.
  • -
  • Real-business features: invoices and bills, fixed assets and depreciation, prepaid expenses, recurring entries, platform payouts grossed up, classes, multi-currency lines.
  • +
  • Answers first: cash, runway, profit, and what needs attention, in a sentence.
  • +
  • Needs attention finds missing receipts, unposted drafts, overdue invoices, 1099 vendors without a W-9, and hands you the prompt to fix each.
  • +
  • All the data: the full journal, every receipt and document, light and dark.
-
A transaction with its lines, its rendered receipt, and its history

Built so you can trust it.

-

An AI is doing your books, so the books have to be impossible to quietly get wrong.

+

An AI is keeping your books, so the books have to be impossible to quietly get wrong.

-

The engine enforces accounting

Every entry balances exactly; money is exact decimal, never floating point. Posted entries are permanent; corrections are reversing entries. Receipts are required over your threshold, enforced by the server.

-

A tamper-evident journal

Every change is an event, hash-chained to the one before it. The database is rebuilt from the journal and checked against it, table by table; every receipt is checked against its hash. The app shows you the result.

-

You approve what matters

The agent previews every posting before it happens. The desktop view can't change anything; when it spots a problem it hands you a prompt for the agent.

+

The engine enforces accounting

Every entry balances exactly; money is exact decimal, never floating point. Posted entries are permanent; corrections are reversing entries. The receipt rule is enforced by the server, not left to the AI's judgment.

+

A tamper-evident journal

Every change is an event hash-chained to the one before. The database is checked against the journal table by table, and rebuilt if it ever drifts; every receipt is checked against its hash. The desktop view shows you the result.

+

You approve what matters

The bookkeeper previews every posting before it happens and waits for you. The desktop view can't change anything at all.

Install

-

The one line above does everything. It installs Rust if you don't have it, builds wanderbooks, makes your books folder with the bookkeeper wired in, and starts a session to set up your company.

-
+

The one line at the top does everything: installs Rust if you don't have it, builds wanderbooks, makes your books folder with the bookkeeper wired in, and starts a session to set up your company.

+

Linux

Needs a C compiler and git (build-essential on Debian and Ubuntu). The installer tells you if anything's missing.

macOS

Needs Apple's command line tools: xcode-select --install.

Windows

Run it inside WSL2 (wsl --install, then Ubuntu). On Windows 11 the desktop view opens as a normal window. Native Windows is on the way.

-

Already installed? wanderbooks new ~/Books/AnotherCompany. Inside a books folder, just gui opens the desktop view and just agent starts your bookkeeper. You'll need Claude Code.

+

Already installed? wanderbooks new ~/Books/AnotherCompany. In a books folder, just agent starts your bookkeeper and just gui opens the desktop view.

+
+ +
+

Questions

+
What does it cost?

wanderbooks is free and open source (MIT or Apache-2.0). Your bookkeeper is Claude, through Claude Code, which comes with a Claude Pro or Max plan or runs on API credits.

+
Do I need to be technical?

You need to paste one command into a terminal, once. After that you talk to your bookkeeper in plain English and read your books in the desktop app.

+
How does it find my receipts?

Connect your email to Claude (an email connector like Gmail's, or the Claude browser extension with your mail open) and ask. It searches for the vendor and amount, files one clean PDF per receipt, and attaches it to the entry. It asks before downloading anything, and it never types passwords or account numbers.

+
Where's my data, and what does the AI see?

Your books are files on your computer: a journal, your receipts, and a settings file, in a git repository you control. Nothing is uploaded to wanderbooks; there is no wanderbooks server. What your bookkeeper reads to help you goes to Anthropic like any Claude conversation.

+
What if the AI gets something wrong?

Every posting is previewed before it happens. If something slips through, it's corrected the way accountants do it, with a reversing entry, so the mistake and the fix are both on the record. The engine itself refuses anything that doesn't balance, anything without a required receipt, and anything in a closed period.

+
Is this real accounting, or a spreadsheet with an AI on top?

Real double-entry accounting, with the rules enforced in code: exact decimal money, balanced entries, immutable posting, closes, depreciation, accrual or cash basis. Its statements are tested against hand-computed numbers, golden files, and property tests.

+
Can I bring my history from QuickBooks or a spreadsheet?

Yes. Export your chart of accounts, journal, and reports, and ask your bookkeeper to move you over. It rebuilds the history and compares every statement against the old system, to the cent, before calling it done. Keep the old system read-only until you've closed a month in wanderbooks.