My last post here covered the architecture of LoanPayoff.org — one math engine, no database, static hosting. This one is about the part that took three times longer than I expected: modeling extra payments correctly. The amortization formula is textbook material; what happens when a borrower pays $100 extra every month, or drops $3,000 on the loan in month 6, is where the real work lives.
The base case is 20 lines
A level-payment loan is just an annuity. Payment = P·r / (1 − (1+r)^−n), then walk the schedule month by month:
function schedule(balance, apr, months, payment) {
const r = apr / 12;
const rows = [];
for (let m = 1; balance > 0 && m <= months * 2; m++) {
const interest = balance * r;
const principal = Math.min(payment - interest, balance);
balance -= principal;
rows.push({ m, interest, principal, balance: Math.max(balance, 0) });
}
return rows;
}
Every calculator on the site calls this one loop. The interesting parts are all in what feeds it.
Extra monthly payments: the final payment is partial
Add $100 extra and the naive assumption is "same schedule, ends sooner." Almost. The loan ends mid-month — the last payment is smaller than all the others. If you compute interest as months × payment − principal, you overstate it.
The fix is to let the loop run until balance <= 0 and count only what was actually paid. On a $28,000 auto loan at 7.4% over 60 months, the required payment is $559.73 and total interest is $5,584. Add $100 extra: the schedule closes in 50 payments, the last one short, and interest lands at $4,563. That's a real $1,021 saved — but only if the final partial payment is counted honestly.
That whole scenario is the worked example on the auto loan payoff calculator, which re-runs this exact engine in your browser.
Lump sums: anchor to a month, not a date
"Pay $3,000 at month 6" is unambiguous. "Pay $3,000 on March 15" is not — you need the disbursement date, the first-payment date, and a day-count convention to convert. I deliberately support only month-numbered lump sums. It sidesteps 30/360 vs actual/365 debates entirely, and for a payoff decision the difference is a rounding error nobody should act on anyway.
One subtlety: a lump sum applied in month k only changes interest from month k onward, but it changes the term nonlinearly — the remaining balance re-amortizes against the same payment. The naive approach of "subtract and keep going" is correct here precisely because the loop recomputes interest off the live balance every month.
Biweekly is not "half a monthly payment every 14 days"
True biweekly plans pay half the monthly payment every two weeks — 26 half-payments a year, which is 13 full payments, roughly +8% extra principal annually. Some lenders instead draft twice a month (24 half-payments), which saves almost nothing.
So the calculator has a toggle, not an assumption: 26 half-payments vs 24 half-payments vs plain monthly. On the $28,000 example, the 26-cycle version cuts the term by about 3 months on its own; the 24-cycle version saves a fraction of that. Borrowers who think they're on biweekly but are actually on semimonthly are usually surprised by the gap.
What I got wrong first
Two bugs worth confessing:
-
Negative amortization guard. If
payment <= interest, principal never falls and the loop runs forever. The capm <= months * 2plus an explicit error state fixed that. - Rounding drift. Displaying rounded cents while computing on floats makes the final balance end at $0.0000001 instead of $0. The comparison view (with vs. without extra payments) made the drift visible as a one-cent discrepancy, which users absolutely will report. Fixed by tracking everything in exact cents internally.
Try it without trusting it
Everything runs client-side, so you can open the auto loan payoff calculator, plug in your own numbers, and check the totals against your lender's statement. If you run a personal-finance blog, the calculators are also embeddable — one iframe, no backend to trust.
Edge cases I still want to handle: interest capitalization events (common on student loans after deferment) and odd first periods where the days before the first payment change accrual. If you've built loan math before, I'd like to hear how you handled those.
Top comments (0)