npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tooltiki-financial

v0.1.2

Published

Zero-dependency financial maths for Node: amortisation schedules, APR, IRR and XIRR, compound growth, progressive tax, inflation, and money that splits without losing a cent.

Readme

tooltiki-financial

npm CI dependencies licence

Financial maths that shows its working.

Amortisation schedules row by row. APR with the fees actually counted. IRR and XIRR that return null rather than a plausible lie. Progressive tax with the per-band breakdown. Money that splits three ways without losing a cent.

No dependencies, no native modules. Touches nothing from Node, so the same code runs in a browser, a worker or an edge function.

npm install tooltiki-financial
import { payment, amortise, apr, xirr } from 'tooltiki-financial';

payment({ principal: 250_000, annualRate: 0.055, periods: 360 });   // 1419.47

amortise({ principal: 250_000, annualRate: 0.055, periods: 360 }, { extraPayment: 250 });
// { periods: 254, interestSaved: 87711.36, periodsSaved: 106, rows: [...] }

Every rate is a decimal fraction. 6.5% is 0.065, never 6.5. It is the convention Excel uses, and mixing the two is the fastest way to be wrong by a factor of a hundred. fromPercent(6.5) is there for form input.


Why this exists

financial and formulajs give you the formulas. What they do not give you is the part people actually need, which is almost always a schedule rather than a single number — the row where the interest finally drops below the principal, the month the extra payment clears the loan, the band that produced the tax.

Three things here that were the reason to write it:

A schedule that reconciles. Rows are rounded as they are written, the way a lender's ledger does, so 360 rows add up to the stated total instead of drifting a few cents apart. The last instalment is whatever clears the balance, not a full payment.

Solvers that admit defeat. irr on a cash flow that never changes sign has no answer. It returns null. So does rateFromPayment when the payments do not cover the principal. A library that returns a number there is worse than one that returns nothing.

No tax rates baked in. Every jurisdiction moves its bands every year. A library that ships them is wrong the following January and silently wrong for everyone who did not upgrade. Brackets come in as data.


Loans

import { payment, amortise, remainingBalance, linearSchedule } from 'tooltiki-financial';

const loan = { principal: 250_000, annualRate: 0.055, periods: 360 };

payment(loan);                       // 1419.47
remainingBalance(loan, 120);         // what is left after ten years

amortise returns the schedule and the summary together:

const { rows, totalInterest, periods } = amortise(loan);

rows[0];
// { period: 1, payment: 1419.47, interest: 1145.83, principal: 273.64,
//   balance: 249726.36, cumulativeInterest: 1145.83, cumulativePrincipal: 273.64 }

totalInterest;                       // 261011.51 — more than the house

The extra-payment question

The one everybody actually arrives with, and the answer is usually more dramatic than they expect:

amortise(loan, { extraPayment: 250 });
// periods: 254        — nine years early
// interestSaved: 87711.36
// periodsSaved: 106

extraFrom starts it later, so you can compare paying extra now against paying extra once the car loan clears. The saving is measured against the same schedule, not against a closed-form approximation of it, so differencing two runs gives the same figure the library reports.

Linear repayment

The standard alternative to an annuity across much of continental Europe — a Dutch lineaire hypotheek, a German Tilgungsdarlehen. The same principal slice every month, interest on the falling balance, so the instalment shrinks:

const linear = linearSchedule(loan);
linear.rows[0].payment;        // 1840.27  — starts higher
linear.rows.at(-1).payment;    //  699.23  — ends lower
linear.totalInterest;          // 206824.24 versus 261011.51 for the annuity

That trade-off — more now, much less overall — is the thing worth being able to put in front of someone, and no other npm package will draw it for you.

Interactive versions: Mortgage calculator · Loan calculator · Amortization schedule


Rates and APR

import { effectiveAnnualRate, rateFromPayment, apr } from 'tooltiki-financial';

effectiveAnnualRate(0.12, 12);       // 0.126825 — what 12% monthly really earns

The dealer question. "350 a month for four years on 15,000" — what rate is that? There is no closed form, so it is solved by bisection:

rateFromPayment({ principal: 15_000, payment: 350, periods: 48 });   // 0.05668

APR is the interest rate plus the fees. The interest rate is charged on the full principal; the APR is measured against the principal minus the fees, because that is what reached you:

apr({ principal: 200_000, annualRate: 0.06, periods: 360 });               // 0.06
apr({ principal: 200_000, annualRate: 0.06, periods: 360, fees: 4000 });   // 0.061895

With no fees the two are the same number, which is the useful way to remember what the APR is for.


Cash flows

import { npv, irr, xnpv, xirr, paybackPeriod } from 'tooltiki-financial';

npv(0.08, [-50_000, 12_000, 15_000, 18_000, 21_000]);
irr([-50_000, 12_000, 15_000, 18_000, 21_000]);      // 0.10983

xirr([
  { amount: -10_000, date: '2008-01-01' },
  { amount:   2_750, date: '2008-03-01' },
  { amount:   4_250, date: '2008-10-30' },
  { amount:   3_250, date: '2009-02-15' },
  { amount:   2_750, date: '2009-04-01' },
]);                                                   // 0.373363

That last one is Microsoft's own worked example for XIRR, and it is in the test suite as a reference — dates in, actual/365 out, the same answer Excel gives. Dates may be strings, Date objects or timestamps, in any order.

