Working principles
One Option per measure family
Each measure type is classified into a single IPMVP family. The platform refuses to scope an intervention with an Option that does not match its family. Option B is reserved for measures that act on a contained sub-circuit.
The boundary meter is part of the record
Every Option B verification ledger row carries a snapshot of the meter that produced it: serial, device type, metering class, accuracy class, calibration certificate URL. The audit chain is portable without reading any other table.
Coverage gates every window
Both the baseline window and every measurement window run through the same coverage rule. Less than eighty percent telemetry coverage halts the write. A case row is raised; nothing settles until the next pass.
Append-only ledger
intervention_verifications is the financial primitive. Rows are immutable. Corrections supersede prior rows via supersedes_id. The mutable interventions cache is reconstructible from the latest ledger row at any time.
Method version stamped on every row
lib/mv/ipmvp.ts exports a METHOD_VERSION string. The validator that accepts a row writes that string into intervention_verifications.mv_method_version. A reviewer two years on can tell which rule set validated which row.
When Option B applies
Option B is the retrofit-isolation path. The platform allows it only when the measure under verification acts on a contained electrical scope that a boundary meter can isolate. The mapping is enforced by a pure validator in lib/mv/ipmvp.ts and is rerun both at scope time and at the moment a verification row is written.
An attempt to scope or verify Option B against any non-sub-circuit family returns option_not_allowed_for_measure_family with the allowed Options for that family in the message.
The boundary meter
Every Option B intervention is attached to exactly one boundary provision (device_provisions row), which is paired to exactly one device channel that feeds exactly one asset. The meter's metering class is recorded on the provision and copied onto every downstream record so the audit chain does not depend on the mutable provision row.
Revenue (Class 0.2 / 0.5S)
Default accuracy used in uncertainty calculations: ±0.5%. Tight enough to stand behind any savings margin.
Billing (Class 1)
Default accuracy used in uncertainty calculations: ±1.0%. Acceptable for Option B at any savings margin.
Screening (estimated)
Default accuracy used in uncertainty calculations: ±5.0%. Acceptable only when the savings-to-uncertainty ratio exceeds three. Annex F's distinguishable-from-noise threshold.
The screening-class path exists so that high-margin retrofits sub-metered by an existing Harvy2-with-VT install can settle without forcing a revenue meter onto every circuit. Marginal measures (savings below three times uncertainty) still require a billing-or-revenue meter; the validator blocks settlement otherwise.
The baseline
The baseline is the reference against which every verification window is measured. One row in mv_baselines, written by the nightly cron the moment the boundary meter is paired to its asset and producing data. Re-deriving the baseline chains the new row to the old via supersedes_id; nothing is overwritten.
Window
Twenty-eight consecutive days ending at the intervention's deployment timestamp. The cron defaults to measuring_started_at, then now(), when the deployment timestamp is not yet set.
Integration
TDEngine returns a per-hour mean kW and sample count over the boundary asset's child table. Energy is summed as the sum of (mean kW × one hour) across the window.
Coverage
At or above ninety-five percent of expected samples is a clean pass. Between eighty and ninety-five percent is a pass with a widened uncertainty flag. Below eighty percent fails outright; the baseline is not written.
Gaps
Hours whose sample count drops below fifty percent of the expected per-hour cadence are flagged. Consecutive low-coverage hours collapse into a single gap span, persisted on mv_baselines.gaps.
Day-type profile
Weekday and weekend hour-of-day mean kW vectors (length twenty-four each) are stored on mv_baselines.day_type_profile. This is the structure verification windows normalise against.
Provenance
Builder version, baseline window, boundary asset, boundary provision, meter snapshot, coverage statistics, and a supersedes_id (when re-derived) are all persisted on the same row.
The builder refuses to write twice for the same intervention without an explicit supersedes_id. A re-baselined intervention chains the prior mv_baselines row via supersedes_id; both rows remain on disk.
The measurement window
Each verification reads the boundary meter for one cadence window (monthly or quarterly) ending at the cron tick. The same helper that built the baseline integrates the kWh and checks coverage. The window passes, passes with a flag, or fails outright.
Coverage is samples observed divided by samples expected over the window. The same rule gates baseline and every verification window. The colour band reflects what lands in the database, not a tolerance the operator can override.
Uncertainty
Each verification row carries an uncertainty band on the savings figure. The computation is a lite implementation of IPMVP Annex F: the two contributors that always exist (meter precision and sampling variability) are combined as the root-sum-square of independent terms and scaled to a confidence half-width.
Contributors
Meter precision (from the meter's accuracy class) and sampling variability (interval-level standard deviation across baseline and measurement windows). Combined via root-sum-square as independent contributors.
Confidence level
Default ninety percent, two-sided. z = 1.645. The platform also supports eighty and ninety-five percent on the same engine.
Reported as
Half-width of the savings confidence band, expressed as a fraction of baseline kWh. Directly comparable to verified_savings_pct in the screening-meter test.
Out of scope (this version)
Regression baseline residual error (Option D / weather normalisation) and t-distribution for small samples are not yet modelled. Sample counts below twenty are flagged on uncertainty_distribution so a reviewer sees the basis.
The screening-meter doctrine
A screening-class boundary meter is acceptable, but only when the savings comfortably exceed the noise floor. The validator runs one extra test at ledger-write time: verified savings, as a fraction of baseline, must be at least three times the uncertainty band, also as a fraction of baseline. Anything below that ratio is refused. The error message names the two ways out: upgrade the meter to billing-grade, or extend the window to tighten uncertainty.
Test
|verified_savings_pct| / max(|uncertainty_pct|, 0.001) ≥ 3
Both terms are dimensionless fractions of baseline kWh. The constant three is the Annex F rule of thumb for distinguishable-from-noise.
The append-only ledger
recordVerification (lib/mv/ledger.ts) is the only sanctioned writer of intervention_verifications. The table has no INSERT policy; only the service-role client can write. Every call follows the same six steps and produces exactly one immutable row.
Read intervention
Pulls the intervention's measure_type, currency, baseline reference, and current state.
Re-validate the method
The same validateMvMethod that gated the Scoped → Approved transition runs again here. Defense in depth: even if a scoped intervention slips through, an inconsistent (option, meter, margin) tuple cannot land in the ledger.
Resolve baseline and currency
Baseline window, kWh, cost and tCO2e default from the intervention row when the caller omits them. The ledger row carries the resolved values verbatim.
Insert the immutable row
One row in intervention_verifications. Carries the full boundary meter snapshot, baseline reference, measurement window, measured kWh and dollars, uncertainty band, verifier identity, outcome, and a stamp of METHOD_VERSION.
Stamp the writeback
interventions.verified_savings, verified_tco2e, verifier_id, verified_at, verification_method_version, verification_uncertainty_pct, measurement_period_start, and measurement_period_end are updated in place. The mutable cache is downstream of the ledger.
Transition the state
Verified outcomes transition Measuring → Verified. Disputed outcomes transition Measuring → Disputed. Under-performing outcomes (|Δ| between 10% and 25%) transition to Verified with the Wattif share recomputed by lib/mv/fail-verification: new share = original × max(0, 1 + Δ), floored at five percent. Revenue tracks delivered value without ever zeroing out.
The three writes are not wrapped in one database transaction. The order is chosen so a mid-flight failure is recoverable. Ledger first: it is the record everything else is rebuilt from, regardless of cache state. Writeback second: rebuildable from the ledger. State transition last: nobody sees Verified without a durable row behind it.
How a reviewer sees it
Three screens. The M&V plan that the intervention is operating under, the ledger row produced at settlement, and the audit chain a reviewer walks when reconstructing a settled claim. Data is drawn from the Acme test facility; every value traces to a real row or registry default.
FCU occupancy setback · L3-East
IPMVP option
B
sub-circuit retrofit isolation
Baseline
28 days
built · mvb-7c91e2 · 99.4% coverage
Cadence
monthly
first verification 2026-07-01
Boundary meter
Current period
Verification cron runs hourly. The first ledger row will land on 2026-07-01 at 02:00 SGT. Coverage below 80% halts the write and surfaces a Case.
Verification row · v-2026-06
Screening-meter doctrine: N/A. Boundary meter is billing-class. The savings-to-uncertainty ratio test (savings_pct ÷ uncertainty_pct ≥ 3) does not apply to billing or revenue-class boundaries.
Audit chain · 5 reads
read order matches /methods/mv-option-b-method · §11The chain reads left-to-right at the database level. Each column is reconstructible from the column to its left, except the ledger (column 03), which is the source of truth.
Where M&V sits in the lifecycle
M&V is not a side-channel. It is the gated revenue transition inside the intervention's own state machine. Diagnosis produces a Scoped intervention. The boundary install moves it through Approved, Deploying, and Live. The M&V engine moves it through Measuring to Verified.
Verifier identity
Every ledger row carries a verifier_kind, verifier_id, and verifier_display_name. The schema accommodates three kinds from day one. The resolver lives in lib/mv/verifier.ts and is the single point through which a verifier identity reaches the ledger writer.
Agent (today, default)
The Accountant runs the monthly P&L cron and signs off the Measuring → Verified write. lib/mv/verifier.ts is a registry; only Accountant.canVerify is true. The other six agents are registered so a future Engineer-led equipment retrofit verification lights up by flipping a flag, not by editing scattered allowlists.
Human (CMVP-credentialed)
resolveHumanVerifier reads user_profiles. The credential column on user_profiles is the planned site of the CMVP check; until it lands, the resolver returns identity without a credential gate.
Third party
Reserved for external CMVP providers. The mv_third_party_verifiers registry is intentionally not yet built; the resolver returns a hard error so a row cannot land with a fabricated identity.
The crons that drive the engine
The platform's M&V engine is two scheduled writers. Both are registered in vercel.json. Both are batched (cap fifty per run) so a single tick does not exhaust the function ceiling, and the backlog drains across subsequent ticks.
mv-baseline-builder
Nightly at 02:30 SGT. Selects interventions in deploying or live without mv_baseline_id, batch of fifty. Resolves facility, boundary asset, and twenty-eight day window. Builds the baseline; coverage failures write a case row and leave the row to retry on the next tick. Idempotent: the selector predicate excludes rows that already have a baseline.
mv-verifier
Hourly. Selects interventions in measuring whose cadence boundary has elapsed since the last verification or measuring_started_at. Reads tariff from facilities.tariff.energy_rate (mean of peak and off-peak when TOU). Computes uncertainty per Annex F lite. Writes the ledger row via recordVerification. Coverage failures and missing tariff each write a case row.
Failure modes
Every failure mode is named, returns a structured error code, and either writes a case row or returns to the cron for the next pass. The system never silently writes a degraded row.
coverage_fail
Less than eighty percent telemetry coverage in the window. No baseline written; no ledger row written. A case row is raised and the intervention waits for the next cron tick.
screening_meter_savings_margin_insufficient
Boundary meter is screening-class and the savings-to-uncertainty ratio is below three. The validator rejects at both scope time and ledger-write time. Resolution paths are documented in the validator's error message: upgrade the meter or extend the window.
option_not_allowed_for_measure_family
Scope time guard. Option B requested on a measure that does not classify as sub_circuit_retrofit. Resolution: change the measure type, or change the Option, before approval.
writeback_failed (ledger row already inserted)
The append-only row is canonical; the mutable writeback can be rebuilt by reading the latest ledger row for the intervention. The cron returns the partial failure so an operator can retry the writeback specifically.
under_performing
Verified savings fell between ten and twenty-five percent below expected. lib/mv/fail-verification.ts classifies the delta and recomputes the Wattif share before the ledger insert: new share = original × max(0, 1 + Δ), floored at five percent. The ledger row carries the signed delta_pct and the rationale string. Overperformance and undershoots within ten percent classify as 'verified' with no recompute.
How a reviewer reconstructs a settlement
Any verified settlement can be reconstructed from five reads against the database, left to right.
interventions
the anchor
mv_baselines
the reference
intervention_verifications
append-only ledger
interventions writeback
cache, rebuildable
state machine
Verified / Disputed
Five reads, left to right. Every column is reconstructible from the column to its left, except the ledger, which is the source of truth.
- Find the intervention. interventions.id is the anchor.
- Read the baseline. mv_baselines (joined on intervention_id) carries the window, kWh, coverage statistics, gap list, day-type profile, boundary asset, boundary provision, meter snapshot, and builder version. supersedes_id chains prior baselines when one was redrawn.
- Read the verifications. intervention_verifications (joined on intervention_id, ordered by created_at) is append-only. Each row carries its own baseline reference (frozen at the time of the row), measurement window, measured kWh and dollars, uncertainty band, meter snapshot, verifier identity, outcome, and mv_method_version.
- Reconcile the cache. interventions.verified_savings, verified_at, verification_method_version, and verification_uncertainty_pct should match the latest non-superseded intervention_verifications row. Any drift is recoverable by replaying from the ledger.
- Confirm the state. The state machine transition that fired on the ledger write is logged on the interventions row and emitted on the event bus for downstream consumers (analytics, partner webhooks, notifications).
What is not yet in scope
The methodology document is honest about its boundaries. The following items are deliberately deferred and are documented here so a reviewer is not surprised.
- Continuous M&V dashboard. Per-intervention savings surface at /facility/[id]/savings is specified and not yet built. Today's interface is the report deliverable and the database.
- TOU-weighted savings. The verifier reads facilities.tariff.energy_rate or the mean of peak and off-peak. A full time-of-use weighted savings computation lands when the cost engine integration is wired.
- Regression baselines. Option D's calibrated simulation path, and the regression-residual contribution to uncertainty, are not yet implemented. The current Option B path uses a day-type profile, not a regression model.
- Third-party verifier registry. The schema accommodates kind='third_party'; the mv_third_party_verifiers table and its CMVP credential references are not yet built.
- External CMVP review of the methodology. Scheduled for Q3 2026. The credential letter from that review is the precondition for any Program Facility that references IPMVP compliance in its term sheet.
Standards alignment
The Wattif Option B engine is implemented against the following standards. The Q3 2026 CMVP review will formally validate the mapping; until then the alignment claims here are the engineering team's, not an independent reviewer's.
IPMVP Core Concepts (Volume I, 2022). Options A through D defined; Annex F treats uncertainty.
Efficiency Valuation OrganizationASHRAE Guideline 14-2014: Measurement of Energy, Demand, and Water Savings.
ASHRAEISO 50047:2016: Energy savings determination in organisations.
ISOFEMP M&V Guidelines 4.0: Measurement and Verification for Federal Performance Contracts.
U.S. Department of Energy