Install

powercost is a dependency-free ES module that runs in the browser and in Node 18+. Tariff data lives in lazy-loaded packs.

npm i powercost

Or use it straight from a static site / GitHub Pages with no build step:

<script type="module">
  import { price } from 'https://esm.sh/powercost';
</script>

Quick start

import { price } from 'powercost';
import { swedenDb } from 'powercost/data/se';

const cost = await price({
  consumption: [
    { start: '2026-04-15T18:00:00+02:00', end: '2026-04-15T18:15:00+02:00', kwh: 0.40 },
  ],
  config: {
    zone: 'SE3',
    providerPlanId: 'tibber-kvartspris',
    gridOperatorId: 'vattenfall-e4-25a',
    reportCurrency: 'USD',
  },
  db: swedenDb,
  live: true,                 // fetch spot + FX live (client-side)
});

cost.total.format();          // "1.23 SEK"
cost.marginalInclVat.format() // "0.92 SEK"  — what these kWh actually cost
cost.presentation.total       // { amount: 0.13, currency: 'USD' }

The cost model

A Swedish bill is two companies plus the state, modelled as composable tariff components:

PieceBilled byWhat it is
Spot energyProvider (elhandel)Day-ahead market price per bidding zone, 15-min.
Provider påslag / feesProviderMarkup + variable costs + monthly fee.
Grid transfer + fixedGrid operator (elnät)Per-kWh transfer + a fixed subscription per fuse.
Energy taxState, via the grid invoiceEnergiskatt per kWh.
VAT (moms)State25% on top of everything — including the tax.

Provider and grid are billed on separate invoices; powercost returns one combined, itemised breakdown.

Marginal vs. total

  • marginalInclVat — cost of the kWh that scale with consumption (spot + per-kWh fees + grid transfer + tax + VAT). What a load-shifting / net-profit decision needs.
  • total — the full bill including amortised fixed fees (subscription, fuse). What you actually pay.

Live data & CORS

With live: true, prices are fetched directly from the browser — no proxy:

  • Spot from elprisetjustnu (sends access-control-allow-origin: *).
  • Presentation FX from the ECB Data Portal API (also CORS-open).

Prefer to supply your own prices? Pass spot and/or fx sources instead of live. FX is presentation-only; the cost itself is computed in the tariff's native currency with exact integer money.

Data packs

Tariff data is separate from the engine and loaded on demand, so you only ship what you use:

const { swedenDb } = await import('powercost/data/se');

A pack exposes a TariffDb (provider plans, grid operators, national tax + VAT). New countries are new packs — see contributing.

price(options)

OptionTypeNotes
consumption{ start, end, kwh }[]ISO-8601 timestamps; any interval length.
configConfig{ zone, providerPlanId, gridOperatorId, fuse?, reportCurrency? }
dbTariffDbFrom a data pack.
livebooleanFetch spot (+ FX) live.
spot / fxsourcesBring your own prices instead of live.

Returns a Promise<CostBreakdown>.

CostBreakdown

{
  period, currency, totalKwh,
  total, marginalInclVat, variableExVat, fixedExVat,   // Money
  byCategory: { energy, grid, tax, subscription, vat }, // Money
  lines: [{ id, label, category, basis, amount }],
  presentation?: { currency, rate, total, marginalInclVat },
}

Every money value is a Money with .format(), .toMajor(), .toJSON(). Use serializeBreakdown(b) for plain JSON.

reconcile()

Compare a computed breakdown to a real invoice total to measure accuracy:

import { reconcile } from 'powercost';
const r = reconcile(cost, { actualTotalMajor: 412.50, currency: 'SEK' }, 0.02);
r.withinTolerance; r.relDiffPct;

Low-level

The pieces behind price() are all exported: priceConsumption, resolveRateCard, loadElprisetSpot, loadEcbFx, StaticSpotSource, StaticFxSource, plus the tariff component types. Build your own flow if you need to.

Config values

The Sweden pack (powercost/data/se):

FieldValues
zoneSE1, SE2, SE3, SE4
providerPlanIdtibber-kvartspris, tibber-rorligt
gridOperatorIdvattenfall-e4-25a, vattenfall-se3

Contributing a pack

A data pack is just a module exporting a TariffDb built from the component grammar (fixed, per_kwh incl. spot-linked, tiered, demand, vat) — all temporal. Add a country or operator, send a PR. See the repo and the Sweden pack as a template.