CodingIdeas.ai
The Builder's Desk
Build GuideMay 24, 2026·6 min read

How to Build LedgerMatch: A Bookkeeper Reconciliation Copilot in 4 Weeks

Freelance bookkeepers spend 3+ hours every Monday morning matching bank transactions to QuickBooks entries by hand. Here's the exact tech stack, schema, and build order to ship a fuzzy-matching reconciliation tool that 300,000 US freelance bookkeepers would pay $49/month for.

C

codingideas.ai

Tracking what developers complain about so you don't have to.


The pain: Freelance bookkeepers manage 5-20 clients each. Every month-end, they open a spreadsheet, export the client's bank transactions, export QuickBooks entries, and manually match rows. The spreadsheet hasn't changed since 2009. Three hours, every single week, per bookkeeper.

The market: ~300,000 freelance bookkeepers in the US alone. Enterprise tools like BlackLine solve this for $500+/month. Nothing exists for the $49/month tier.

The window: Plaid's transaction API and QuickBooks Online's OAuth API are both stable and well-documented. The matching logic is a fuzzy search problem, not a hard AI problem. This is buildable in 4 weeks with a solo founder.

What You're Building

A web app with three screens:

  1. Client dashboard — list of all clients with reconciliation status (up to date / has unmatched items)
  2. Triage queue — one screen per client showing unmatched transactions ranked by match confidence, with one-click approve
  3. Report export — PDF reconciliation report for client sign-off

No mobile app. No Xero integration (yet). No multi-user team collaboration. Just the core loop: connect bank feed → connect QuickBooks → auto-match → human reviews edge cases → export.

Tech Stack

Next.js 14 (App Router) + TypeScript
Supabase (Postgres + Auth + RLS)
Plaid API — bank transaction feed
QuickBooks Online API — accounting entries
Fuse.js — fuzzy matching engine
Stripe — $49/month subscription
pdf-lib — reconciliation report export
Cursor for matching engine + API routes
v0 for triage queue UI

Schema (Start Here Before Any Code)

-- clients: one row per bookkeeper's client
clients (
  id uuid primary key,
  user_id uuid references auth.users,  -- the bookkeeper
  name text,
  plaid_access_token text,             -- encrypted
  qbo_realm_id text,
  qbo_access_token text,               -- encrypted
  qbo_refresh_token text
)

-- bank transactions from Plaid
bank_transactions (
  id uuid primary key,
  client_id uuid references clients,
  plaid_transaction_id text unique,
  amount numeric,
  date date,
  description text,
  matched boolean default false
)

-- accounting entries from QuickBooks
accounting_entries (
  id uuid primary key,
  client_id uuid references clients,
  qbo_id text unique,
  amount numeric,
  date date,
  memo text,
  matched boolean default false
)

-- confirmed matches
matches (
  id uuid primary key,
  bank_tx_id uuid references bank_transactions,
  acct_entry_id uuid references accounting_entries,
  confidence numeric,   -- 0-1, from Fuse.js
  status text           -- 'auto' | 'approved' | 'rejected'
)

Enable RLS on every table. Gate all rows to auth.uid() = user_id. Do this before you write a single API route.

Build Order

Step 1 — Fuzzy matching engine (lib/fuzzy.ts)

Build this first, before any UI. It's the core value and you want to test it in isolation.

import Fuse from 'fuse.js';

export interface BankTx {
  id: string; amount: number; date: string; description: string;
}
export interface AcctEntry {
  id: string; amount: number; date: string; memo: string;
}
export interface MatchResult {
  bankTxId: string; acctEntryId: string; confidence: number;
}

export function matchTransactions(
  bankTxs: BankTx[],
  acctEntries: AcctEntry[]
): MatchResult[] {
  const results: MatchResult[] = [];

  for (const tx of bankTxs) {
    // Only consider entries within ±3 days and ±2% amount
    const candidates = acctEntries.filter(e => {
      const dayDiff = Math.abs(
        new Date(e.date).getTime() - new Date(tx.date).getTime()
      ) / 86400000;
      const amtDiff = Math.abs(e.amount - tx.amount) / Math.abs(tx.amount);
      return dayDiff <= 3 && amtDiff <= 0.02;
    });

    if (!candidates.length) continue;

    const fuse = new Fuse(candidates, {
      keys: ['memo'],
      includeScore: true,
      threshold: 0.5,
    });

    const hits = fuse.search(tx.description);
    if (hits[0]) {
      results.push({
        bankTxId: tx.id,
        acctEntryId: hits[0].item.id,
        confidence: 1 - (hits[0].score ?? 1),
      });
    }
  }

  return results.sort((a, b) => b.confidence - a.confidence);
}

