Payments approval — a whole service in one file: a ledger that cannot overdraw, a payment lifecycle the verifier proves, an HTTP API that maps error kinds to status codes, and tests that run offline
What is PROVEN (soma verify + soma.toml): paid is unreachable without authorized; nothing follows rejected; every payment ends paid or rejected. What is ENFORCED: the balance never goes negative (invariant, checked on every write); a handler that fails is rolled back entirely, so a payment whose debit is refused stays exactly where it was.
A Soma program from the services part of the verified corpus: 165 lines, cells Ledger, Payments, PaymentsTests. It passes soma check with soma 3.2.4.
How it is built
The cell Ledger declares 3 signals in its face: _deposit(amount: Int) -> Int, _debit(amount: Int) -> Int and balance() -> Int; keeps 1 memory slot: account (Map<String, Int>, persistent) under the invariant account >= 0. Handlers: balance (line 26). Private helpers: _deposit and _debit.
The cell Payments declares 7 signals in its face: propose(id: String, amount: Int) -> Map, authorize(id: String) -> Map, sign_off(id: String, officer: String) -> Map, settle(id: String) -> Map, reject(id: String) -> Map, payment(id: String) -> Map and request(method: String, path: String, body: String) -> Map; keeps 1 memory slot: amounts (Map<String, Int>, persistent) under the invariant amounts > 0 && amounts <= 1000000. Its state machine flow starts in proposed and allows 7 transitions: proposed -> authorized, proposed -> pending_signoff, pending_signoff -> signed_off, signed_off -> authorized, authorized -> paid, proposed -> rejected and pending_signoff -> rejected. paid and rejected are terminal. Handlers: propose (line 66), authorize (line 71), sign_off (line 80), settle (line 86), reject (line 94), payment (line 98) and request (line 110). Private helpers: _amount and _route.
The file also holds 1 test cell (PaymentsTests) with 21 assertions, run by soma test.
The source
soma.toml
[package]
name = "services-corpus"
version = "0.1.0"
description = "Whole verified services: HTTP + lifecycle + invariants, in one file each"
[agent]
mock = "echo"
# payments_approval.cell — the properties its README promises
[verify]
cells = ["Payments"]
eventually = ["paid", "rejected"]
[verify.before.paid]
requires = ["authorized"]
[verify.after.rejected]
never = ["paid", "authorized", "signed_off"]
[verify.after.paid]
never = ["rejected"]payments_approval.cell
// PAYMENTS APPROVAL — a whole service in one file: a ledger that cannot
// overdraw, a payment lifecycle the verifier proves, an HTTP API that maps
// error kinds to status codes, and tests that run offline.
//
// What is PROVEN (soma verify + soma.toml): paid is unreachable without
// authorized; nothing follows rejected; every payment ends paid or rejected.
// What is ENFORCED: the balance never goes negative (invariant, checked on
// every write); a handler that fails is rolled back entirely, so a payment
// whose debit is refused stays exactly where it was.
//
// soma check payments_approval.cell
// soma verify payments_approval.cell
// soma test payments_approval.cell
// soma run payments_approval.cell request POST /payments '{"id":"p1","amount":250}'
cell Ledger {
face {
signal _deposit(amount: Int) -> Int
signal _debit(amount: Int) -> Int
signal balance() -> Int
}
memory {
account: Map<String, Int> [persistent]
invariant account >= 0 // an overdraft is unrepresentable
}
on balance() { return account.get("cash") ?? 0 }
on _deposit(amount: Int) {
require amount > 0 else InvalidAmount
account.set("cash", balance() + amount)
return balance()
}
on _debit(amount: Int) {
require amount > 0 else InvalidAmount
account.set("cash", balance() - amount) // raises kind "invariant" when it would overdraw
return balance()
}
}
cell Payments {
face {
signal propose(id: String, amount: Int) -> Map
signal authorize(id: String) -> Map
signal sign_off(id: String, officer: String) -> Map
signal settle(id: String) -> Map
signal reject(id: String) -> Map
signal payment(id: String) -> Map
signal request(method: String, path: String, body: String) -> Map
}
memory {
amounts: Map<String, Int> [persistent]
invariant amounts > 0 && amounts <= 1000000
}
// Small payments: proposed -> authorized -> paid.
// Large ones need a second person: proposed -> pending_signoff -> signed_off -> authorized.
state flow {
initial: proposed
proposed -> authorized
proposed -> pending_signoff
pending_signoff -> signed_off
signed_off -> authorized
authorized -> paid
proposed -> rejected
pending_signoff -> rejected
}
on propose(id: String, amount: Int) {
if amounts.get(id) != () { fail("conflict", "payment {id} already exists") }
amounts.set(id, amount) // > 0 and <= 1,000,000 or it raises
return payment(id)
}
on authorize(id: String) {
let amount = _amount(id)
if amount > 10000 {
transition(id, "pending_signoff") // large: a second person first
} else {
transition(id, "authorized")
}
return payment(id)
}
on sign_off(id: String, officer: String) {
require officer != "" else OfficerRequired
transition(id, "signed_off")
transition(id, "authorized")
return payment(id)
}
on settle(id: String) {
// Either order is safe — the handler is atomic. Here the transition
// goes first: when the ledger refuses the debit, the transition is
// rolled back and the payment is still `authorized`.
transition(id, "paid")
_debit(_amount(id))
return payment(id)
}
on reject(id: String) {
transition(id, "rejected")
return payment(id)
}
on payment(id: String) {
return map("id", id, "amount", _amount(id), "state", get_status(id))
}
on _amount(id: String) {
let a = amounts.get(id)
if a == () { fail("not_found", "payment {id}") }
return a
}
// Error kinds → HTTP statuses. Every route is spelled out here, so the
// handlers above are reached through this router (soma check warns when a
// route and a handler share a name; here they do not).
on request(method: String, path: String, body: String) {
let r = try { _route(method, path, body) }
if r.error == () { return r.value }
if r.kind == "not_found" { return response(404, map("error", r.detail)) }
if r.kind == "conflict" { return response(409, map("error", r.detail)) }
if r.kind == "invalid_transition" { return response(409, map("error", r.error)) }
if r.kind == "invariant" { return response(422, map("error", "refused by the ledger", "detail", r.error)) }
return response(422, map("error", r.error))
}
on _route(method: String, path: String, body: String) {
return match map("method", method, "path", path) {
{method: "POST", path: "/payments"} -> {
let p = from_json(body)
propose(p.id, p.amount)
}
{method: "POST", path: "/payments/authorize/" + id} -> authorize(id)
{method: "POST", path: "/payments/settle/" + id} -> settle(id)
{method: "POST", path: "/payments/reject/" + id} -> reject(id)
{method: "GET", path: "/payments/" + id} -> payment(id)
{method: "GET", path: "/balance"} -> map("balance", balance())
_ -> response(404, map("error", "no route"))
}
}
}
cell test PaymentsTests {
rules {
assert _deposit(500) == 500
// happy path, small payment
assert propose("p1", 200).state == "proposed"
assert authorize("p1").state == "authorized"
assert settle("p1").state == "paid"
assert balance() == 300
// a rejected debit leaves the payment where it was — atomic handler
assert propose("p2", 900).state == "proposed"
assert authorize("p2").state == "authorized"
let s = try { settle("p2") }
assert s.kind == "invariant"
assert payment("p2").state == "authorized"
assert balance() == 300
// large payments need a second person; the proof: paid requires authorized
assert propose("p3", 50000).state == "proposed"
assert authorize("p3").state == "pending_signoff"
assert_fails settle("p3") matching "invalid transition"
assert sign_off("p3", "cfo").state == "authorized"
// errors carry kinds the router turns into statuses
assert_fails propose("p1", 1) matching "conflict"
assert_fails payment("nope") matching "not_found"
assert_fails propose("p4", 0) matching "invariant"
assert request("GET", "/payments/nope", "")._status == 404
assert request("POST", "/payments/settle/p1", "")._status == 409
assert request("GET", "/balance", "").balance == 300
assert request("POST", "/payments", "{\"id\":\"p5\",\"amount\":10}").state == "proposed"
}
}What the toolchain says
$ soma check payments_approval.cell
✓ All checks passed.$ soma test payments_approval.cell
test PaymentsTests ...
✓ assert _deposit(500) == 500
✓ assert propose("p1", 200).state == "proposed"
✓ assert authorize("p1").state == "authorized"
✓ assert settle("p1").state == "paid"
✓ assert balance() == 300
✓ assert propose("p2", 900).state == "proposed"
✓ assert authorize("p2").state == "authorized"
✓ assert s.kind == "invariant"
✓ assert payment("p2").state == "authorized"
✓ assert balance() == 300
✓ assert propose("p3", 50000).state == "proposed"
✓ assert authorize("p3").state == "pending_signoff"
✓ assert_fails settle("p3") matching "invalid transition" — raised invalid transition: pending_signoff → paid. Current state: 'pending_signoff'. Valid targets: [signed_off, rejected]
✓ assert sign_off("p3", "cfo").state == "authorized"
✓ assert_fails propose("p1", 1) matching "conflict" — raised conflict: payment p1 already exists
✓ assert_fails payment("nope") matching "not_found" — raised not_found: payment nope
✓ assert_fails propose("p4", 0) matching "invariant" — raised memory invariant violated on 'amounts': amounts > 0 && amounts <= 1000000 — rejected write of 0 (key "p4"); the slot is unchanged
✓ assert request("GET", "/payments/nope", "")._status == 404
✓ assert request("POST", "/payments/settle/p1", "")._status == 409
✓ assert request("GET", "/balance", "").balance == 300
✓ assert request("POST", "/payments", "{\"id\":\"p5\",\"amount\":10}").state == "proposed"
21 tests: 21 passed, 0 failed$ soma verify payments_approval.cell
Verifying payments_approval.cell...
State machine 'flow': 6 states, initial 'proposed'
States: [authorized, paid, pending_signoff, proposed, rejected, signed_off]
✓ 6 states, 7 transitions
✓ all states reachable from 'proposed'
✓ terminal states: [paid, rejected]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 6 literal transitions, 0 dynamic)
✓ termination: all 9 handlers structurally terminate
✓ refinement: handler `authorize` ⟶ {pending_signoff [if amount > 10000], authorized [if not (amount > 10000)]}
✓ refinement: handler `sign_off` ⟶ {signed_off, signed_off → authorized}
✓ refinement: handler `settle` ⟶ {paid}
✓ refinement: handler `reject` ⟶ {rejected}
· transition sources: `authorize`, `sign_off`, `settle`, `reject` 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)
Ledger/invariants:
✓ invariant account >= 0 — writer '_deposit' proven by induction (writes balance() + amount)
⚠ invariant account >= 0 — runtime-checked (computed values): _debit → account
Payments/invariants:
⚠ invariant amounts > 0 && amounts <= 1000000 — runtime-checked (computed values): propose → amounts because `amount` is a parameter (narrow it: `require amount > 0 && amount <= 1000000 else …`)
12 passed, 2 warnings, 0 failures
Temporal properties for 'flow':
✓ deadlock_free — no deadlocks in any reachable state
✓ eventually(state in [paid, rejected]) — all paths eventually satisfy the predicate
✓ requires('paid' only after one of [authorized]) — every path to 'paid' passes through one of [authorized]
✓ after('rejected', never 'paid') — 'paid' is unreachable once 'rejected' has been reached
✓ after('rejected', never 'authorized') — 'authorized' is unreachable once 'rejected' has been reached
✓ after('rejected', never 'signed_off') — 'signed_off' is unreachable once 'rejected' has been reached
✓ after('paid', never 'rejected') — 'rejected' is unreachable once 'paid' has been reached
soma.toml: 6 user-defined properties loaded
Temporal: 7 passed, 0 failed
note: state machines are verified per cell. A value crosses a cell boundary by a face contract (`requires` / `ensures` on the signal); a rule between two cells' machines is proven when soma.toml states it — `[verify.before."Pay.paid"] requires = ["Approval.signed_off"]` explores the product of the machines.
VERIFY OK$ soma run payments_approval.cell request POST /payments '{"id":"p1","amount":250}'
{"id": "p1", "amount": 250, "state": "proposed"}