This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
Every month, somewhere, a bookkeeper is retyping a bank statement. The bank hands over a PDF; the books want rows in a spreadsheet, or an import file for QuickBooks or Xero. People pay to skip that job. A one-person web app that does nothing but convert bank statements was reported this year at about $38,000 in monthly recurring revenue, and freelance boards carry a steady stream of "PDF statement to Excel" requests.
Two things keep bothering the people who do this work.
The first is that the converters want the statement uploaded. For an accountant, that means sending a client's bank history to a server they know nothing about.
The second is trust. A converter that reads a 1 as a 7 does not tell you. You find out at month end, when the books are six dollars off and you go hunting through two hundred rows.
Tickmark is a web page that turns a bank statement PDF into a spreadsheet on your own computer, and checks every row while it does it. Each amount is added to the running balance, and a row gets a tick only when the balance the bank printed beside it agrees to the cent. A row that does not agree is ringed in red, with the size of the gap and the place on the page to look.
Scanned statements, which have no text to copy, are read by Gemma 4 running on your own graphics card through WebGPU. The model writes the rows down; the same arithmetic then decides which of them are right.
Who pays for this: bookkeepers, accountants and small business owners who convert statements every month, the same people already paying the upload-based converters. The plan is a free open core and paid extras that save hours: a year of statements in one go, rules remembered per client, column mappings for each accounting package.
Demo
Try it: https://luoy16002-svg.github.io/tickmark/
It opens with seven made-up statements from invented banks, printed the different ways real banks print them: one table with a signed amount column; paid in and paid out columns with an overdraft and one balance a day; decimal commas and two date columns; CR and DR markers; sections by kind (deposits, withdrawals, checks, fees) with no running balance; a check-number column with credits, debits and an ending daily balance; and sections with unsigned amounts and a separate daily balance table. There is a scanned one too. Or drop your own PDF on the page. The counter at the bottom shows every request the page makes to another site after you open a file. It stays at 0.
Here I changed one figure by hand, 27.32 to 37.32, to show what a misread looks like. The ledger rings the printed balance in red, says the gap is exactly 10.00, marks the line on the bank's page, and picks up again on the next row:
A scanned statement being read by Gemma 4 in the browser. The frame on the page is the band the model is reading; the rows appear as it writes them:
It works on a phone too, minus the scanning:
Exports: an Excel workbook (a transactions sheet and a reconciliation sheet), CSV, QuickBooks Online CSV, Xero CSV and OFX.
Code
luoy16002-svg
/
tickmark
Bank statements into spreadsheets, checked row by row against the running balance, in your browser. Scans are read by Gemma 4 on your own GPU.
Tickmark
Bank statements into spreadsheets, checked row by row, on your own computer.
Open a PDF statement in the browser. Tickmark copies every transaction into a ledger and adds it to the running balance. A row gets a tick only when the balance the bank printed agrees to the cent; a row that does not agree is ringed in red, with the gap and the place to look. Scanned statements are read by Gemma 4, Google's open model running on your graphics card through WebGPU, and go through the same check, so a figure the model misreads shows up as a row that does not tie instead of slipping into your books.
Nothing is uploaded. The page counts every request it makes to another site and shows the number at the bottom.
What it reads
- Text PDFs as banks issue them: one table or sections by kind (deposits, withdrawals…
How I Built It
Reading the text layer. pdf.js gives every piece of text on a page with its position. Tickmark groups pieces on the same baseline into lines, joins pieces closer than about half a character into cells, finds the header line (Date ... Balance, in a dozen spellings), and assigns every figure to the column whose right edge it lines up with. Dates come as 09/01, 01 Sep 26, SEP 01 or 01.09.2026, so the statement period is read first and each date takes its year from it. Amounts come signed, in separate in and out columns, with CR and DR, in brackets, or with decimal commas, and an overdrawn balance can be printed three different ways. Some banks print one long table; others print a section per kind of transaction with the amounts unsigned and the balances in a separate daily table. Tickmark takes the direction of the money from the section title, lists such rows by date and checks them day by day. When a PDF runs two headings together into one piece of text ("Deposits/Credits Withdrawals/Debits"), it splits them again at the phrases banks use.
The check. The running balance is the bank's own arithmetic, printed on the statement. Tickmark walks the rows, adds each amount, and compares. When a statement prints a balance only at the end of each day, the day's rows are checked together. When an amount is printed without a sign, the balance settles it: if only one choice of in and out makes the day add up, that is the one used. When nothing adds up, the row is flagged and the running balance picks up again from the next printed figure, so one bad row does not turn the rest of the page red.
Gemma 4 in the browser. Transformers.js 4.3 runs Gemma 4 E2B (q4f16) on WebGPU, in a web worker so the page stays responsive. The first time, the browser downloads 3.38 GB of weights from Hugging Face; after that it loads from the browser's cache in about five seconds on my machine (an RTX 5060 Ti).
My first attempt gave it a whole page, and the result was useless: three rows right, then shifted amounts, repeated rows and missed lines. The reason was in the image processor. The web build of the vision encoder turns any picture into 280 tokens, and at that budget a page of 9 point print is a blur. Raising the budget failed twice (first a WebGPU buffer over 2 GB, then an encoder fixed at 280 features), so I went the other way: the page is cut into ten overlapping bands and the model reads one band at a time. At that scale the print is legible.
What comes back still needs care, and every repair is one the arithmetic can prove:
- a row read twice where two bands overlap is dropped, because a real transaction cannot leave the balance unchanged;
- the second printed line of a description, read as a row of its own carrying the line above's balance, is joined back to its row;
- a row that the next row ties without is not on the statement;
- blank paper is never shown to the model, because shown an empty band it sometimes invents an example table.
On the scanned sample (three pages, 64 transactions) Gemma 4 reads every row correctly in about two minutes, and all 64 tie. On the UK sample, which prints one balance a day and leaves many cells empty, it does much worse: in my benchmark it got 23 of 58 rows exactly right. The check still does its job there. Every day that does not add up is marked, in the ledger and in the export, so the errors are in plain sight, but you would be correcting a lot by hand. Reading one row at a time, with the column headings pasted above it, is what I am working on next. The benchmark page in the repository runs the scan pipeline on any sample and scores it against the sample's answer key.
Categories. Plain rules handle the common payees: payment processors, utilities, couriers, fuel, taxes, bank fees. Gemma 4 suggests the rest, one question per payee, and any answer that contradicts the direction of the money (an expense category on money that came in) is thrown away. Suggestions stay in italics until you confirm them, and your choices are remembered for next month.
Proving nothing leaves. The page runs under a strict content security policy, the fonts and the ONNX runtime ship with the site, and a PerformanceObserver in the page and another in the model's worker count every request to another site. Open a statement, export it, and the count is still 0.
Testing. A generator writes statements for seven invented banks in seven layouts, each with an answer key. The test suite parses all seven and compares every date, amount, balance and description with the key (386 rows; all match and all tie), checks that a misread amount is caught on its own row and nowhere else, that signs come from the balance when the statement prints none, and opens every export with an independent reader.
Why Does Open Innovation Matter?
A bank statement is about as private as a document gets. The only way to put a capable reader next to it without sending it somewhere is to run the model where the document already is, and that only works with open weights. Gemma 4 is Apache 2.0, so a static web page can hand it to anyone's browser: no API key, no account, no bill that grows with every page read. A closed API would have made the whole idea impossible, because the first step would be uploading the statement.
Open code matters for the same reason. "We never see your data" is a sentence every converter puts on its homepage. Here you can read the code and watch the request counter while you work.
And it keeps the business honest. With no inference bill, the core can stay free for good. What people would pay for is time saved, not server time.
Prize Categories
Best Use of Gemma: Gemma 4 E2B reads scanned statements and suggests categories, entirely in the browser on WebGPU, with every figure it reads checked by the statement's own arithmetic.




Top comments (0)