Meter billing — you are billed exactly what the meter read, once
In December 2015 the UK regulator Ofgem fined npower £26 million for billing failures: estimated and duplicated invoices that didn't match what meters had recorded, sent to half a million customers. The root pattern is always the same — billing reads a copy of the meter data that can drift, and nothing structural ties the invoice to the register. Here conservation is by construction. Each billing cycle is a state machine: usage accumulates only while `accumulating`; `locked` snapshots the register; the ONLY write to `billed` copies the metered value at invoice time. You cannot invoice an unlocked register (the transition doesn't exist), cannot invoice twice (invoiced -> invoiced doesn't exist), and a settled cycle is TERMINAL — no retroactive "adjustment".
A Soma program from the energy part of the verified corpus: 115 lines, cells Billing, BillingTests. It passes soma check with soma 3.2.4.
How it is built
The cell Billing declares 6 signals in its face: record_usage(meter: String, kwh: Int) -> Map, lock_register(meter: String) -> Map, issue_invoice(meter: String) -> Map, post_payment(meter: String) -> Map, audit_gap(meter: String) -> Int and cycle_state(meter: String) -> String; keeps 2 memory slots: metered (Map<String, Int>, persistent and consistent) under the invariant metered >= 0 and billed (Map<String, Int>, persistent and consistent) under the invariant billed >= 0. Its state machine cycle starts in accumulating and allows 3 transitions: accumulating -> locked, locked -> invoiced and invoiced -> settled. settled is terminal. Handlers: record_usage (line 45), lock_register (line 56), issue_invoice (line 62), post_payment (line 71), audit_gap (line 77) and cycle_state (line 81).
The file also holds 1 test cell (BillingTests) with 14 assertions, run by soma test.
The source
meter_billing.cell
// ============================================================================
// METER BILLING — you are billed exactly what the meter read, once
// ============================================================================
//
// In December 2015 the UK regulator Ofgem fined npower £26 million for
// billing failures: estimated and duplicated invoices that didn't match
// what meters had recorded, sent to half a million customers. The root
// pattern is always the same — billing reads a copy of the meter data
// that can drift, and nothing structural ties the invoice to the register.
//
// Here conservation is by construction. Each billing cycle is a state
// machine: usage accumulates only while `accumulating`; `locked` snapshots
// the register; the ONLY write to `billed` copies the metered value at
// invoice time. You cannot invoice an unlocked register (the transition
// doesn't exist), cannot invoice twice (invoiced -> invoiced doesn't
// exist), and a settled cycle is TERMINAL — no retroactive "adjustment".
// soma verify proves the lifecycle; the audit signal pins billed==metered.
// ============================================================================
cell Billing {
face {
signal record_usage(meter: String, kwh: Int) -> Map
signal lock_register(meter: String) -> Map
signal issue_invoice(meter: String) -> Map
signal post_payment(meter: String) -> Map
signal audit_gap(meter: String) -> Int
signal cycle_state(meter: String) -> String
}
memory {
metered: Map<String, Int> [persistent, consistent]
invariant metered >= 0
billed: Map<String, Int> [persistent, consistent]
invariant billed >= 0
}
state cycle {
initial: accumulating
accumulating -> locked // register snapshot taken
locked -> invoiced // bill issued for EXACTLY the snapshot
invoiced -> settled // payment posted — TERMINAL: history
}
on record_usage(meter: String, kwh: Int) {
if kwh <= 0 { return map("ok", false, "reason", "usage must be positive") }
if get_status(meter) != "accumulating" {
return map("ok", false, "reason",
"register is locked (state: {get_status(meter)}) — no backfilled usage")
}
let total = (metered.get(meter) ?? 0) + kwh
metered.set(meter, total)
return map("ok", true, "metered", total)
}
on lock_register(meter: String) {
let r = try { transition(meter, "locked") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(meter))
}
on issue_invoice(meter: String) {
let r = try { transition(meter, "invoiced") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
// conservation by construction: the ONLY write to `billed`
// copies the metered register, verbatim
billed.set(meter, metered.get(meter) ?? 0)
return map("ok", true, "billed", billed.get(meter))
}
on post_payment(meter: String) {
let r = try { transition(meter, "settled") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(meter))
}
on audit_gap(meter: String) {
return (billed.get(meter) ?? 0) - (metered.get(meter) ?? 0)
}
on cycle_state(meter: String) { return get_status(meter) }
}
cell test BillingTests {
rules {
// a normal cycle: meter ticks, register locks, invoice goes out
assert record_usage("m1", 300).ok == true
assert record_usage("m1", 150).ok == true
// cannot invoice an unlocked register — the transition is absent
assert_fails transition("m1", "invoiced")
assert lock_register("m1").ok == true
// once locked, no backfilled usage sneaks onto the bill
assert record_usage("m1", 999).ok == false
assert issue_invoice("m1").ok == true
// CONSERVATION: billed == metered, exactly
assert audit_gap("m1") == 0
// cannot invoice the same cycle twice
assert_fails transition("m1", "invoiced")
assert post_payment("m1").ok == true
// settled is TERMINAL: no retroactive adjustment, ever
assert_fails transition("m1", "locked")
assert_fails transition("m1", "accumulating")
// negative invoices and registers are rejected by the memory
assert_fails billed.set("m1", -50)
assert_fails metered.set("m1", -1)
assert audit_gap("m1") == 0
}
}What the toolchain says
$ soma check meter_billing.cell
✓ All checks passed.$ soma test meter_billing.cell
test BillingTests ...
✓ assert record_usage("m1", 300).ok == true
✓ assert record_usage("m1", 150).ok == true
✓ assert_fails transition("m1", "invoiced") — raised invalid transition: accumulating → invoiced. Current state: 'accumulating'. Valid targets: [locked]
✓ assert lock_register("m1").ok == true
✓ assert record_usage("m1", 999).ok == false
✓ assert issue_invoice("m1").ok == true
✓ assert audit_gap("m1") == 0
✓ assert_fails transition("m1", "invoiced") — raised invalid transition: invoiced → invoiced. Current state: 'invoiced'. Valid targets: [settled]
✓ assert post_payment("m1").ok == true
✓ assert_fails transition("m1", "locked") — raised invalid transition: settled → locked. Current state: 'settled'. Valid targets: []
✓ assert_fails transition("m1", "accumulating") — raised invalid transition: settled → accumulating. Current state: 'settled'. Valid targets: []
✓ assert_fails billed.set("m1", -50) — raised memory invariant violated on 'billed': billed >= 0 — rejected write of -50 (key "m1"); the slot is unchanged
✓ assert_fails metered.set("m1", -1) — raised memory invariant violated on 'metered': metered >= 0 — rejected write of -1 (key "m1"); the slot is unchanged
✓ assert audit_gap("m1") == 0
14 tests: 14 passed, 0 failed$ soma verify meter_billing.cell
Verifying meter_billing.cell...
State machine 'cycle': 4 states, initial 'accumulating'
States: [accumulating, invoiced, locked, settled]
✓ 4 states, 3 transitions
✓ all states reachable from 'accumulating'
✓ terminal states: [settled]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 3 literal transitions, 0 dynamic)
✓ termination: all 6 handlers structurally terminate
✓ refinement: handler `lock_register` ⟶ {locked}
✓ refinement: handler `issue_invoice` ⟶ {invoiced}
✓ refinement: handler `post_payment` ⟶ {settled}
· transition sources: `lock_register`, `issue_invoice`, `post_payment` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
Billing/invariants:
✓ invariant metered >= 0 — writer 'record_usage' proven by induction (writes total)
✓ invariant billed >= 0 — writer 'issue_invoice' proven by induction (writes (metered.get(meter) ?? 0))
12 passed, 0 warnings, 0 failures
Temporal properties for 'cycle':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK