<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Khan</title>
    <description>The latest articles on DEV Community by John Khan (@john_khan_949799213ffa4e7).</description>
    <link>https://dev.to/john_khan_949799213ffa4e7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191572%2F82fe71f1-63ec-40b8-8634-4660e8f0e66e.png</url>
      <title>DEV Community: John Khan</title>
      <link>https://dev.to/john_khan_949799213ffa4e7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/john_khan_949799213ffa4e7"/>
    <language>en</language>
    <item>
      <title>Shoebox: sort a freelancer's bank statement into T2125 lines without uploading it</title>
      <dc:creator>John Khan</dc:creator>
      <pubDate>Sun, 18 Oct 2026 06:32:37 +0000</pubDate>
      <link>https://dev.to/john_khan_949799213ffa4e7/shoebox-sort-a-freelancers-bank-statement-into-t2125-lines-without-uploading-it-3b8h</link>
      <guid>https://dev.to/john_khan_949799213ffa4e7/shoebox-sort-a-freelancers-bank-statement-into-t2125-lines-without-uploading-it-3b8h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every spring, many Canadian freelancers do the same thing. They export a year of bank transactions to a CSV, open it in a spreadsheet, and go down it row by row deciding which line of Form T2125 each charge belongs on. A client lunch is meals, at 50%. Rogers is utilities, but only partly. Staples is supplies, unless it was a desk. It takes an evening.&lt;/p&gt;

&lt;p&gt;I work in Canadian mortgage finance, where self-employed people's tax returns come up all the time.&lt;/p&gt;

&lt;p&gt;Shoebox does the first pass. You drop in the CSV, and a small open-weight model running in your browser tab puts each transaction on a T2125 expense line. Rows it isn't sure about get a red "check" tag. When you fix one, it learns that merchant, moves that merchant's other rows (with an undo), and remembers it in your browser. At the end you get a total for each line and two CSV downloads.&lt;/p&gt;

&lt;p&gt;The statement never leaves your laptop. There's no server, no account and no upload.&lt;/p&gt;

&lt;p&gt;Some tax rules sit on top of the model, because a sorter that gets the totals wrong isn't worth much:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meals and entertainment are generally limited to 50%.&lt;/li&gt;
&lt;li&gt;Utilities and vehicle costs only count once you enter a business-use percentage, which for a vehicle should come from your logbook. Until then the total leaves them out and says so.&lt;/li&gt;
&lt;li&gt;Card payments, tax instalments and transfers to savings are set aside as "not an expense", so paying off a Visa doesn't count twice.&lt;/li&gt;
&lt;li&gt;A refund comes off the merchant's line instead of being counted as income.&lt;/li&gt;
&lt;li&gt;If you're registered for GST/HST, it warns you that the totals still include tax you may be claiming back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it?&lt;/strong&gt; A sole proprietor with a few hundred business transactions a year is the user, but I think the paying customer is the bookkeeper who gets a "shoebox" of CSVs every April from clients who don't use bookkeeping software. QuickBooks and Xero have bank rules, but only after the client's data is in them. The plan is to keep the app free and open source and sell a bookkeeper version with a saved merchant list per client and output mapped straight to T2125 lines, at something like $10 per client per tax year. Because the model runs on the user's machine, one more customer costs me almost nothing to serve. I haven't had a paying user yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live: &lt;a href="https://alenzacodes.github.io/shoebox/" rel="noopener noreferrer"&gt;https://alenzacodes.github.io/shoebox/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Use a sample statement" to load 34 made-up September transactions. The first load downloads the model, about 23 MB, and after that it starts in a few seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1kosb0hyulyqur5pqhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1kosb0hyulyqur5pqhy.png" alt="Shoebox with the sample statement loaded" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Things to try:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find "LE BRIDGE BISTRO MONTREAL". The model put it on utilities and flagged it. Change it to meals and watch the meals total go up by half the amount.&lt;/li&gt;
&lt;li&gt;Type 50 in the business-use box under 9220 Utilities and see the total change.&lt;/li&gt;
&lt;li&gt;Change a Staples row and use Undo when it moves the other Staples row with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ALENZACODES" rel="noopener noreferrer"&gt;
        ALENZACODES
      &lt;/a&gt; / &lt;a href="https://github.com/ALENZACODES/shoebox" rel="noopener noreferrer"&gt;
        shoebox
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sort bank transactions into Form T2125 expense lines in your browser, with an open-weight model
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Shoebox&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Sort a year of bank transactions into Form T2125 expense lines without sending them anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://alenzacodes.github.io/shoebox/" rel="nofollow noopener noreferrer"&gt;https://alenzacodes.github.io/shoebox/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/ALENZACODES/shoebox/main/assets/screenshot.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FALENZACODES%2Fshoebox%2Fmain%2Fassets%2Fscreenshot.png" alt="Shoebox with the sample statement loaded: transactions on the left, T2125 totals on the right"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A freelancer gets to April with a bank export and no bookkeeping. Shoebox takes that CSV, puts each transaction on a Form T2125 expense line, flags the ones it isn't sure about, and learns from every fix. A small open-weight sentence model does the sorting inside your browser tab, so the statement never leaves your laptop. There's no server, no account and no upload.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Who would pay for it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The person I have in mind is a sole proprietor with a few hundred business transactions a year who doesn't want to pay a bookkeeper to sort them, and doesn't want to hand a full bank history to a hosted app to save an hour.&lt;/p&gt;
&lt;p&gt;The plan, if people use it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The app stays free and open source, as it is here.&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ALENZACODES/shoebox" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;It's plain HTML and JavaScript with no build step. &lt;code&gt;logic.js&lt;/code&gt; has the CSV parser, the classifier and the totals, and &lt;code&gt;node test.js&lt;/code&gt; covers them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The model is &lt;a href="https://huggingface.co/Xenova/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;, an open-weight sentence embedding model under the Apache 2.0 license, run in the browser with &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; using the 8-bit quantized weights. The 23 MB of model files live in the repo and are served from the same GitHub Pages site as the app, so it doesn't depend on a model host being up.&lt;/p&gt;