Test this against 50 real-looking transactions before moving on. The matching quality is your product.

Step 2 — Plaid integration (app/api/plaid/route.ts)

// POST /api/plaid/link-token — creates a Plaid Link token for the frontend
// POST /api/plaid/exchange — exchanges public token for access token, saves to DB
// POST /api/plaid/sync — fetches new transactions since last sync, saves to bank_transactions

Use Plaid's /transactions/sync endpoint (not /transactions/get — sync handles incremental updates correctly). Store the cursor per client.

Cost note: Plaid charges per Item (connected account). At $49/month with bookkeepers managing 15 clients, model your Plaid costs before you price. At ~$0.30/month per Item, a bookkeeper with 15 clients costs you ~$4.50/month in Plaid fees alone.

Step 3 — QuickBooks Online integration (app/api/qbo/route.ts)

// GET /api/qbo/connect — redirects to QBO OAuth
// GET /api/qbo/callback — stores realm_id + tokens
// POST /api/qbo/sync — fetches JournalEntry + Purchase + Deposit entities

Use the node-quickbooks package. QuickBooks tokens expire every hour — implement refresh logic before you think you need it, because you will need it on day 1.

Step 4 — Triage queue UI

Use v0 to generate the base component, then customise. The key UX decisions:

  • Show each bank transaction with its top 3 candidate matches ranked by confidence
  • High confidence (>0.85): show a green "Auto-match" badge — bookkeeper can bulk-approve
  • Medium (0.5–0.85): show candidate with "Approve" button
  • Low (<0.5): show "No match found" with a manual search field

One rule: never auto-approve without a human click, even at 99% confidence. Bookkeepers are legally liable for reconciliation accuracy. The product assists, it doesn't decide.

Step 5 — Stripe billing

Single plan: $49/month. Gate the Plaid connect flow behind an active subscription. Add a 14-day free trial — bookkeepers need to see the match quality on their real data before they pay.

Step 6 — PDF report export

import { PDFDocument, StandardFonts } from 'pdf-lib';

// Generate a 2-page report:
// Page 1: summary (client name, period, matched count, unmatched count, total amounts)
// Page 2: table of all matches with bank description, QBO memo, amount, date, status

This is the deliverable the bookkeeper sends to their client. It's also why they'll pay $49/month — it saves them an hour of report formatting on top of the matching time saved.

Milestone Plan

Week 1–2: Matching engine + triage queue live with seed data (50 synthetic transactions). Confirm match quality before building integrations.

Week 3–4: Plaid and QBO integrations live. Stripe billing. Invite 3 freelance bookkeepers from r/bookkeeping or the QuickBooks ProAdvisor community to test with real client data.

Month 2: Fix the edge cases that real data reveals (split transactions, rounding differences, duplicate entries). Launch in the QuickBooks ProAdvisor forum. Target: 15 paying users.

Pitfalls to Avoid

Don't build Xero before QuickBooks is solid. Every week you're not in front of bookkeepers, someone else is. QBO has ~80% of the US freelance bookkeeper market. Xero is a second product.

Don't skip Plaid cost modelling. A bookkeeper with 20 clients costs you ~$6/month in Plaid fees. At $49/month, that's 12% of revenue before server costs. It changes your unit economics materially.

Your first 10 customers won't come from ads. They'll come from participating in the QuickBooks ProAdvisor community and r/bookkeeping. Budget 3 weeks of community participation before launch, not after.


Built from real pain signals sourced from freelance bookkeeper communities. View the full LedgerMatch idea spec →

Ideas mentioned in this post

500+ more ideas based on real builder pain

Fresh ideas every day. No signup required.

Browse all ideas →
🤖

AI-assisted content. This post was drafted with the help of AI using real signals from Reddit, Hacker News, and App Store reviews. Facts and figures are verified before publishing, but if you spot an error let us know.