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

@casomoltd/nhs-pay

v0.19.4

Published

NHS Agenda for Change pay scales, pension tiers, regions, and take-home calculator

Downloads

2,744

Readme

@casomoltd/nhs-pay

NHS pay scales — Agenda for Change plus medical & dental — with pension tiers, regions, HCAS supplements, and a take-home calculator. Built on top of @casomoltd/paye-calc.

See docs/pay-frameworks.md for the domain model — the pay frameworks (AfC, medical, dental, VSM), the shared scale-resolver core, and where the consumer boundary sits.

About Casomo

Casomo is a registered UK limited company founded and directed by David Mohamad. It builds focused software tools and offers engineering consulting. This library powers the NHS pay calculators and explainers on casomo.co.uk — where the differentiator is showing take-home pay (after tax, NI and pension), not just the gross figures most published sources stop at.

Install

npm install @casomoltd/nhs-pay

Public on the npm registry — no auth or .npmrc config needed.

Requires @casomoltd/paye-calc (>=0.5.0) as a peer dependency.

Usage

Look up a band's salary range

import {getAfcScales} from '@casomoltd/nhs-pay';

const {bands} = getAfcScales();
const band5 = bands.find((b) => b.band === '5')!;
console.log(band5.salaryMin); // entry salary
console.log(band5.salaryMax); // top of band
console.log(band5.points);    // all pay points

Calculate take-home for a Band 5 nurse

import {
  nhsTakeHome,
  getAfcScales,
  pensionTierRate,
} from '@casomoltd/nhs-pay';

const {bands, pensionTiers} = getAfcScales();
const band5 = bands.find((b) => b.band === '5')!;
const salary = band5.salaryMin;
const rate = pensionTierRate(salary, pensionTiers);

const thp = nhsTakeHome(salary, rate / 100);
console.log(thp.net);               // annual net
console.log(thp.incomeTax);          // annual tax
console.log(thp.nationalInsurance);  // annual NI
console.log(thp.pensionDeduction);   // annual pension

Apply HCAS supplement

import {
  getAfcScales,
  calculateHcasSupplement,
} from '@casomoltd/nhs-pay';

const {bands, hcas} = getAfcScales();
const base = bands[0].salaryMin;
const supplement = calculateHcasSupplement(
  base, hcas.innerLondon,
);
console.log(base + supplement); // London-adjusted

Format salary for display

import {fmtSalary, fmtPct} from '@casomoltd/nhs-pay';

fmtSalary(31049); // '£31,049'
fmtPct(8.3);      // '8.3%'

Take-home for a doctor or dentist

Medical and dental grades resolve the same way as AfC, via a per-family resolver. getMedicalScales(year, nation) lists the grades published for a nation and year; fromPoint builds a Post (gross, pension tier, tax, NI, take-home) from one point.

import {getMedicalScales, medicalResolver} from '@casomoltd/nhs-pay';

const grades = getMedicalScales('2026-27', 'england');
const consultant = grades.find((g) => g.grade === 'consultant')!;
const top = consultant.points.at(-1)!;

const post = medicalResolver.fromPoint(
  'consultant', top, 'england', '2026-27',
);
console.log(post.salary);        // basic pay
console.log(post.pensionRate);   // member contribution %
console.log(post.takeHome.net);  // annual net after tax + NI + pension
console.log(consultant.source.reference); // the circular it came from

fromPoint takes the point itself. There is also fromScalePoint(grade, label, …) for a caller holding only a label, but a label does not identify a point on every scale — England's consultant scale is 20 payroll steps over 5 threshold labels, so 'Threshold 4' names six of them and that call throws AmbiguousScalePoint rather than picking one.

getDentalScales / dentalResolver mirror this for salaried dental grades. Both fail loud (ScaleUnavailable) for an unpublished nation/year or grade rather than defaulting.

Project a 2015-scheme pension

import {
  COMMUTATION_FACTOR,
  LUMP_SUM_ALLOWANCE,
  commute,
  projectPension,
} from '@casomoltd/nhs-pay';

