<?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: Mokshraj(ssr7)</title>
    <description>The latest articles on DEV Community by Mokshraj(ssr7) (@mokshrajssr7).</description>
    <link>https://dev.to/mokshrajssr7</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%2F4091827%2F8d953bfd-9e35-4696-8a5c-2471300849ef.jpg</url>
      <title>DEV Community: Mokshraj(ssr7)</title>
      <link>https://dev.to/mokshrajssr7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mokshrajssr7"/>
    <language>en</language>
    <item>
      <title>Every 401(k) fee analyzer wanted my login. The data was in a PDF they already mail me.</title>
      <dc:creator>Mokshraj(ssr7)</dc:creator>
      <pubDate>Wed, 26 Aug 2026 20:17:07 +0000</pubDate>
      <link>https://dev.to/mokshrajssr7/every-401k-fee-analyzer-wanted-my-login-the-data-was-in-a-pdf-they-already-mail-me-5hld</link>
      <guid>https://dev.to/mokshrajssr7/every-401k-fee-analyzer-wanted-my-login-the-data-was-in-a-pdf-they-already-mail-me-5hld</guid>
      <description>&lt;p&gt;second post here. The first was about a bank reconciler that can't upload anything; this one is&lt;br&gt;
about a smaller tool with one genuinely interesting bug in it.&lt;/p&gt;

&lt;p&gt;Here's the thing I didn't know until I went looking: if you have a 401(k) in the US, your plan is&lt;br&gt;
legally required to mail you an annual fee disclosure. It's called the 404(a)(5) notice. It lists&lt;br&gt;
every fund in your plan, every expense ratio, and any administration fee. It arrives once a year&lt;br&gt;
and — going by everyone I asked — approximately nobody opens it.&lt;/p&gt;

&lt;p&gt;So the data is already sitting in your house. But every fee analyzer I tried wanted me to connect&lt;br&gt;
my actual retirement accounts through an aggregator first. Handing over brokerage credentials to&lt;br&gt;
find out what I'm being charged felt like a strange trade for arithmetic I could do from a&lt;br&gt;
document I already had.&lt;/p&gt;

&lt;p&gt;I built the version that just takes the numbers: &lt;a href="https://stepwisecalc.com/tools/401k-fee-analyzer" rel="noopener noreferrer"&gt;https://stepwisecalc.com/tools/401k-fee-analyzer&lt;/a&gt;&lt;br&gt;
(free, no signup, nothing leaves the browser)&lt;/p&gt;

&lt;p&gt;The rest of this is the three parts that were actually interesting to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first bug was an average
&lt;/h2&gt;

&lt;p&gt;My first version averaged the expense ratios. This is wrong, and it's wrong in a way that looks&lt;br&gt;
completely fine until you check it.&lt;/p&gt;

&lt;p&gt;Say you hold $90,000 in an index fund charging 0.10%, and $10,000 in an active fund charging&lt;br&gt;
1.00%. The mean of those two numbers is 0.55%. Your actual blended cost is 0.19%.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
ts
// wrong: every fund counts equally regardless of how much is in it
const naive = funds.reduce((s, f) =&amp;gt; s + f.expenseRatio, 0) / funds.length;

// right: a fee is charged against a balance, so weight by balance
const total = funds.reduce((s, f) =&amp;gt; s + f.balance, 0);
const weighted = funds.reduce((s, f) =&amp;gt; s + f.balance * f.expenseRatio, 0) / total;
Nearly 3x off, and in the direction that makes your plan look worse than it is. The general shape of the bug — averaging rates that apply to different-sized quantities — is one I've now seen in enough places that I think of it as its own category. An expense ratio isn't a number you can average with other numbers. It's a rate attached to a quantity, and the quantity is the point.

Fix it and the units make sense again: 0.19% of $100,000 is $190, which is the $90 and the $100 the two funds actually charge.

Flat fees don't fit in a percentage model at all
Plenty of plans charge a flat recordkeeping fee — a fixed number of dollars a year, independent of your balance. To get an all-in cost you have to express it as a percentage, which means dividing by the balance, which means it isn't a constant any more:

const flatAsPercent = (adminFeeFlat / totalBalance) * 100;
const allIn = weightedExpenseRatio + adminFeePercent + flatAsPercent;
Running the numbers on a $100/year flat fee:

On a $5,000 balance, all-in cost is 2.10%
On a $250,000 balance, all-in cost is 0.14%
Fifteen times heavier on the smaller balance, for identical service. I hadn't expected the tool to surface a fairness property, but that's what falls out of the arithmetic, and it's the number a new employee with a small balance most needs to see.

