Overview
The Levelized Cost of Hydrogen (LCOH) is the minimum price at which hydrogen must be sold over the lifetime of a production facility to recover all costs, including capital expenditure, operating expenses, and a required return on investment. It is the hydrogen-sector equivalent of the Levelized Cost of Energy (LCOE) used in power generation.
LCOH is the primary metric used by project developers, investors, and policymakers to compare hydrogen production pathways across different technologies, geographies, and market conditions. A lower LCOH generally indicates a more economically competitive production route.
GaugeH2 implements a full Discounted Cash Flow (DCF) methodology rather than a simplified parametric approach. This means every calculation reflects the time value of money, financing structure, and multi-year operating profiles — not just a levelized average.
DCF Formula
The LCOH is the ratio of the present value of all costs to the present value of all hydrogen produced. The expression below is the one the engine evaluates, transcribed from §3 of the sealed methodology document — not a simplification of it.
P0 = capacity_MW x 1000 x operating_hours x (availability_pct / 100)
/ electricity_consumption_kwh_kg
E = P0 x electricity_consumption_kwh_kg (the kWh bought, every year)
age(t) = years since the last stack replacement, at the start of year t
(t - 1 until the first one; 0 the year after each one)
P(t) = P0 x (1 - d)^age(t) d = stack_degradation_pct / 100
infl(t) = (1 + f)^(t-1) f = inflation_rate_pct / 100
df(t) = (1 + i)^t i = wacc_pct / 100
electricity(t) = E x (price_eur_mwh / 1000) x infl(t)
opex(t) = CAPEX_physical x (om_pct / 100) x infl(t)
water(t) = P(t) x water_l_kg x (water_cost_eur_m3 / 1000) x infl(t)
heat(t) = P(t) x heat_demand_kwh_kg x (heat_cost_eur_mwh_th / 1000) x infl(t)
optional (7.1.0): 0 unless both heat inputs are given
stack(t) = CAPEX_electrolyser x (stack_cost_pct / 100) x infl(t)
when t mod stack_replacement_interval = 0 and t < N, else 0
CAPEX_capitalised + SUM over t=1..N [ cost(t) / df(t) ]
LCOH = ----------------------------------------------------------------
SUM over t=1..N [ P(t) / df(t) ]The prices and costs entered are those of the first operating year: infl(1) = 1, and every later year is escalated at the inflation rate of the inputs. So i is a nominal rate: the costs it discounts are nominal, and a real rate would mix two money bases.
The LCOH is therefore a nominal levelized price: the one price per kilogram which, charged unchanged every year, recovers those costs discounted at i. It is a figure in the money year declared for the inputs only if all the inputs are in that year’s money and the inflation rate is zero.
Since model 6.1.0 the calculator also publishes, beside it, the LCOH in constant money: the same project with nothing escalated, discounted at the real WACC, (1 + i) / (1 + f) − 1, in the money of the inputs. With a zero inflation rate the two are the same figure.
Four properties of that expression are easy to get wrong and each one is pinned by a test:
- The capital sits at t = 0 and is not discounted. It is also the capitalised figure, while the O&M base and the stack-replacement base are the physical figure. They are three different quantities and the engine publishes all three rather than conflating them.
- The operating years run t = 1 to N, discounted end-of-year.
- A stack replacement restores the output of year 1, from the year after it. Degradation follows the age of the stack, not of the project (model 5.3.0). The replacement is charged in its own year, which still runs on the stack being replaced.
- The last-year replacement is suppressed (t < N): buying a stack the plant will never use would be a modelling artefact, not a cost.
The construction capitalisation factor. CAPEX_capitalised = CAPEX_physical x CF(T, i), where T = construction_months / 12 and i is the WACC: CF = SUM over s [ a_s x (1 + i)^(T - s) ], with the yearly disbursement fractions a_s taken from Seddon's Table A7-1 (non-uniform). A build of zero months gives CF = 1; a fractional duration interpolates the result linearly between the tabulated years; builds beyond five years use the five-year profile. The table, the interpolation and a worked example are in §4–5 of the frozen methodology document.
What the degradation convention does and does not say. Since model 5.3.0 degradation is modelled as lost efficiency: the stack draws the same power every year and produces less the older it is, so each kilogram takes more electricity, and a replacement restores the output of year 1 from the year after it. Until model 5.2.0 it was modelled as lost production at constant efficiency, with no reset: the electricity per kilogram was flat, and degradation raised the LCOH only by spreading fixed costs over fewer kilograms. This page called that convention conservative, and on the T-HYNET sample it was not — 9.89 EUR/kg against 10.09 under the new model — because a flat electricity cost per kilogram understates what an ageing stack costs by more than the missing reset overstates it. A calculation stored before 5.3.0 declares no degradation model and keeps the earlier one; its report says which.
Sensitivity. Each row of the sensitivity table moves one input and holds the rest at their base values, and prints beside itself how far it moved it. Since model 5.3.0 the electrolyser cost moves across the accuracy band of the declared estimate class, the inflation rate one percentage point either way, and the other inputs by the same fixed fraction. No row asks for a value the calculator would reject: an end that would leave the range an input accepts, such as the 8,760 hours of a year, stops at that edge, and the report says which row stopped and where.
The model uses a 20-year default project lifetime and a default stack replacement interval that depends on the technology — 10 years for PEM, 13 for alkaline, 6 for SOEC and 8 for AEM; both are user-configurable, and the interval is a calendar interval, so a plant running far from 4000 h/year should be adjusted to its own duty.
Scope: what this cost excludes
This levelized cost excludes working capital. It covers the plant and its operation, not the funds tied up in stock, receivables and payables while the plant runs. Published factorial guidance puts working capital at 10 to 20 per cent of fixed capital investment (Towler and Sinnott, Chemical Engineering Design, chapter 7); it is recovered when the plant stops, so it is carried as an annual charge on the funds rather than amortised with the capital. Nothing computed here includes that charge, so a figure that has to cover it will be higher than the one above.
No figure is escalated to another money year: this model version applies no cost index to any figure it computes or prints. The Nelson-Farrar refinery construction cost index series runs from 1946, where it stands at 100, to 2018, the base year of its escalation factors. When the declared money year falls before that base, the money-year line of the comparability block states the factor that escalating to 2018 money would apply, and that it is not applied; from the base year on, it states that the series ends there. That factor uses the index with its published weighting, 60 per cent labour and 40 per cent materials. That weighting is the one its publisher, the Oil & Gas Journal, maintains for continuity of the series; the same publisher reports that surveyed project composition has moved towards 40 per cent labour, and publishes the series at five weightings so an evaluator can match their own project. The index is a refinery construction index, accurate for the US Gulf Coast and described as suited to petroleum and petrochemical work, not an electrolysis index. No alternative weighting is applied.
Corrected 21 September 2026. From methodology 2.9 to 3.5 this paragraph said that money years are aligned with the Nelson-Farrar index, that the index is applied here to electrolysis capital, and that the figures above use its weighting. No figure computed or printed by this platform is, or has been, escalated to another money year.
Key input variables
The input contract declares 34 fields, grouped below. The count is read from the contract itself (src/lib/validation/calculation-input.ts), so it cannot drift from what the API accepts.
- Comparability declaration (5): system boundary, energy basis, money year, site basis, estimate class
- Electrolyser and plant: technology, capacity (MW), CAPEX (€/kW), balance of plant (%), specific consumption (kWh/kg), operating hours, availability (%), project lifetime
- Stack: degradation (%/year), replacement interval (years), replacement cost (% of electrolyser CAPEX)
- Consumables: electricity price (€/MWh), water price (€/m³), water consumption (l/kg)
- Operating and financial: O&M as % of physical CAPEX, WACC, inflation, subsidy (€)
- Capital charge (optional, model 5.0.0): construction period (months), CAPEX reference capacity, scale exponent, and the four itemised CAPEX surcharges. Every one has a documented no-op, so an input written for 4.1.1 reproduces its 4.1.1 result.
- Per-parameter declarations (optional): the source kind and cost year the author states for any individual input and, if the author adds them, how the figure is derived, the document behind it, cited by its SHA-256 fingerprint (the file itself is never uploaded), and a web source with its publication and reading dates. The report prints all of it as stated by the author, not verified.
Two things are not in that list, and both matter for reading a result. Debt share, interest rate, tenor, tax rate and depreciation are TEA inputs: they change the returns of a project, never its cost of production, so they do not enter the LCOH. And the calculator prices no compression, storage or delivery: its figure stops at the plant gate, as every report declares on its first page. An author whose own figures already carry a leg past the gate — compressed to a stated pressure, or liquefied — can declare it, and the report then states the boundary as delivered. The platform checks only that the declared electricity consumption is large enough to contain that leg, and refuses the calculation when it is not.
TEA: Investor Metrics
The techno-economic analysis (TEA) runs the same discounted cash flow as the LCOH and adds the capital structure. Capital is sized on the CAPEX net of subsidy and, when a construction period is declared, capitalised over it: the cash the promoter must actually raise. Debt is that capital times the declared debt share, repaid as a level annuity over the tenor at the declared interest rate; equity is the rest. The frozen document, calculator-methodology-v7.1.0.md §7, is the authoritative specification; this section summarises it.
- NPV and project IRR — on the unlevered project free cash flow: revenue − recurring opex − stack replacement − tax on (revenue − opex − stack − depreciation), with no interest deduction, discounted at the WACC; year 0 is minus the capital funded. Financing cost lives in the WACC, not in the flows.
- Equity IRR — on the levered equity flow: minus the equity at year 0, then revenue − opex − stack − reserve contribution + reserve drawdown − debt service − tax, where the tax base deducts interest. Not reported when it does not resolve or when there is no equity.
- IRR solver — every IRR (project, equity, each Monte Carlo run) comes from one solver: Newton's method from 10 %, falling back to bisection on the first sign change of the NPV when Newton does not converge. An IRR exists when the NPV changes sign.
- DSCR, after tax (since 7.0.0) — for each year inside the debt tenor: (revenue − recurring opex − maintenance-reserve contribution − the cash tax paid that year) ÷ debt service. The report gives the minimum and the average. Every stack replacement inside the tenor is funded by a reserve in equal contributions over the years of that stack. Without debt there is no DSCR: it is reported as "n/a", never as 0.
- Breakeven price — the hydrogen selling price (€/kg) at which the project NPV is zero, found by bisection between 0 and 100 €/kg. When the project does not reach a zero NPV in that range, no figure is printed: it is reported as not viable in range.
- Payback — the simple payback is the first operating year in which the cumulative equity cash flow turns non-negative; the discounted payback does the same with each year's equity cash flow discounted at the WACC. Neither is reported when it never happens.
- Project-finance options (since 7.1.0, all optional) — LLCR and PLCR: the present value, at the debt rate, of the cash flow available for debt service over the remaining loan life (LLCR) or the project life (PLCR), divided by the debt outstanding (Yescombe, Principles of Project Finance, §12.4). A repayment sculpted so each year's DSCR equals a target, flagged with a final-year balloon when the target cannot repay the debt within the tenor. A DSRA in months of debt service, funded from equity at the start of operation and released at maturity. The carry-forward of tax losses without time limit, with an optional cap and floor (Spain, Corporate Income Tax Act art. 26: 70 % and 1 M€); it applies to the tax paid, not to the unlevered project flow. Without any of them every figure is that of 7.0.0.
The NPV, the project IRR, the breakeven price and the payback are unchanged by the 7.0.0 corrections; the DSCR moved to after tax, and an IRR that Newton missed now resolves. Stored calculations keep the figures they were computed with.
Technology Assumptions
Platform defaults for a first estimate, for four electrolysis technologies. The PEM and alkaline CAPEX are installed-system figures (EUR 2024, installation included, non-Chinese manufacturer) from the IEA Global Hydrogen Review 2025 (2024 data), via ACER CHEST; cross-checks: BloombergNEF Electrolyzer Price Survey 2024 (~2,500 USD/kW Europe), Hydrogen Europe Clean Hydrogen Production Pathways 2024 (2,250 EUR/kW alkaline, 100 MW, all-in). Accessed Oct 2026. Because they are installed figures, the default Balance of Plant is 0 %; adding a BoP on top would count it twice. Typical installed range: 1800–3000 €/kW; a low case of 1170 €/kW applies to low-cost or Asian equipment and excludes EPC and owner's costs. The SOEC and AEM defaults are platform assumptions with no published source. Every report marks each default as a platform default, so a reader can tell it from a project figure. Replace them with figures for the actual project.
PEM (Proton Exchange Membrane)
- Default CAPEX: 2365 €/kW (installed system, EUR 2024)
- Efficiency: 50 kWh/kg H₂
- Default degradation: 1%/year
- Stack: replaced every 10 years at 40% of electrolyser CAPEX
- Ramp-up capability: suitable for variable renewables
Alkaline
- Default CAPEX: 1819 €/kW (installed system, EUR 2024)
- Efficiency: 52 kWh/kg H₂
- Default degradation: 0.5%/year
- Stack: replaced every 13 years at 35% of electrolyser CAPEX
- Ramp-up capability: limited; suited for baseload operation
SOEC (Solid Oxide Electrolyzer Cell)
- Default CAPEX: 2000 €/kW
- Efficiency: 38 kWh/kg H₂ (high-temperature heat integration assumed)
- SOEC: the electricity consumption assumes an external supply of high-temperature heat, and that heat is not costed in this model (it enters at zero cost). If the plant has to buy or generate that heat, the LCOH shown understates the real cost.
- Since model 7.1.0 the heat can be costed: give a heat demand (kWh_th per kg H₂) and a heat price (€/MWh_th) and heat(t) enters the LCOH numerator and the TEA operating cost. Suggested demand for SOEC: 8 kWh_th/kg — the US DOE system targets for high-temperature electrolysis, 44 kWh/kg of total energy minus 36 kWh/kg of electricity (source). It is not filled in automatically: the price of heat depends on the site.
- Default degradation: 1.5%/year
- Stack: replaced every 6 years at 50% of electrolyser CAPEX
- Applicability: industrial heat integration, nuclear coupling
AEM (Anion Exchange Membrane)
- Default CAPEX: 1100 €/kW
- Efficiency: 51 kWh/kg H₂
- Default degradation: 0.8%/year
- Stack: replaced every 8 years at 45% of electrolyser CAPEX
- Status: early commercial; higher uncertainty bands
The PEM and alkaline default costs are EUR 2024 (see above). The vintage of the SOEC and AEM default costs is not on file: the platform does not state a year for figures whose year it does not have. The money year is declared by whoever runs the calculation and travels with it. O&M is modeled as a flat 2% of CAPEX, gross of subsidy, per year for every technology. The stack life and its replacement cost differ by technology, as listed above; they are platform assumptions that follow the ordering the literature agrees on — alkaline longest, solid oxide shortest — not figures from a published source. Users can override any parameter to model future cost trajectories or project-specific conditions.
Data Sources
What the platform reads as data, and what it only cites.
OMIE day-ahead market
Day-ahead electricity prices for Spain and Portugal, ingested daily. When a calculation anchors its electricity price to the market, this is the price, and the report prints its window and method beside it. Source: OMIE day-ahead market (omie.es).
Catalogue benchmark
The benchmark is this engine’s LCOH for the public catalogue projects: each takes its technology, capacity and announced investment from the catalogue, and every other assumption from a frozen, published base. It is what our model gives for announced projects under declared assumptions — a modelled figure, not the cost of producing hydrogen, and not validated by the projects’ developers. No user calculation enters it. We report percentile distributions rather than averages to reflect the heterogeneity of the catalogue.
Corrected 21 September 2026. Before this correction, this section said the benchmark was derived from calculations performed on the platform by its users, and that it represented real projects with real parameters rather than modelled scenarios. Since 23 August 2026 no user calculation enters it, and it is a modelled figure by construction.
Euro-area consumer prices (HICP)
Eurostat, harmonised index of consumer prices (HICP), all items, euro area (changing composition): annual average index, 2015 = 100 (prc_hicp_aind), from 1996 to 2025; for 2026, the monthly index, 2025 = 100 (prc_hicp_minr), averaged over the months published and chained to the 2025 average, read on 4 October 2026. It is used for one thing: to place a figure on the catalogue band, which is in 2024 money, a figure declared in another money year from 1996 to 2026 is converted to 2024 money with the ratio of the two years' index, and both figures are shown. The figure itself, its fingerprint and every report stay in the money it declares. 2026 is provisional, the average of the months published, until Eurostat publishes the year. It is a consumer price index, not an electrolysis cost index: no public index measures the cost of an electrolyser.
References
NREL’s Annual Technology Baseline, the IEA and IRENA are cited on this page for conventions — the subsidy convention below, for instance. The platform does not ingest their data: no default is updated from them and no input is validated against them.
Refresh Cadence
The catalogue, the market data and the derived benchmarks are updated by scheduled jobs. They run on the free tier of GitHub Actions, where a scheduled job can start several hours late, and the operator can also start any of them by hand. The times below are the scheduled times, checked against each job’s definition.
Refresh jobs
- OMIE ingest — Daily 01:00 UTC. OMIE day-ahead prices for Spain and Portugal, with a rolling three-day backfill. This is what feeds the market-anchored electricity price in a report.
- Recalculate benchmarks — Daily 02:00 UTC. The catalogue benchmark: LCOH percentiles per region and technology, computed by this engine for public catalogue projects under a frozen base of declared assumptions.
- Auto-enrich — Sundays 02:00 UTC. Model-assisted field completion for verified catalogue rows below the completeness threshold. Off by default; it runs only when a repository variable enables it.
- Recalculate scores — Sundays 03:00 UTC. Readiness and completeness for every catalogue project. Powers the “Scores recomputed” indicator on /projects.
- Policies scrape — Daily 05:00 UTC. EU, UK and national regulator feeds for the policy tracker.
- Verify source links — Sundays 05:00 UTC. Re-checks the source link of every verified catalogue project.
- News scrape — Daily 06:00 UTC. Hydrogen news feeds through a classifier, an extraction agent and a research verifier, into the catalogue review queue.
- BDNS ingest — Mondays 08:00 UTC. Spanish public-subsidy calls (BDNS) for the subsidy schemes a calculation can cite.
- Cost index — Mondays 09:00 UTC. The published Iberian green hydrogen cost index, over a rolling 30-day OMIE window.
What “Scores recomputed” means
The freshness indicator at the top of /projects reflects the timestamp of the most recent converging recalculate-scores run. It is a convergence-layer signal — it tells you when the score recomputation last completed, not whether every individual field on every row is up to date. The date of each project’s own last update is a different thing, and can be months older. A specific row may carry a stale score if its data was updated through a path that bypasses the recalc trigger (for example, a manual curation script). The next converging recalc closes that gap.
Model Versioning
Every calculation is tagged with the model version used to produce it. Model versions follow semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR — the same stored inputs legitimately produce materially different outputs.
- MINOR — what moves is what the input contract writes into new calculations, or an output is added beside the others, and a stored input re-run gives what it always gave (5.3.0, 6.1.0 and 6.2.0 are of this kind).
- PATCH — bug fixes, UI changes, or non-material documentation updates.
Issued reports are never retroactively recalculated: a report carries the version pair that produced it.
Current production model: v7.1.0 (LCOH / TEA Calculator), methodology v3.33. The methodology text for v7.1.0 is frozen and content-addressed: its sha256 is published beside it and embedded in the reference cases shipped with the calculator, so a figure can be tied to the exact method text that produced it. The frozen document is published at calculator-methodology-v7.1.0.md, beside its sha256; it defines the LCOH, the construction factor and the TEA investor cash flow in full. Earlier model changes are described below; the methodology changes since that text are in the published changes document linked after them.
v5.0.0 capitalises the construction period. Capital spent during a build earns nothing until the plant starts, and the interest accrued over that period belongs in the capital charge; before this release it was absent altogether. The factor follows the ICI/Stratton concept-estimate method, using the non-uniform disbursement profile of the source rather than a flat split across the build years — a flat split front-loads spend the real profile defers, and overstates the interest. The period is an input (construction_months, 18 months in the shipped defaults); declaring zero reproduces the previous capital charge exactly, which is what makes the change auditable figure by figure. The same release corrects an asymmetry in the stack replacement — it was priced in year-0 money while the O&M beside it in the same loop was escalated — and sizes debt, equity, depreciation and NPV on the capitalised capital, the cash a promoter actually has to raise. capex_total keeps its meaning and a new capex_funded is published beside it. On the reference case the LCOH moves from 4.53 to 4.71 €/kg (+4.1%): +1.05% from the stack correction and +3.06% from the capitalisation, isolated and measured separately so neither hides inside the other. The methodology moves to v2.3 rather than a major: the change is additive — a cost term that was silently absent is now present and documented — and every previously published definition still holds.
v4.1.1 corrects the CAPEX base of the LCOH: the levelized cost of hydrogen is now computed gross of subsidy — both the O&M base and the CAPEX component use the full system cost, independent of any capital grant. This follows the convention of IRENA’s cost analyses, which exclude “the impact of government incentives or subsidies”, and of the US DOE Hydrogen Program Record 24005, which reports “untaxed and unsubsidized” hydrogen production cost; NREL’s ATB applies it to tax credits in its R&D-only case. A subsidy therefore no longer lowers the reported LCOH; it is retained where it economically belongs — in the investor cash flow (TEA NPV / IRR). The same release replaces the breakeven price ceiling: a project that never reaches profitability within the modeled range is now labelled explicitly instead of shown as a saturated figure. The methodology version is unchanged (v2.2) — the financial method is the same; only the LCOH cost base was corrected.
The EU Hydrogen Observatory’s LCOH calculator instead carries subsidies as a term of its formula, zero in its default values: compare this figure against its default, not against a subsidised run.
Corrected 20 September 2026. This paragraph previously attributed the convention to NREL ATB, the IEA and the EU Hydrogen Observatory. Checked against their documents: the EU Hydrogen Observatory’s calculator includes subsidies as a formula term, the IEA’s Global Hydrogen Review 2025 assumptions annex does not address subsidies, and NREL’s ATB addresses tax credits rather than capital grants.
v6.2.0 changes what a take-or-pay charges. The buyer pays for committed volume the plant produced and did not take, and no longer for volume the plant did not produce: until v6.1.0 a plant making less than its committed minimum was paid the difference, so cutting its operating hours could raise its NPV. A commitment above what the plant produces is now reported for what it is, a shortfall on the seller's side that earns nothing here and that this model does not price. Nothing moves without a take-or-pay. The rule travels inside the inputs of every new calculation, so a calculation stored before it keeps its model, its figures and its hash, and its report is re-issued as its engine made it.
v6.1.0 changes what the DSCR measures and publishes two things beside what it already did. The DSCR deducts a maintenance reserve: each stack replacement inside the debt tenor is funded in equal contributions over the years of that stack, and the reserve pays it, where until v6.0.0 the replacement stayed out of the numerator. On the reference case the minimum DSCR moves from 1.64 to 1.05; the NPV and the project IRR do not move. The LCOH is also published in constant money beside the nominal figure, and the TEA sensitivity moves the electrolyser cost across the band of its estimate class, as the LCOH one already did. The reserve rule travels inside the inputs of every new calculation, so a calculation stored before it keeps its model, its figures and its hash, and its report is re-issued as its engine made it.
v6.0.0 changed the Monte Carlo TEA only: a simulation whose project IRR does not exist is no longer discarded, because that is a simulation in which the project loses money. It counts in the NPV, the DSCR and the payback, and is left out of the IRR distribution alone, which publishes how many there were.
v5.3.0 changes two things the engine computes, and nothing else: the stack degrades by lost efficiency and a replacement restores its output, and the sensitivity moves the electrolyser cost across the band of its estimate class and adds the inflation rate. A calculation stored before it keeps its model, its figures and its hash; the degradation model travels inside the inputs of every new one.
Reports print the methodology version they were issued under; the current one is Methodology version: v3.33. Every change to what the reports assert since the frozen v7.1.0 method document above — the model changes to v5.3.0, v6.0.0, v6.1.0 and v6.2.0 included — is published, content-addressed, at calculator-methodology-v7.1.0-m3.33-changes.md, beside its sha256.
Monte Carlo Engine
Single-point LCOH and TEA estimates compress real uncertainty into one number. The Monte Carlo engine propagates input distributions and correlations through the full DCF model, producing percentile distributions for LCOH, NPV, IRR, and DSCR — together with a tornado chart that ranks which inputs drive output variance.
The engine targets the workflows that previously lived in Excel + @Risk or proprietary consulting models: defensible distributions, explicit correlation structure, regression-based sensitivity, and reproducible outputs designed to be auditable. Every run is bit-deterministic given a fixed seed.
Architecture overview
- Default 10,000 simulations per run. Percentiles reported: P10, P25, P50, P75, P90.
- A deterministic seed yields the same draws on the same runtime: the sampler adds no randomness of its own beyond the seed. The same seed and inputs produce the same SHA-256 output hash on that runtime (see Hash Verification).
- Clean separation between inputs (variable configurations plus correlation matrix) and outputs (percentile distributions plus standardized regression tornado).
- State persists across navigation: closing and reopening the simulator restores the most recent configuration and seed.
Probability distributions
The engine offers six distribution families: Triangular, Uniform, Normal, Truncated Normal, Lognormal and PERT. Every family is configured by its minimum, most-likely value and maximum. When a correlation is active every variable is sampled by inverse-CDF transform through the copula below; without correlation each family uses its own direct sampler (Box-Muller for the normals, Gamma draws for the PERT). Enabling a correlation therefore changes the whole draw sequence for the same seed.
- Normal and Truncated Normal. Both are centred on the most-likely value with
σ = (max − min) / 4, and both keep samples inside[min, max]. They differ in how: the truncated normal samples the renormalized density on that interval; the normal redraws out-of-range samples and, after 100 failed attempts, returns the mean. - Triangular(min, mode, max). Expert judgment with three points and no shape preference. PDF is piecewise linear: rising to the mode, falling to the max. Sampling: inverse-CDF in closed form.
- Uniform(min, max). Non-informative prior when only bounds are known (e.g., regulated tariff bands, permit-window durations). PDF:
f(x) = 1 / (max − min). - Lognormal(μ, σ). Strictly positive, right-skewed quantities such as electricity prices, commodity costs, or downtime intervals. If
X ~ Lognormalthenln(X) ~ N(μ, σ²). Sampling:x = exp(μ + σ · Φ⁻¹(u)). - PERT(min, mode, max). A Beta reparameterization that weights the mode more strongly than Triangular. Useful for electrolyzer CAPEX, where 2030 estimates have asymmetric upside and a clear consensus mode. Internally:
α = 1 + 4 (mode − min) / (max − min),β = 1 + 4 (max − mode) / (max − min).
Gaussian copula for correlations
Real H₂ project variables are correlated: high electricity prices tend to coincide with fewer economic operating hours, and balance-of-plant cost moves with electrolyser cost. Sampling marginals independently understates tail risk and produces over-optimistic P10/P90 spreads.
The engine imposes a correlation structure via a Gaussian copula, which preserves each variable's distributional shape while inducing the target rank correlation:
1. Draw z ~ N(0, Σ) // multivariate normal with target correlation Σ
2. Map u_i = Φ(z_i) // each component to U[0,1] via standard-normal CDF
3. Map x_i = F_i⁻¹(u_i) // to the target marginal via its inverse CDFThe correlation matrix Σ must be positive semi-definite (PSD). PSD is validated on the client before submission and re-validated on the server; a matrix that is not PSD is rejected when its Cholesky factorisation fails, with a diagnostic naming the row whose pivot went negative. The simulator pre-loads six default pairs, which users can override or extend. They are a GaugeH2 platform assumption — directional, not sourced from a published correlation matrix:
electricity_cost_eur_mwh↔operating_hours: −0.4 (LCOH and TEA)electrolyzer_cost_eur_kw↔bop_pct: +0.5 (LCOH and TEA)wacc_pct↔interest_rate_pct: +0.7 (TEA)wacc_pct↔debt_share_pct: −0.3 (TEA)capex_overrun↔electrolyzer_cost_eur_kw: +0.4 (TEA)hydrogen_selling_price↔electricity_cost_eur_mwh: +0.5 (TEA)
Standardized Regression Coefficients (SRC) tornado
Earlier engine versions ranked drivers by normalized Spearman² — robust but coarse, and stripped of sign. The current engine reports Standardized Regression Coefficients (SRC), which are interpretable, signed, and standard practice in quantitative risk analysis.
For each simulation set, fit an OLS regression of the standardized output on the standardized inputs:
Ŷ* = Σ β_i · X_i* where Z* = (Z − mean(Z)) / std(Z)
β_i interpretation:
"a 1σ change in input i moves the output by β_i σ,
holding all other inputs fixed."- Magnitude
|β_i|determines the tornado ranking. - Sign of
β_ishows direction: positive increases the output, negative decreases it. - R² is reported alongside the tornado. When
R² < 0.7the results panel shows a warning: SRC is a linear approximation, and significant non-linearity reduces its reliability — interpret rankings cautiously and complement with scenario analysis. - For TEA simulations the regression target is NPV (approximately linear in inputs), not IRR (highly non-linear near zero crossings). LCOH simulations target LCOH directly.
Goodness-of-fit validation
The sampler is validated against analytical references on every commit. Two complementary test layers:
- Kolmogorov-Smirnov tests at
N = 10,000withp > 0.05for all six distribution families. The KS statistic measures the maximum gap between empirical and theoretical CDFs; passing the test means the sample is statistically indistinguishable from the target distribution at production sample size. - Analytical benchmarks. Degenerate distributions (zero variance) collapse to the deterministic value; closed-form NPV under constant cash flows matches the analytical solution to machine precision; sample mean and standard deviation converge to the theoretical moments as
Ngrows.
These tests run in CI on every push to the main branch.
Reproducibility & verification
The engine is deterministic: the same seed and the same inputs produce the same outputs on the same runtime, with no hidden state between runs. Across runtimes the last digits can differ, which is why each calculation records the one it ran on. Each run's full result set — percentiles, summary statistics, tornado coefficients, R², seed, and model version — is hashed with SHA-256 and stored alongside the calculation. A third party can re-run with the recorded seed and inputs, hash the result, and confirm bit equivalence. See Hash Verification for the full verification workflow.
Hash anchoring and independent verification
What you can verify yourself
Every GaugeH2 calculation produces two SHA-256 hashes stored alongside the row:
- Event hash (
hash_sha256) — a fingerprint of the specific run. Includes a server-side timestamp, so two runs of the same inputs produce DIFFERENT event hashes. Used as the legacy lookup key for the URLs printed on every PDF. - Content hash (
content_sha256) — a fingerprint of the calculation's content. Computed over a canonical JSON representation of the inputs, outputs and model version. The recipe is portable: recomputing the canonical bytes over a published payload gives the same SHA-256 in any runtime that follows it, and we validate that between JavaScript and Python. Re-deriving the outputs from the inputs is not: the engine discounts with Math.pow, which ECMAScript does not require to be correctly rounded, and V8 changed it between Node majors. So every calculation records the runtime that produced it, and the verifier publishes it as node_version — that is what you reproduce on. This page documents the recipe so a third party can recompute the hash on their own machine and confirm a match against the value we publish.
Both hashes resolve through the same endpoint: /api/verify/[hash]. The response is identical in shape; a matched_by field tells the caller whether the URL matched against the event-hash column or the content-hash column.
Since 24 September 2026 the SHA-256 of a sealed PDF resolves through that endpoint too, to the calculation it was issued from, with matched_by set to pdf and the model and methodology versions that PDF carried. Before that date only the drag-and-drop verifier resolved it. A shared link in verifiable mode lists the SHA-256 of the sealed PDFs issued from its calculation, so whoever received one can check it is one of them.
The content payload (what gets hashed)
The canonical content payload is the only thing that feeds the SHA-256. Field names are snake_case. The set depends on the calculation type. Source of truth: src/utils/hash.ts (function buildContentPayload).
| Calculation type | Fields in the payload |
|---|---|
lcoh, tea | { type, inputs, outputs, model_version } |
monte_carlo_lcoh, monte_carlo_tea | { type, inputs, outputs, model_version, seed, correlation_matrix } |
What is NEVER in the payload (and why):
timestamp— the whole point of content_sha256 is to be reproducible without knowing when the calculation ran.methodology_version— it versions what the documents assert, not what the engine computes. A formula change movesmodel_version, which is in the payload: the seventeen methodology changes from v2.5 to v3.12 moved no formula and no output, and v3.13 came with model v5.3.0 because it did, as v3.14, v3.15 and v3.28 came with v6.0.0, v6.1.0 and v6.2.0.created_at,user_id,calculation_id,hash_sha256,pdf_sha256,reviewed_at,verification_tier— facts about the event or row metadata, not about the content.
The canonical-JSON recipe
Five steps, all deterministic. Implementations are byte-equivalent across runtimes that follow the recipe; Section 5 ships two reference implementations.
- Build the payload object per the type-specific schema above.
- Recursively sort all object keys lexicographically by code point (ASCII keys behave identically under the JavaScript
Object.keys(o).sort()default and Pythonsort_keys=True). Arrays preserve order; their elements are still sorted by key if they are objects. - Serialise to JSON with the most compact separators:
(",", ":"). No whitespace inside the document. - Preserve UTF-8 — do NOT escape non-ASCII characters (Python users: pass
ensure_ascii=False). - UTF-8 encode the resulting string and compute SHA-256 over the bytes.
Reference implementation in JavaScript: the fast-json-stable-stringify npm package (v2.1.0, used in production). In Python (3.6+): json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":")). Both produce byte-identical output for the LCOH / TEA / Monte Carlo float range we exercise today, with every numeric value going through the same “shortest decimal that round-trips” algorithm.
Worked example — LCOH
A synthetic LCOH calculation produced under model 4.0.1. The engine in production today is 7.1.0, and this example has deliberately not been regenerated: what it teaches is the recipe for the hash — how the canonical bytes are built and what SHA-256 they produce — and that does not depend on the model version. It is also exactly what /verify checks.
So read it for what it is. Re-running these inputs on today’s calculator will not reproduce these outputs: the input contract has gained required declarations since 4.0.1 — the five comparability dimensions among them — and the engine capitalises the construction period differently. Its region is likewise from that era’s vocabulary and is not one of the eight the calculator offers today. The outputs below are the ones calculateLCOH(inputs) produced then, byte for byte. Re-run the recipe over the payload as published and you will obtain the canonical bytes and the expected SHA-256 below.
Inputs + outputs (payload, pretty-printed)
Show payload JSON (11697 chars pretty-printed)
{
"type": "lcoh",
"inputs": {
"technology": "PEM",
"region": "EU-Iberia",
"electrolyzer_capacity_mw": 100,
"electrolyzer_cost_eur_kw": 1000,
"bop_pct": 25,
"subsidy_eur": 0,
"electricity_cost_eur_mwh": 45,
"electricity_consumption_kwh_kg": 53,
"operating_hours": 5000,
"availability_pct": 95,
"water_cost_eur_m3": 1.5,
"water_consumption_l_kg": 10,
"om_pct": 2.5,
"project_lifetime_years": 25,
"wacc_pct": 8,
"stack_degradation_pct": 1.5,
"stack_replacement_interval": 7,
"stack_replacement_cost_pct": 40,
"inflation_rate_pct": 2
},
"outputs": {
"lcoh_eur_kg": 5.28,
"annual_production_kg": 8962264,
"lifetime_production_kg": 188004702,
"capex_total": 125000000,
"cost_breakdown": {
"electricity_pct": 52.928777421597694,
"capex_pct": 27.8854185529534,
"opex_pct": 8.835519848036894,
"stack_pct": 10.017398784823358,
"water_pct": 0.33288539258866484
},
"annual_cashflow": [
{
"year": 1,
"production_kg": 8962264.150943397,
"electricity_cost": 21375000,
"opex_cost": 3125000,
"water_cost": 134433.96226415096,
"stack_replacement": 0,
"total_cost": 24634433.96226415,
"discounted_cost": 22809661.076170508,
"discounted_production": 8298392.732354997
},
{
"year": 2,
"production_kg": 8827830.188679246,
"electricity_cost": 21475462.500000004,
"opex_cost": 3187500,
"water_cost": 135065.80188679247,
"stack_replacement": 0,
"total_cost": 24798028.301886797,
"discounted_cost": 21260312.33014986,
"discounted_production": 7568441.519786733
},
{
"year": 3,
"production_kg": 8695412.735849056,
"electricity_cost": 21576397.17375,
"opex_cost": 3251250,
"water_cost": 135700.61115566036,
"stack_replacement": 0,
"total_cost": 24963347.784905657,
"discounted_cost": 19816710.315457545,
"discounted_production": 6902698.97869438
},
{
"year": 4,
"production_kg": 8564981.54481132,
"electricity_cost": 21677806.240466624,
"opex_cost": 3316275.0000000005,
"water_cost": 136338.404028092,
"stack_replacement": 0,
"total_cost": 25130419.644494716,
"discounted_cost": 18471608.65200605,
"discounted_production": 6295517.124087003
},
{
"year": 5,
"production_kg": 8436506.82163915,
"electricity_cost": 21779691.929796815,
"opex_cost": 3382600.5,
"water_cost": 136979.194527024,
"stack_replacement": 0,
"total_cost": 25299271.624323837,
"discounted_cost": 17218259.164707623,
"discounted_production": 5741744.784468238
},
{
"year": 6,
"production_kg": 8309959.219314563,
"electricity_cost": 21882056.48186686,
"opex_cost": 3450252.5100000002,
"water_cost": 137622.99674130103,
"stack_replacement": 0,
"total_cost": 25469931.988608163,
"discounted_cost": 16050377.537999256,
"discounted_production": 5236683.900649273
},
{
"year": 7,
"production_kg": 8185309.831024845,
"electricity_cost": 21984902.147331636,
"opex_cost": 3519257.5602,
"water_cost": 138269.82482598515,
"stack_replacement": 40000000,
"total_cost": 65642429.53235762,
"discounted_cost": 38301727.15380211,
"discounted_production": 4776049.668647716
},
{
"year": 8,
"production_kg": 8062530.183559472,
"electricity_cost": 22088231.187424093,
"opex_cost": 3589642.711404,
"water_cost": 138919.69300266728,
"stack_replacement": 0,
"total_cost": 25816793.59183076,
"discounted_cost": 13948010.27527616,
"discounted_production": 4355934.1885351855
},
{
"year": 9,
"production_kg": 7941592.23080608,
"electricity_cost": 22192045.87400499,
"opex_cost": 3661435.5656320807,
"water_cost": 139572.6155597798,
"stack_replacement": 0,
"total_cost": 25993054.05519685,
"discounted_cost": 13002998.443704406,
"discounted_production": 3972773.3108399604
},
{
"year": 10,
"production_kg": 7822468.347343989,
"electricity_cost": 22296348.489612814,
"opex_cost": 3734664.276944722,
"water_cost": 140228.60685291077,
"stack_replacement": 0,
"total_cost": 26171241.37341045,
"discounted_cost": 12122348.579256186,
"discounted_production": 3623316.399238297
},
{
"year": 11,
"production_kg": 7705131.322133829,
"electricity_cost": 22401141.327513993,
"opex_cost": 3809357.5624836166,
"water_cost": 140887.68130511948,
"stack_replacement": 0,
"total_cost": 26351386.571302727,
"discounted_cost": 11301658.020212607,
"discounted_production": 3304598.753009002
},
{
"year": 12,
"production_kg": 7589554.352301821,
"electricity_cost": 22506426.69175331,
"opex_cost": 3885544.713733289,
"water_cost": 141549.8534072535,
"stack_replacement": 0,
"total_cost": 26533521.25889385,
"discounted_cost": 10536826.357232649,
"discounted_production": 3013916.4552906174
},
{
"year": 13,
"production_kg": 7475711.037017294,
"electricity_cost": 22612206.89720455,
"opex_cost": 3963255.6080079544,
"water_cost": 142215.13771826762,
"stack_replacement": 0,
"total_cost": 26717677.642930776,
"discounted_cost": 9824034.621366857,
"discounted_production": 2748803.433760424
},
{
"year": 14,
"production_kg": 7363575.371462034,
"electricity_cost": 22718484.26962141,
"opex_cost": 4042520.720168114,
"water_cost": 142883.54886554347,
"stack_replacement": 40000000,
"total_cost": 66903888.538655065,
"discounted_cost": 22778167.563115362,
"discounted_production": 2507010.53912409
},
{
"year": 15,
"production_kg": 7253121.740890103,
"electricity_cost": 22825261.14568863,
"opex_cost": 4123371.1345714764,
"water_cost": 143555.10154521154,
"stack_replacement": 0,
"total_cost": 27092187.38180532,
"discounted_cost": 8540587.341495678,
"discounted_production": 2286486.4639233598
},
{
"year": 16,
"production_kg": 7144324.914776752,
"electricity_cost": 22932539.873073366,
"opex_cost": 4205838.557262906,
"water_cost": 144229.810522474,
"stack_replacement": 0,
"total_cost": 27282608.240858745,
"discounted_cost": 7963533.275708101,
"discounted_production": 2085360.339781953
},
{
"year": 17,
"production_kg": 7037160.0410551,
"electricity_cost": 23040322.810476813,
"opex_cost": 4289955.328408164,
"water_cost": 144907.69063192964,
"stack_replacement": 0,
"total_cost": 27475185.829516906,
"discounted_cost": 7425689.664910465,
"discounted_production": 1901925.8654492807
},
{
"year": 18,
"production_kg": 6931602.640439274,
"electricity_cost": 23148612.327686056,
"opex_cost": 4375754.434976327,
"water_cost": 145588.75677789972,
"stack_replacement": 0,
"total_cost": 27669955.519440286,
"discounted_cost": 6924379.504425369,
"discounted_production": 1734626.8309884646
},
{
"year": 19,
"production_kg": 6827628.600832685,
"electricity_cost": 23257410.80562618,
"opex_cost": 4463269.523675854,
"water_cost": 146273.02393475585,
"stack_replacement": 0,
"total_cost": 27866953.35323679,
"discounted_cost": 6457109.278769367,
"discounted_production": 1582043.9152996645
},
{
"year": 20,
"production_kg": 6725214.171820194,
"electricity_cost": 23366720.63641262,
"opex_cost": 4552534.914149371,
"water_cost": 146960.5071472492,
"stack_replacement": 0,
"total_cost": 28066216.05770924,
"discounted_cost": 6021556.343796462,
"discounted_production": 1442882.6449723789
},
{
"year": 21,
"production_kg": 6624335.9592428915,
"electricity_cost": 23476544.223403763,
"opex_cost": 4643585.612432359,
"water_cost": 147651.22153084129,
"stack_replacement": 40000000,
"total_cost": 68267781.05736697,
"discounted_cost": 13561787.082695078,
"discounted_production": 1315962.4123127714
},
{
"year": 22,
"production_kg": 6524970.919854248,
"electricity_cost": 23586883.98125376,
"opex_cost": 4736457.324681005,
"water_cost": 148345.1822720362,
"stack_replacement": 0,
"total_cost": 28471686.488206804,
"discounted_cost": 5237096.448744054,
"discounted_production": 1200206.4593778516
},
{
"year": 23,
"production_kg": 6427096.356056434,
"electricity_cost": 23697742.335965652,
"opex_cost": 4831186.471174627,
"water_cost": 149042.40462871478,
"stack_replacement": 0,
"total_cost": 28677971.211768996,
"discounted_cost": 4884296.819804982,
"discounted_production": 1094632.7430436886
},
{
"year": 24,
"production_kg": 6330689.910715587,
"electricity_cost": 23809121.724944696,
"opex_cost": 4927810.200598119,
"water_cost": 149742.90393046977,
"stack_replacement": 0,
"total_cost": 28886674.829473287,
"discounted_cost": 4555409.477580303,
"discounted_production": 998345.60360929
},
{
"year": 25,
"production_kg": 6235729.562054854,
"electricity_cost": 23921024.597051933,
"opex_cost": 5026366.404610082,
"water_cost": 150446.69557894298,
"stack_replacement": 0,
"total_cost": 29097837.69724096,
"discounted_cost": 4248805.298047119,
"discounted_production": 910528.1662547691
}
],
"sensitivity": [
{
"variable": "electricity_consumption_kwh_kg",
"label": "Specific Consumption",
"lcoh_minus20": 4.23,
"lcoh_base": 5.28,
"lcoh_plus20": 6.33,
"impact_range": 2.104954655361709
},
{
"variable": "electricity_cost_eur_mwh",
"label": "Electricity Cost",
"lcoh_minus20": 4.72,
"lcoh_base": 5.28,
"lcoh_plus20": 5.84,
"impact_range": 1.1178479167883024
},
{
"variable": "operating_hours",
"label": "Operating Hours",
"lcoh_minus20": 5.9,
"lcoh_base": 5.28,
"lcoh_plus20": 4.87,
"impact_range": 1.0282361860139657
},
{
"variable": "electrolyzer_cost_eur_kw",
"label": "Electrolyzer Cost",
"lcoh_minus20": 4.79,
"lcoh_base": 5.28,
"lcoh_plus20": 5.77,
"impact_range": 0.9871067385734076
},
{
"variable": "wacc_pct",
"label": "WACC",
"lcoh_minus20": 5.17,
"lcoh_base": 5.28,
"lcoh_plus20": 5.4,
"impact_range": 0.2355606088519986
}
]
},
"model_version": "4.0.1"
}Pretty-printed version, for reading. The bytes that get hashed are NOT this JSON — they are the canonical version below.
Canonical bytes (what gets hashed)
Show canonical bytes (8493 bytes)
{"inputs":{"availability_pct":95,"bop_pct":25,"electricity_consumption_kwh_kg":53,"electricity_cost_eur_mwh":45,"electrolyzer_capacity_mw":100,"electrolyzer_cost_eur_kw":1000,"inflation_rate_pct":2,"om_pct":2.5,"operating_hours":5000,"project_lifetime_years":25,"region":"EU-Iberia","stack_degradation_pct":1.5,"stack_replacement_cost_pct":40,"stack_replacement_interval":7,"subsidy_eur":0,"technology":"PEM","wacc_pct":8,"water_consumption_l_kg":10,"water_cost_eur_m3":1.5},"model_version":"4.0.1","outputs":{"annual_cashflow":[{"discounted_cost":22809661.076170508,"discounted_production":8298392.732354997,"electricity_cost":21375000,"opex_cost":3125000,"production_kg":8962264.150943397,"stack_replacement":0,"total_cost":24634433.96226415,"water_cost":134433.96226415096,"year":1},{"discounted_cost":21260312.33014986,"discounted_production":7568441.519786733,"electricity_cost":21475462.500000004,"opex_cost":3187500,"production_kg":8827830.188679246,"stack_replacement":0,"total_cost":24798028.301886797,"water_cost":135065.80188679247,"year":2},{"discounted_cost":19816710.315457545,"discounted_production":6902698.97869438,"electricity_cost":21576397.17375,"opex_cost":3251250,"production_kg":8695412.735849056,"stack_replacement":0,"total_cost":24963347.784905657,"water_cost":135700.61115566036,"year":3},{"discounted_cost":18471608.65200605,"discounted_production":6295517.124087003,"electricity_cost":21677806.240466624,"opex_cost":3316275.0000000005,"production_kg":8564981.54481132,"stack_replacement":0,"total_cost":25130419.644494716,"water_cost":136338.404028092,"year":4},{"discounted_cost":17218259.164707623,"discounted_production":5741744.784468238,"electricity_cost":21779691.929796815,"opex_cost":3382600.5,"production_kg":8436506.82163915,"stack_replacement":0,"total_cost":25299271.624323837,"water_cost":136979.194527024,"year":5},{"discounted_cost":16050377.537999256,"discounted_production":5236683.900649273,"electricity_cost":21882056.48186686,"opex_cost":3450252.5100000002,"production_kg":8309959.219314563,"stack_replacement":0,"total_cost":25469931.988608163,"water_cost":137622.99674130103,"year":6},{"discounted_cost":38301727.15380211,"discounted_production":4776049.668647716,"electricity_cost":21984902.147331636,"opex_cost":3519257.5602,"production_kg":8185309.831024845,"stack_replacement":40000000,"total_cost":65642429.53235762,"water_cost":138269.82482598515,"year":7},{"discounted_cost":13948010.27527616,"discounted_production":4355934.1885351855,"electricity_cost":22088231.187424093,"opex_cost":3589642.711404,"production_kg":8062530.183559472,"stack_replacement":0,"total_cost":25816793.59183076,"water_cost":138919.69300266728,"year":8},{"discounted_cost":13002998.443704406,"discounted_production":3972773.3108399604,"electricity_cost":22192045.87400499,"opex_cost":3661435.5656320807,"production_kg":7941592.23080608,"stack_replacement":0,"total_cost":25993054.05519685,"water_cost":139572.6155597798,"year":9},{"discounted_cost":12122348.579256186,"discounted_production":3623316.399238297,"electricity_cost":22296348.489612814,"opex_cost":3734664.276944722,"production_kg":7822468.347343989,"stack_replacement":0,"total_cost":26171241.37341045,"water_cost":140228.60685291077,"year":10},{"discounted_cost":11301658.020212607,"discounted_production":3304598.753009002,"electricity_cost":22401141.327513993,"opex_cost":3809357.5624836166,"production_kg":7705131.322133829,"stack_replacement":0,"total_cost":26351386.571302727,"water_cost":140887.68130511948,"year":11},{"discounted_cost":10536826.357232649,"discounted_production":3013916.4552906174,"electricity_cost":22506426.69175331,"opex_cost":3885544.713733289,"production_kg":7589554.352301821,"stack_replacement":0,"total_cost":26533521.25889385,"water_cost":141549.8534072535,"year":12},{"discounted_cost":9824034.621366857,"discounted_production":2748803.433760424,"electricity_cost":22612206.89720455,"opex_cost":3963255.6080079544,"production_kg":7475711.037017294,"stack_replacement":0,"total_cost":26717677.642930776,"water_cost":142215.13771826762,"year":13},{"discounted_cost":22778167.563115362,"discounted_production":2507010.53912409,"electricity_cost":22718484.26962141,"opex_cost":4042520.720168114,"production_kg":7363575.371462034,"stack_replacement":40000000,"total_cost":66903888.538655065,"water_cost":142883.54886554347,"year":14},{"discounted_cost":8540587.341495678,"discounted_production":2286486.4639233598,"electricity_cost":22825261.14568863,"opex_cost":4123371.1345714764,"production_kg":7253121.740890103,"stack_replacement":0,"total_cost":27092187.38180532,"water_cost":143555.10154521154,"year":15},{"discounted_cost":7963533.275708101,"discounted_production":2085360.339781953,"electricity_cost":22932539.873073366,"opex_cost":4205838.557262906,"production_kg":7144324.914776752,"stack_replacement":0,"total_cost":27282608.240858745,"water_cost":144229.810522474,"year":16},{"discounted_cost":7425689.664910465,"discounted_production":1901925.8654492807,"electricity_cost":23040322.810476813,"opex_cost":4289955.328408164,"production_kg":7037160.0410551,"stack_replacement":0,"total_cost":27475185.829516906,"water_cost":144907.69063192964,"year":17},{"discounted_cost":6924379.504425369,"discounted_production":1734626.8309884646,"electricity_cost":23148612.327686056,"opex_cost":4375754.434976327,"production_kg":6931602.640439274,"stack_replacement":0,"total_cost":27669955.519440286,"water_cost":145588.75677789972,"year":18},{"discounted_cost":6457109.278769367,"discounted_production":1582043.9152996645,"electricity_cost":23257410.80562618,"opex_cost":4463269.523675854,"production_kg":6827628.600832685,"stack_replacement":0,"total_cost":27866953.35323679,"water_cost":146273.02393475585,"year":19},{"discounted_cost":6021556.343796462,"discounted_production":1442882.6449723789,"electricity_cost":23366720.63641262,"opex_cost":4552534.914149371,"production_kg":6725214.171820194,"stack_replacement":0,"total_cost":28066216.05770924,"water_cost":146960.5071472492,"year":20},{"discounted_cost":13561787.082695078,"discounted_production":1315962.4123127714,"electricity_cost":23476544.223403763,"opex_cost":4643585.612432359,"production_kg":6624335.9592428915,"stack_replacement":40000000,"total_cost":68267781.05736697,"water_cost":147651.22153084129,"year":21},{"discounted_cost":5237096.448744054,"discounted_production":1200206.4593778516,"electricity_cost":23586883.98125376,"opex_cost":4736457.324681005,"production_kg":6524970.919854248,"stack_replacement":0,"total_cost":28471686.488206804,"water_cost":148345.1822720362,"year":22},{"discounted_cost":4884296.819804982,"discounted_production":1094632.7430436886,"electricity_cost":23697742.335965652,"opex_cost":4831186.471174627,"production_kg":6427096.356056434,"stack_replacement":0,"total_cost":28677971.211768996,"water_cost":149042.40462871478,"year":23},{"discounted_cost":4555409.477580303,"discounted_production":998345.60360929,"electricity_cost":23809121.724944696,"opex_cost":4927810.200598119,"production_kg":6330689.910715587,"stack_replacement":0,"total_cost":28886674.829473287,"water_cost":149742.90393046977,"year":24},{"discounted_cost":4248805.298047119,"discounted_production":910528.1662547691,"electricity_cost":23921024.597051933,"opex_cost":5026366.404610082,"production_kg":6235729.562054854,"stack_replacement":0,"total_cost":29097837.69724096,"water_cost":150446.69557894298,"year":25}],"annual_production_kg":8962264,"capex_total":125000000,"cost_breakdown":{"capex_pct":27.8854185529534,"electricity_pct":52.928777421597694,"opex_pct":8.835519848036894,"stack_pct":10.017398784823358,"water_pct":0.33288539258866484},"lcoh_eur_kg":5.28,"lifetime_production_kg":188004702,"sensitivity":[{"impact_range":2.104954655361709,"label":"Specific Consumption","lcoh_base":5.28,"lcoh_minus20":4.23,"lcoh_plus20":6.33,"variable":"electricity_consumption_kwh_kg"},{"impact_range":1.1178479167883024,"label":"Electricity Cost","lcoh_base":5.28,"lcoh_minus20":4.72,"lcoh_plus20":5.84,"variable":"electricity_cost_eur_mwh"},{"impact_range":1.0282361860139657,"label":"Operating Hours","lcoh_base":5.28,"lcoh_minus20":5.9,"lcoh_plus20":4.87,"variable":"operating_hours"},{"impact_range":0.9871067385734076,"label":"Electrolyzer Cost","lcoh_base":5.28,"lcoh_minus20":4.79,"lcoh_plus20":5.77,"variable":"electrolyzer_cost_eur_kw"},{"impact_range":0.2355606088519986,"label":"WACC","lcoh_base":5.28,"lcoh_minus20":5.17,"lcoh_plus20":5.4,"variable":"wacc_pct"}]},"type":"lcoh"}Or download as a file (SHA-256 over the file bytes matches the hash below): example-lcoh-canonical.txt.
Expected SHA-256
983865341cf967500f034e0639c5bd8d9d0b89f7691baf344e1d115e5cdcdc34Snippets
Each snippet, run against the worked-example payload above, produces the SHA-256 listed in §4. Versions tested: Node 22 + fast-json-stable-stringify 2.1.0; Python 3.12. Cross-runtime byte-equivalence is empirically validated against four production rows (PASO 0 of #77 C4, 2026-06-12).
JavaScript
import stableStringify from "fast-json-stable-stringify";
import { createHash } from "node:crypto";
// payload is the JSON of /methodology/example-lcoh-payload.json
const canonical = stableStringify(payload);
const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
console.log(hash);
// → 983865341cf967500f034e0639c5bd8d9d0b89f7691baf344e1d115e5cdcdc34Python
import json, hashlib
# payload loaded from /methodology/example-lcoh-payload.json
canonical = json.dumps(
payload,
sort_keys=True,
ensure_ascii=False,
separators=(",", ":"),
)
hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
print(hash)
# → 983865341cf967500f034e0639c5bd8d9d0b89f7691baf344e1d115e5cdcdc34curl — query the live verify endpoint
Confirms the same hash from §4 is in our database. The row is the synthetic demo calculation we inserted on 2026-06-12 to make this end-to-end example complete; it carries application: "methodology-example" in the public projection so any auditor can tell it apart from real-user calculations at a glance.
curl -s https://gh2.ai/api/verify/983865341cf967500f034e0639c5bd8d9d0b89f7691baf344e1d115e5cdcdc34
# → {"verified":true,"calculation":{...},"matched_by":"content"}The verify endpoint
GET /api/verify/[hash] accepts either an event hash or a content hash. The response below is the LITERAL output of the curl in §5, captured against the live endpoint on 2026-06-12— not a sketch. It predates the node_version field added on 21 September 2026, so a call today returns that one too; the capture is left as taken rather than edited after the fact:
{
"verified": true,
"calculation": {
"type": "lcoh",
"model_version": "4.0.1",
"methodology_version": "2.1",
"created_at": "2026-06-12T18:16:57.918426+00:00",
"region": "EU-Iberia",
"technology": "PEM",
"application": "methodology-example",
"lcoh_result": 5.28,
"content_sha256": "983865341cf967500f034e0639c5bd8d9d0b89f7691baf344e1d115e5cdcdc34"
},
"matched_by": "content"
}The calculation object is the public projection of the row — minimum metadata a third party needs to cross-check against a PDF cover. It includes content_sha256 (so a caller looking up by event-hash can still see and compare the content hash) and node_version, the runtime that produced the outputs — the one to reproduce them on. null there means not recorded, never a runtime you may assume: calculations issued before 23 August 2026 did not record it and there is nothing to back-fill it from. It NEVER includes inputs, outputs, user_id or any private columns; that contract is enforced by the PUBLIC_FIELDS allowlist in src/lib/verify/public-fields.ts and pinned by a privacy-shape test in the verify route suite.
When a content hash is shared by N rows (the same inputs run by N users), the endpoint returns the row with the oldest created_at deterministically; same hash, same row, same bytes, every time. The matched_by field reports whether the URL hash matched against hash_sha256 (legacy event-hash), content_sha256, or — since 24 September 2026 — the SHA-256 of a sealed PDF issued from the calculation (pdf).
Limits, scope and when this recipe stops being enough
What the hash proves:
- The calculation, with these exact inputs and outputs, existed in our database at the time the row was written.
- Recomputing the canonical bytes from those same inputs/outputs, in any runtime that follows the recipe, produces the same SHA-256.
- Forging a hash to point at non-existent content is computationally infeasible (SHA-256 collision resistance).
What it does NOT prove:
- Cryptographic protection of the PDF binary itself (covered separately by
pdf_sha256and the drag-drop verifier). - Identity authentication via digital signature — the hash proves the calculation existed, not who ran it.
- Protection against modification of database records by parties with privileged infrastructure access at GaugeH2.
- That re-running the engine in a different runtime reproduces the same outputs, and therefore the same hash. It does not: that is why the runtime is recorded and published.
When this recipe needs to migrate to RFC 8785 (JSON Canonicalization Scheme): the first time a content hash is computed by a runtime outside the V8 / CPython 3.6+ / Rust-ryu family of “shortest decimal that round-trips” implementations. Today every hash on file was produced by Node V8; we have empirically confirmed byte-equivalence with CPython's json.dumps for our domain. RFC 8785 adds number normalisation rules (1.0 → 1, scientific-vs-decimal boundary at a fixed exponent) that matter only when a runtime outside that family enters the picture; at that point we will publish a methodology v2 and surface both hashes during the transition window.
If your use requires a different assurance scope (for example, a regulatory audit or cryptographic non-repudiation), please contact us.
Readiness Score Methodology
Disclaimer: The Readiness Score is a structured maturity assessment tool. It is not a prediction of project success or financial performance. It does not constitute investment advice. Past maturity scores are not indicative of future outcomes.
The Readiness Score aggregates publicly available project data into a single score across four weighted buckets. A factor is credited when its milestone is reached — either from an explicit project flag or implied by the project’s development status:
- Development milestones (40 pts) — Final Investment Decision (15), EPC contract signed (10), permits obtained (8), grid connection secured (7).
- Commercial viability (25 pts) — binding offtake (15) or a softer MOU (5); confirmed price (5); application to a public support programme (5).
- Financing (20 pts) — subsidy disbursing (10) or approved (5); project finance secured (10).
- Track record (5 pts) — developer has completed an H₂ project (5), a renewable project (3), or has relevant experience (2).
The maximum attainable score is 90. Scores map to four maturity bands: Early Stage (0–25), Developing (26–50), Advanced (51–75), and Mature (76–90).
Data Quality & Provenance
Audit status is shown row by row. Provenance indicators render on rows that have cleared the audit (checked against a public source by an analyst or by the research agent). Rows still in pending_audit show an audit banner and withhold the “Source-checked” badge until they clear.Every data point on the project tracker is tagged with its source provenance and confidence level. Investment decisions require auditable data, not opaque numbers — so each value carries metadata about where it came from and how confidently it is held.
Source tiers
Each field on a project page is classified into one of five tiers, visible as a small badge next to the value:
- Curated record — Set by an administrator, or carried over from the curated catalogue import, which was compiled with AI research agents. Not necessarily re-checked field by field.
- Primary source — Taken from an authoritative source (developer announcements, regulator publications, established industry media), read by a person or extracted by an AI agent; the field’s detail says which.
- AI-enriched · corroborated — Established with AI assistance and cross-referenced against several independent sources that agree.
- AI-enriched · single source — Established with AI assistance but resting on one source; nothing independent corroborates it yet, and it stays flagged for review.
- No provenance tracked — Legacy records without provenance metadata; will be backfilled.
What you see, by access tier
- Every visitor — the provenance badge on every field, so you can see at a glance which figures are human-curated, which come from a verified source, and which were established with AI assistance.
- Analyst and above — the evidence behind each field: a link to the source it came from, how it was established (direct extraction, AI enrichment, manual curation, catalog lookup), whether anything independent corroborates it, and the date that provenance was recorded. Enough to go to the source and check the figure yourself, and to tell a number established last month from one carried since 2023.
- Internal admin — the same, plus the curation notes and the internal scores described below.
We do not publish confidence percentages, and that is deliberate. A confidence score is a model’s self-assessment of its own output. It looks precise, it cannot be checked against anything, and it invites arithmetic it will not carry — filtering, ranking or averaging on a number the model assigned to itself. What we publish instead is what you can verify: which source, by what method, corroborated or not, and when. If a figure rests on a single uncorroborated source, the badge says so in words rather than implying it with a percentage.
This system reflects our commitment to transparency: clients should be able to verify our data, not just trust it. For detailed audit trails on specific projects, upgrade to Analyst.
Limitations
The GaugeH2 models do not capture the following:
- Transmission and distribution costs — LCOH outputs are at the production gate. Pipeline, truck, or shipping costs to the end user are not included. A calculation can declare a delivery state, which is checked against a physical minimum, but compression, storage and transport are never costed.
- Subsidies — The LCOH is computed gross of subsidy: a capital grant does not lower it. A declared capital subsidy reduces the capital the promoter raises in the TEA, and a per-kilogram premium enters the TEA as revenue. The platform does not determine eligibility for EU, US or national programmes.
- Country-specific tax regimes — The tax rate input is a flat effective rate. Country-specific depreciation schedules, investment tax credits, or carbon pricing are not modeled separately.
- Supply chain risk — Electrolyzer lead times, commodity price volatility, and geopolitical supply chain disruptions are not reflected in the default cost trajectories.
- Revenue from by-products — Oxygen sales enter the TEA as a declared yearly revenue (
o2_revenue_eur_year); the LCOH does not credit them. Heat recovery credits and demand response revenues are outside the current model scope. - Project-level uncertainty — Both the LCOH calculator and the TEA offer a Monte Carlo simulation beside the point estimate.
- TEA financing model — The DSCR is computed on after-tax cash flow available for debt service (CFADS). Debt is a level annuity only (no sculpting), no LLCR is reported, tax losses are not carried forward (tax is floored at zero each year), and interest during construction is capitalised at the WACC through the construction factor rather than at the debt rate. See §7 of the frozen methodology document.