const projection = projectPension({
  kind: 'statement',
  accruedPension: 5000, // from the Annual Benefit Statement
  statementDate: new Date(2026, 2, 31), // the date it names
  currentSalary: 54000,
  dateOfBirth: new Date(1990, 0, 1),
  exitDate: new Date(2035, 0, 1),
  retirementDate: new Date(2053, 0, 1), // before NPA → ERF
  npa: 67,
  assumedCpi: 0.02,
});
console.log(projection.annualPension); // after ERF/LRF
console.log(projection.factorType);    // 'erf'
console.log(projection.curve);         // chart-ready points

// Commutation takes the whole {nominal, real, asAt} figure, not
// one side of it: the lump sum allowance is a cash amount tested
// on a date, so a dateless number could not say which ruler it
// was in. Every limit is required for the same reason — a
// default would answer a question the caller did not ask.
const {lumpSum, residualPension, limit} = commute(
  projection.annualPension,
  1, // take the permitted maximum
  {
    commutationFactor: COMMUTATION_FACTOR,
    allowance: {
      amount: LUMP_SUM_ALLOWANCE,
      asAt: new Date(2026, 2, 31),
    },
    // The run's OWN price series, so the allowance is carried
    // forward at the rate the pension was projected at.
    prices: projection.prices,
  },
);
console.log(lumpSum.real, lumpSum.nominal); // both rulers
// The maximum, and which cap stopped it — PER RULER.
console.log(limit.real.amount, limit.real.binding);
console.log(limit.nominal.amount, limit.nominal.binding);

The maximum is the lower of two caps, and both are statutory: 25% of the capital value of the benefits, and the Lump Sum Allowance. The scheme's own contribution to the swap is the 12:1 rate and nothing else, so a consumer must not attribute the 25% to the scheme — LUMP_SUM_CAPS.Scheme names the discriminant, not the rule's author. binding says which cap stopped it, so a consumer can explain the figure rather than restate the rule.

Why that name is left as it is, and what moves where when it changes: The two caps on tax-free cash.

There is one limit per ruler, and no single flag, because the two can genuinely disagree: real is the model run at zero CPI while the allowance is carried forward at CPI, so around the crossover the allowance binds in today's money while the scheme limb still binds in cash. Show the limit belonging to the ruler on screen. See The two caps on tax-free cash.

Without a statement figure, kind: 'estimation' accrues from a joinDate instead. ERF/LRF factors are transcribed verbatim from the GAD consolidated factor workbook (30 June 2023 issue) with GAD's own rounding rules; a retirement date beyond the printed tables throws RetirementFactorOutOfRange.

How it works

docs/how-it-works.md — the model this library implements: the recurrence it walks, what a scheme year and an exit date mean to it, why cash and today's money are two runs rather than one and a deflator, and every assumption it makes, declared. Read it before trusting a figure.

API reference