Testing a closed form against a simulation
The projection uses the closed-form future value with monthly compounding and end-of-month contributions:

const monthly = (grossReturnPercent - annualCostPercent) / 100 / 12;
const growth = Math.pow(1 + monthly, months);
return startBalance * growth + monthlyContribution * ((growth - 1) / monthly);
I don't trust myself with that formula. Off-by-one on the contribution timing, a sign error, the degenerate case where the net return is exactly zero and you divide by it — all of them produce a number that looks plausible and is wrong by tens of thousands of dollars.

So the test doesn't assert a magic constant. It runs a month-by-month loop and demands the two agree:

it('matches an iterative month-by-month simulation', () =&amp;gt; {
  const closed = futureValue(50000, 500, 7, 0.5, 20);
  let balance = 50000;
  const monthly = (7 - 0.5) / 100 / 12;
  for (let m = 0; m &amp;lt; 240; m += 1) balance = balance * (1 + monthly) + 500;
  expect(closed).toBeCloseTo(balance, 0);
});
Two independent implementations of the same idea, one obviously correct and slow, one fast and easy to get subtly wrong. If they disagree, one of them is broken and I don't have to guess which kind of broken. The zero-return case gets its own test, because that's where the closed form divides by zero and has to fall back to plain addition.

What the whole thing exists to show, on a $100,000 balance with $500/month for 30 years at a 7% gross return:

At 1.00% all-in: $1,104,515
At 0.05% (a broad index fund): $1,403,637
Difference: $299,122, or 21% of the final balance
Same contributions, same market, same everything. One number changed.

The part where I refused to make it look better
Modelling fees as a straight one-for-one reduction in annual return is the standard approach and it's what I did. But it's a floor, not the truth: funds carry internal trading costs that never appear in the expense ratio, some share classes route revenue sharing back to the plan's recordkeeper, and a target-date fund can layer its own fee on top of the funds it holds.

I put that in the UI rather than a footnote, and it was tempting not to — "your fees are at least this bad" is a weaker headline than a precise-looking figure. But a tool whose entire pitch is that it doesn't want anything from you shouldn't then overstate its own precision.

Built in TypeScript, no dependencies in the engine, everything client-side. Same site as the reconciler I posted about last time, if you saw that one.

What I'd like to know: has anyone here actually read their 404(a)(5) disclosure, and did the numbers on it match what you expected? I've now looked at a handful and the admin fee is almost always the surprise.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a bank reconciliation engine that never uploads your files</title>
      <dc:creator>Mokshraj(ssr7)</dc:creator>
      <pubDate>Mon, 24 Aug 2026 07:44:51 +0000</pubDate>
      <link>https://dev.to/mokshrajssr7/i-built-a-bank-reconciliation-engine-that-never-uploads-your-files-2eep</link>
      <guid>https://dev.to/mokshrajssr7/i-built-a-bank-reconciliation-engine-that-never-uploads-your-files-2eep</guid>
      <description>&lt;p&gt;First post here, so bear with me.&lt;/p&gt;

&lt;p&gt;I've spent the last few weeks building a site full of browser-side calculators and tools, and&lt;br&gt;
somewhere along the way I went down a rabbit hole about what bookkeepers actually spend their&lt;br&gt;
time on. The answer, over and over: reconciliation. Books against the bank statement, every&lt;br&gt;
month, mostly by hand, mostly in Excel.&lt;/p&gt;

&lt;p&gt;There are tools for this. But every one I found — the CSV converters, the SaaS reconcilers, the&lt;br&gt;
inevitable "just paste it into ChatGPT" suggestion — starts the same way: upload the bank&lt;br&gt;
statement. And that's exactly the step a lot of these people can't take. A client's bank&lt;br&gt;
statement might be the most sensitive file a bookkeeper touches. Sending it to some website's&lt;br&gt;
server is either against firm policy or just a bad instinct they've rightly developed.&lt;/p&gt;

&lt;p&gt;So my pitch is almost stupid in its simplicity: this one can't upload anything, because there's&lt;br&gt;
nowhere to upload to. No server side at all. You drop two CSVs — your ledger and the bank&lt;br&gt;
export — and the parsing and matching run entirely in the browser. You can open the network tab&lt;br&gt;
and watch nothing leave.&lt;/p&gt;

&lt;p&gt;It's here if you want to poke at it: &lt;a href="https://stepwisecalc.com/tools/reconciliation" rel="noopener noreferrer"&gt;https://stepwisecalc.com/tools/reconciliation&lt;/a&gt;&lt;br&gt;
(free, no signup — and I'm a developer, not an accountant, so part of why I'm posting is to&lt;br&gt;
find out where it falls apart).&lt;/p&gt;