&lt;p&gt;Each T2125 line has a handful of example phrases, like "Google Ads" for advertising or "coworking space membership" for rent. When a statement loads, every description and every example becomes a 384-number vector. A transaction goes to the line whose closest example is nearest. If the best match is weak, or the top two lines are nearly tied, the row is flagged.&lt;/p&gt;

&lt;p&gt;Before embedding, I strip store numbers, reference codes and city names. That came from a real bug. In an early version, fixing "LE BRIDGE BISTRO MONTREAL" to meals dragged "MARRIOTT MONTREAL" over to meals too, because "Montreal" was doing most of the matching. I also made corrections count only for close matches, so one fix can't pull unrelated rows across.&lt;/p&gt;

&lt;p&gt;I wanted an honest number for how well it sorts, so after the hint phrases were done I wrote a held-out set of 186 transactions in the style of real bank exports, labelled them myself, and didn't touch the thresholds afterwards. The one exception is the "not an expense" class, which I added with its 9 rows after the rest. The set is in the repo with &lt;a href="https://alenzacodes.github.io/shoebox/eval/" rel="noopener noreferrer"&gt;a page that reruns the check&lt;/a&gt; in your browser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shoebox put 133 of 186 (72%) on the right line before any fixes.&lt;/li&gt;
&lt;li&gt;A plain keyword matcher using the same example phrases got 92 (49%). It had no guess at all for 76 rows, but when it did guess it was right 92 of 110 times.&lt;/li&gt;
&lt;li&gt;Shoebox flagged 85 rows, and those flags caught 46 of its 53 mistakes. The 101 rows it didn't flag were right 94 times (93%).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you still review about half the rows the first time. What the model buys you is coverage plus honest flags. It handles merchants it has never seen, like "CACTUS CLUB CAFE" or "FLAIR AIRLINES", that a keyword list has nothing for. The labels are my own, and a few are judgment calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project, it's the whole product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy is the pitch.&lt;/strong&gt; A bank statement shows where you shop, who you pay and who pays you. Asking a freelancer to upload a year of it to a hosted API to save an evening is a hard sell, and a bookkeeper shouldn't do it with client data without each client's consent. An open-weight model that runs in the tab means I can say "nothing leaves your computer" and anyone can check the code to see that's true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost works.&lt;/strong&gt; A hosted model bills per request, so every new customer adds to the bill. Here the user's laptop does the work, so there's no API bill that grows with the customer list and no account to set up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can be tuned for a niche.&lt;/strong&gt; A large hosted model probably knows T2125 well, but it needs the statement sent to it to help. Here the tax knowledge lives in a small, editable list of example phrases per line, and the held-out page tells you whether a change helped. Anyone can add better phrases for their trade. Because the weights are open and hosted alongside the app, nobody can raise the price or retire the model under me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A bookkeeper mode with a separate saved merchant list per client.&lt;/li&gt;
&lt;li&gt;Splitting GST/HST out of the totals for registrants.&lt;/li&gt;
&lt;li&gt;More bank layouts, starting with the ones people report not loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;After I came up with the initial idea, I used AI tools to help critique it, draft portions of the write-up and write portions of the code. I tested the app myself and checked the T2125 lines and the 50% meals rule against the CRA pages linked in the app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