docs/api.md — every export, organised by domain, with typed signatures and short notes. Kept in sync with the code by a check-time drift gate. Agents may prefer the shipped dist/*.d.ts, which carry the full JSDoc per module.

Data sources

AfC pay scales

| Year | Nations | Source | | ------- | -------------------- | ------ | | 2025-26 | England, NI, Wales | NHS Employers pay scales 2025/26 | | 2026-27 | England, NI | NHS Employers pay scales 2026/27 |

Cross-checked against NHSPRB 39th Report (2026), Table A25 (AfC 2025/26 data for England, NI, Wales).

2015 CARE pension projection

| Data | Source | | ---- | ------ | | September CPI + in-service rate | HM Treasury Revaluation Orders, one SI per year | | Accrual rate, NPA | NHSBSA 2015 Members' Guide (V13) | | ERF / LRF factors | GAD NHS EW consolidated factor workbook | | Commutation rate (£12 : £1) | NHSBSA Key Notes — 2015 Scheme Estimates (V2) | | Permitted maximum (lowest of three limbs) | Sch 29 Finance Act 2004 para 2 | | Applicable amount, defined benefits, (A + (B × C)) / 4 | Sch 29 Finance Act 2004 para 2C | | Relevant valuation factor (20) | Finance Act 2004 s.276 | | Lump Sum Allowance (£268,275, frozen) | ITEPA 2003 s.637P |

Reconciled line by line against a real Annual Benefit Statement (2015 Section, updated to 31/03/2025, name and membership number redacted), and against a ten-year projection built by hand from it.

Both are archived and inventoried in docs/source-archive.md, with the links themselves in the header of tests/golden-abs.test.ts — beside the assertions they justify, which is where a citation belongs and where it cannot drift from what it is citing.

The statement is the source of truth and the library never restates it: enter its figure and ask for its own date, and you are handed back exactly what it says. See Two rulers, one model for how cash and today's money are produced, and A projection never applies a published Order for why a legislated rate is declined even where one exists.

That promise is exact for a statement handed over with its date. Passing the figure undated instead asks a different question — "this is my balance today" — and the library must then work backwards to the year end, at its own revaluation rule rather than the scheme's. For a member who left at a year end the two differ; see Reading a statement back applies the SAME rule for the size of it and why passing the date avoids it.

Scotland

Scotland negotiates its own AfC award independently and has completely different base salaries from England. Separate scale tables are stored for each tax year — getAfcScales(year, 'scotland') returns Scotland figures directly. Scotland also has structural differences: Band 2 has 2 points (vs 1 in England) and Bands 8a–9 have 2 points each (vs 3).

Source: PCS(AFC)2026/1 — Scottish Health Workforce Directorate circular (23 Jan 2026). Annex B holds both years' rate tables; Annex C sets them out increment by increment and corroborates Annex B point for point.

Scotland's 2025-26 settlement was 4.25%, but carried an inflation guarantee — at least one percentage point above average CPI for the calendar year of the uplift. 2025 CPI confirmed at 3.4%, so the guarantee triggered and the rate was revised to 4.4%, backdated to 1 April 2025 with arrears. 2026-27's 3.75% then applies to the revised base, so both years moved. Take both from Annex B rather than the MSG consolidated table, which still prints the original 4.25% rates.

Wales

Wales runs its own pay ladder and both years are transcribed from it: AfC(W) 02/2025 (29 May 2025) and AfC(W) 02/2026 (12 Feb 2026), Annex 1 of each.

Wales is not England's table with a floor applied. The two ladders differ at every band from 4 upward, where no floor reaches: Band 9 tops out at £131,732 in Wales against £129,783 in England. A floor lifts the bottom of a ladder and cannot raise its top. Wales also carries two Band 2 points where England has one.

Point labels come from each circular's own "years until eligible for pay progression" column, so they are stated by the Welsh source rather than borrowed from England's table, and they do not always agree with England's for the same band.

The living-wage floor is real but separate: it is an advance payment that lifts the lowest points, which is why the 2026/27 uplift explicitly does not apply to them. WALES_LIVING_WAGE carries both published figures — the annual floor and the hourly rate it is set against — for reference; nothing derives a scale from it, and neither figure derives from the other. Source: AfC(W) 01/2026 (6 Jan 2026).

Band 1 is published in both years (£24,833, then £26,300) and closed to new entrants. It is absent from AFC_BANDS, the same gap Scotland has.

Both Welsh years were transcribed during an outage of the NHS Wales web estate (2 September 2026), so the circulars were read from NHS Wales Employers' copy rather than the publisher's own. Each file was checked byte-for-byte against a copy downloaded from www.nhs.wales the previous day, before the outage, and both matched. The links above are the publisher's, which is where these documents live.

Other data

| Data | Source | | ---------------------- | ------ | | Pension contribution rates 2025/26 | NHSBSA contribution rates | | Income tax / NI rates | gov.uk (via paye-calc) | | National Living Wage | gov.uk NLW rates | | HCAS PCT zones | NHS Employers Annex 8 Table 12 |

Medical & dental pay scales

Doctors and dentists are paid on a separate set of pay circulars — one per nation — from Agenda for Change. This library encodes them so it can render take-home (not just gross) for medical and dental grades, which is what most published sources omit.

| Nation | Circular | Year | Source | | ------ | -------- | ---- | ------ | | England | PC(M&D) 1/2026 R2 | 2026/27 | NHS Employers | | Scotland | PCS(DD)2026/01 | 2026/27 (training grades only) | NHS Scotland | | Scotland | PCS(DD)2025/01 + addendum | 2025/26 (complete round) | NHS Scotland | | Wales | M&D(W) 01/2026 | 2026/27 | NHS Wales | | Wales | M&D(W) 01/2025 | 2025/26 | NHS Wales | | Northern Ireland | HSC(TC8) 05/2025 | 2025/26 | DoH NI |

Wales's 2026/27 round is a 3.5% uplift (3.75% for salaried dentists) and removes the closed Associate Specialist (MC01) code, so that grade resolves only at 2025/26.

NI 2025/26 pension member tiers (HSC — distinct thresholds and rates from NHSBSA) are sourced from HSC Pensions.

How the data is modelled

The circulars vary widely in structure (nodal training points, consultant thresholds, SAS experience bands, GP ranges, dental spines), so the data is built in three layers that decouple transcription fidelity from the uniform domain model:

  1. Verbatim circular (src/circulars/*.ts) — one file per PDF, each table transcribed 1:1 in source order with a row shape that mirrors that table's own columns, under a comment citing the Annex / section / page. Every table in the circular is either transcribed or recorded with a reason for skipping it, so nothing is silently dropped and each file diffs against the PDF top-to-bottom.
  2. Translation layer (src/medical-scales.ts, src/dental-scales.ts) — selects which scales feed the calculator and maps each verbatim row to a canonical scale point. Inclusive by default: closed-to-new-entrant grades, devolved training variants, GP registrars and the Community Dental Service are all wired.
  3. Canonical domain (getMedicalScales / getDentalScales + medicalResolver / dentalResolver) — a uniform (grade, nation, year) → scale points view, identical in shape to the AfC resolver.

Scope. (a) every basic-pay salary scale, including closed-to-new-entrant grades still paid to incumbents, plus (b) earnings-affecting supplements expressed as an annual £ (Clinical Impact / Excellence awards, DPH and intensity supplements). Pure expense tables (mileage, fees) and self-employed GDS/UDA dentist contract income are out of scope.

Unpublished data fails loud. England and Wales publish complete 2026/27 rounds. Scotland's 2026/27 circular uplifts training grades only, so its consultant / SAS / GP / dental scales resolve to the complete 2025/26 round (PCS(DD)2025/01) until the Scottish Government publishes the non-training uplift; Northern Ireland's latest circular is 2025/26 throughout. Each grade resolves at its own cited year and latestYearFor(grade, nation) reports it — figures are never silently carried forward, and a query for an unpublished (year, nation) throws rather than defaulting to another nation's or year's figures.

Test fixtures

tests/fixtures/band-take-home.csv — 90 golden-value rows covering bands 5, 7, 8a across all 4 nations (England, Scotland, Wales, NI), both tax years, 3 HCAS zones, and part-time (0.6 FTE). Gross figures are derived from getAfcScales(year, nation) using the sources above. Pension, tax, NI and net values are computed by nhsTakeHome and should be cross-checked against:

Tax years

AfC scale + pension coverage (all four nations):

| Year | Scales | Pension | | ------- | ------ | ------- | | 2023-24 | Yes | Yes | | 2024-25 | Yes | Yes | | 2025-26 | Yes | Yes | | 2026-27 | Yes | Yes |

Medical & dental coverage is per-nation (England/Wales 2026/27; Scotland 2026/27 for training grades and 2025/26 for the rest; NI 2025/26) — see Medical & dental pay scales.

Development

npm run check       # lint + typecheck + knip + jscpd
                    # + api-docs drift gate + test
npm run build       # compile to dist/
npm test            # vitest
npm run test:watch  # vitest watch mode

License

AGPL-3.0-only — see LICENSE. Copyright © 2026 Casomo Ltd.

There is no linking exception: combine this library with your own code and the combined work is AGPL too, and letting people use that work over a network obliges you to offer them its source. Charging money is not what triggers this, and not charging is no exemption. A commercial licence is available for use that needs to stay closed — enquire via casomoltd.com.