paybackPeriod interpolates inside the period it happens in, because "somewhere in year four" is not something anyone can act on:

paybackPeriod([-100, 40, 40, 40]);   // { periods: 2.5, neverRecovered: false }

Growth

import { compound, cagr, realRate, yearsToDouble } from 'tooltiki-financial';

compound({ principal: 1000, contribution: 250, annualRate: 0.06, years: 25, inflation: 0.02 });
// final:              177713.46
// finalInTodaysMoney: 108321.84   <- the number that matters
// contributed:         76000.00
// years: [ { year: 1, start, contributed, interest, end, endInTodaysMoney }, ... ]

Contribution and compounding frequencies may differ, because in practice they do — a monthly deposit into an account that compounds daily. contributeAtStart switches between an ordinary annuity and an annuity due, which over thirty years is worth a full year of contributions.

realRate(0.07, 0.03);     // 0.03883 — Fisher, not 0.04
cagr(100, 337, 12);       // the steady rate that would have got there

Interactive versions: Compound interest calculator · Retirement calculator


Tax

Brackets are data. Keep them wherever you keep things that expire, with the date you last checked them beside the numbers.

import { progressiveTax, grossUp, validateBrackets } from 'tooltiki-financial';

const brackets = [
  { upTo: 10_000, rate: 0.1 },
  { upTo: 40_000, rate: 0.2 },
  { upTo: null,   rate: 0.4 },     // the top band must be unbounded
];

progressiveTax(50_000, brackets);
// tax: 11000, marginalRate: 0.4, effectiveRate: 0.22, net: 39000
// bands: [ { from: 0, to: 10000, rate: 0.1, taxable: 10000, tax: 1000 }, ... ]

The per-band breakdown is the point. It is what lets you show someone that entering a higher band never taxes the income below it at the higher rate — the single most persistent misunderstanding about how income tax works.

grossUp(39_000, brackets);        // 50000 — the salary that nets that
validateBrackets(brackets);       // [] when sound, otherwise what is wrong

validateBrackets catches an unsorted table, a gap, a missing top band, and a rate given as 20 instead of 0.2 — all of which otherwise produce a number that looks entirely plausible.

Interactive version: Paycheck calculator


Money

Two places floating point embarrasses you, both solved here rather than left to the caller.

import { roundMoney, allocate, splitEvenly, formatMoney } from 'tooltiki-financial';

Math.round(1.005 * 100) / 100;    // 1     <- 1.005 is really 1.00499999...
roundMoney(1.005);                // 1.01
roundMoney(-1.005);               // -1.01  half away from zero, symmetrically

allocate(100, [1, 1, 1]);         // [33.34, 33.33, 33.33] — adds to exactly 100
allocate(0.05, [3, 7]);           // [0.02, 0.03]
splitEvenly(100, 3);              // [33.34, 33.33, 33.33]

formatMoney(1234.5, { currency: 'EUR', locale: 'nl-NL' });   // '€ 1.234,50'

allocate rounds each share down and then hands the remaining cents to the largest fractional parts, ties to the earlier index. Deterministic, and the parts always sum to the total.


Inflation

From a published index rather than from a rate, where you have one — a rounded average compounded over twenty years drifts far enough to be visibly wrong.

import { adjustByIndex, purchasingPower } from 'tooltiki-financial';

adjustByIndex(100, 100.0, 134.2, 20);
// value: 134.20, change: 0.342, annualRate: 0.01482

purchasingPower(100, 0.03, 20);   // 55.37 — twenty years at 3% takes nearly half

Interactive version: Inflation calculator


What this does not do

It is not a ledger, an accounting system or a currency library. It has no opinion about how you store money, no exchange rates, and no notion of a transaction. It computes; you decide what to do with the number.

It also ships no jurisdiction-specific rates — no tax bands, no contribution ceilings, no CPI series. Those expire, and a library is the wrong place for data with an expiry date. progressiveTax and cappedRate take them as arguments so you can keep them somewhere you will actually update.


Runtimes

Node 18 and up. ESM and CommonJS, with TypeScript declarations for both:

import { payment } from 'tooltiki-financial';        // ESM
const { payment } = require('tooltiki-financial');   // CommonJS

Nothing is imported from Node, so it runs unchanged in browsers, workers, Deno, Bun and edge runtimes.


Accuracy

Where an authoritative reference figure exists, it is in the test suite: Excel's PMT and XIRR worked examples, the standard 30-year amortisation figure. Where one does not, the tests assert the property instead — that the payment stream discounts back to exactly the principal, that grossUp inverts progressiveTax, that allocated parts sum to the total, that npv at the computed irr is zero. Those catch more than a magic number does, and they cannot quietly encode a mistake in the expectation.


Contributing

See CONTRIBUTING.md. A failing case with a reference value you can point at is the most useful thing you can send.

npm install
npm test          # builds, then runs the suite against the built output
npm run typecheck

About

Built and maintained by TecWeb B.V., who also run ToolTiki — browser tools that do one thing, run entirely on your own device, and are genuinely free. This library is the arithmetic behind the finance ones, extracted and tested on its own. Its sibling is tooltiki-image-tools.

MIT licensed. See LICENSE.