&lt;p&gt;The rest of this is the four parts that were genuinely fun to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working out which column is which, without trusting headers
&lt;/h2&gt;

&lt;p&gt;My first version trusted header names. That survived about two test files. Banks rename headers&lt;br&gt;
constantly — &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;Txn Date&lt;/code&gt;, &lt;code&gt;Value Date&lt;/code&gt;, &lt;code&gt;Posted&lt;/code&gt; — so matching on names is hopeless.&lt;/p&gt;

&lt;p&gt;What stays consistent is the data itself. Dates look like dates. Amounts look like amounts. So&lt;br&gt;
each column gets scored for date-ness, amount-ness and description-ness, and the best-scoring&lt;br&gt;
assignment wins.&lt;/p&gt;

&lt;p&gt;The case that took me longest: banks that split debits and credits into two separate columns.&lt;br&gt;
The giveaway turned out to be fill patterns — you get two numeric columns where every row has a&lt;br&gt;
value in exactly one of them. When the detector sees that disjoint pattern, it folds the pair&lt;br&gt;
into signed amounts. Same general approach handles &lt;code&gt;(500.00)&lt;/code&gt; meaning negative, trailing&lt;br&gt;
&lt;code&gt;DR&lt;/code&gt;/&lt;code&gt;CR&lt;/code&gt; markers, and day-first vs month-first dates (you disambiguate from the rows where the&lt;br&gt;
day is bigger than 12).&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching in tiers, because fuzzy matching in one pass produces confident garbage
&lt;/h2&gt;

&lt;p&gt;I learned quickly that one big fuzzy match gives you results that look great and are wrong. So&lt;br&gt;
matching runs in three labelled tiers, strictest first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exact — same amount, same date.&lt;/li&gt;
&lt;li&gt;Near-date — same amount within a small date window, because deposits clear days after
they're booked.&lt;/li&gt;
&lt;li&gt;Combined — a bounded subset-sum search in both directions: one bank line that equals the sum
of several book lines (a batched deposit), or the reverse. This is the case that breaks
everyone's VLOOKUP, and honestly it's the reason the tool deserves to exist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every match carries its tier in the output, so you know exactly how much to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The divide-by-9 trick
&lt;/h2&gt;

&lt;p&gt;This one I stole from audit folklore. If two digits get transposed — 54 typed as 45 — the error&lt;br&gt;
is always divisible by 9. Always. So when two amounts almost match, the engine checks the&lt;br&gt;
difference, and if it divides cleanly by 9 it flags "possible transposed digits" instead of a&lt;br&gt;
bare mismatch. Decades-old accountant knowledge, three lines of TypeScript. Easily my favourite&lt;br&gt;
part of the whole build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it trustworthy (I hope)
&lt;/h2&gt;

&lt;p&gt;Here's the thing that bugged me about fuzzy matching: how do you know the reconciliation as a&lt;br&gt;
whole is right, even if individual matches are heuristic?&lt;/p&gt;

&lt;p&gt;The answer I landed on: the exceptions have to explain the difference. So the engine computes&lt;br&gt;
two numbers completely independently — the difference between the files (books total minus bank&lt;br&gt;
total), and the exception schedule (missing-in-bank minus missing-in-books plus the&lt;br&gt;
mismatches). It only ever claims "fully explained" when the two agree to the cent. If they&lt;br&gt;
disagree, the UI says so instead of quietly absorbing it.&lt;/p&gt;

&lt;p&gt;That property, not any particular match, is what the test suite actually asserts. The matcher&lt;br&gt;
is allowed to be heuristic. The arithmetic isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring implementation notes
&lt;/h2&gt;

&lt;p&gt;TypeScript. Zero dependencies in the engine, including the CSV parser — RFC 4180 is small&lt;br&gt;
enough that writing the ~40 lines felt more honest than pulling in a library. The reconciler is&lt;br&gt;
one of 350+ tools on the site, all built on the same idea: the computation happens on your&lt;br&gt;
machine, not mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want from you
&lt;/h2&gt;

&lt;p&gt;A bank export that breaks it. I've tested against constructed files and the arithmetic holds,&lt;br&gt;
but real bank CSVs are their own kind of chaos, and the failure modes are where this gets&lt;br&gt;
better. Tell me in the comments what mangled it and I'll dig in.&lt;/p&gt;

&lt;p&gt;Also a genuine question for anyone who's done bookkeeping work: is CSV enough? If most banks&lt;br&gt;
in your world only give PDF statements, the tool needs a different front door, and I'd rather&lt;br&gt;
find that out now.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
