PayFlow

Agentic SDLC · verification pyramid

A human does not read every line.

A payment system built by an AI agent from a frozen spec. Nobody reviews every line before it ships; four automatic checks stand in for that review, each catching a class of mistake the other three would miss. The whole trick is one boundary: an agent proposes the checks, a deterministic engine disposes by deciding pass or fail, so the model never grades its own work.

4
checks: 2 block a merge, 2 warn nightly
0
hand written test cases behind the kill rate
<3 min
blocking checks, per change
~$0.45
total LLM spend, whole build

What PayFlow is

Seven states, integer money, no floats.

A payment intent moves through seven states. Every amount is whole minor units, never a fraction. There is no dispute or chargeback flow in the first version.

  • moneyInteger minor units. No float touches any money path, ever.
  • terminalVOIDED and REFUNDED end the intent. Any later operation is rejected and nothing changes.
  • ledgerAppend only. Corrections are new entries, and one component writes them.
CREATED AUTHORIZED PARTLY_CAPTURED CAPTURED PARTLY_REFUNDED VOIDEDterminal REFUNDEDterminal
Solid: the capture and refund path. Dashed: routes to VOIDED.

How a change gets checked

fail spec/ agent codes L0 structural L1 behavioral MERGE fix message L2 + L3 nightly
Layers 0 and 1 block a merge. Layers 2 and 3 run nightly and warn.

A failure is written for the agent, not a human.

Every change runs the pyramid before it merges. A failure comes back as the violated rule, the shrunk counterexample, or the exact offending import, so the coding agent can act on it and try again.

  • blockLayers 0 and 1 gate every commit and stop a bad merge in seconds.
  • nightlyLayers 2 and 3 run slower, warn only, and feed drift back into the loop.
  • humanPulled in only when a verdict cannot be trusted (needs_human), or a rule does not exist yet and needs a new ADR.

Proof you can run

Change the spec, and the gate stops you.

The safety argument is one claim: you cannot change the frozen spec without the pipeline noticing. So we tested it. We added one new invariant to specs/invariants.md and changed nothing else.

  • addedINV-8: the sum of a merchant's captured amounts never exceeds its lifetime authorized total.
  • caughtThe blocking suite went red at once. A human moved the contract, and the gate refused the build until the new rule is accounted for: listed in the coverage inventory and given a real oracle in the discovered suite.
  • revertedINV-8 was a demonstration, not a real rule. The point stands: a spec change is a deliberate, visible event, never a silent one.
$ uv run pytest tests/  FAILED
spec_coverage.json invariant inventory
drifted from specs/invariants.md:

  frozen but uninventoried ['INV-8']

Every frozen invariant must appear in the
coverage inventory so its oracle presence
is tracked.

The same gate guards the other direction. If a fresh discovery run silently drops an oracle it used to carry, "covered stays covered" turns the build red too. A real regeneration recently did exactly that, dropping the non negativity oracle, and in doing so exposed a blind spot in this very gate; it was strengthened to anchor on the oracle's own assertion, so it now catches that case as well. The discipline audits itself by being run.

The four checks

Each layer falsifies a different claim.

L0 · structuralblocks L1 · behavioralblocks L2 · agent judgmentwarns L3 · mutationwarns
Each layer above validates the layer below it.

In plain terms: an invariant is a fact that must always hold (never capture more than was authorized); a metamorphic relation checks that two routes to one result match (100 at once must equal 50 then 50), catching bugs no single run can see; Hypothesis generates thousands of random operation sequences and shrinks any failure to its smallest form; the mutation kill rate is the share of deliberately injected bugs the suite notices.

00
import-linter

Is the code even in the right place?

A static contract enforces the layering api → domain → infrastructure and a single writer to the ledger tables.

Once caught: an admin route wired straight into the ledger, bypassing all domain validation. Reported 2 kept, 1 broken and blocked the merge.

blocks merge
01
Hypothesis

Does it behave the way the spec says?

Agent discovered invariants (INV-1..7) and metamorphic relations (MR-1..6) replay against the implementation across thousands of generated situations, plus a concurrency harness.

Once caught: a race no sequential test can express. Sixteen threads sending one idempotency key produced up to six duplicate captures, while the sequential replay stayed green.

blocks merge
02
AGENT-MR

Can you trust the checker's own verdict?

This layer tests the triage agent, not PayFlow. Reword a failure without changing its data, and the verdict should not move.

Once caught: a fee misroute verdict flipped from real_bug to bad_relation on a single reword, while reordering and padding never moved it. The fragility is lexical.

nightly · warns
03
mutmut

Is the suite actually checking anything?

A mutation pass breaks the payment core in small ways and asks whether any test notices. The kill rate is the ground truth on Layer 1.

Once caught: a refund path the suite never reached. 62 of 181 survivors clustered in service.refund. Closing that loop moved the kill rate from a 65.3% floor to 73.1%.

nightly · warns

Results

Injected bugs killed by the agent's own suite, with zero hand written test cases.

Every property was discovered by an agent and every counterexample found by Hypothesis. Two checks block a merge, two run nightly and warn until baselined. The whole build cost roughly forty five cents in model calls, estimated from the committed run reports.

The agent suite matches the full local suite exactly, 73.1% both: a hand written Phase 1 sanity machine, run alongside, adds no kills the agent's own rules, invariants, and relations do not already make. So the headline stands entirely on the agent's work. The live numbers, from real run artifacts, are just below.

Trust report, measured

Every number below comes from a real artifact.

Nothing here is hand typed: gate status is run live, kill rates come from the committed mutation baseline, and the discovery funnel is read from each agent run.

PASS
fast gates, run live just now
n/a
no validation run baked in (see uv run catch)
n/a
triage false positives not measured this build
0
recorded agent discovery runs

Discovery funnel

No agent runs recorded yet.

Layer 3 mutation ground truth

How many injected bugs the suite actually kills.

Kill rate is killed over covered. Mutants on paths a suite never exercises are shown, not folded in to flatter the number. Scope: payflow/domain, payflow/infrastructure/ledger/core.py. Baseline generated 2026-07-11T22:18:26Z, nightly recomputes.

Agent discovered suites only, zero hand written tests
73.1%
385 killed142 survived44 never covered
385 detected / 527 covered · 571 generated · 146.8s
Full local suite, agent suites plus the Phase 1 sanity machine
73.1%
385 killed142 survived44 never covered
385 detected / 527 covered · 571 generated · 188.4s

Semantic exploration · informational

Realistic bugs the suite misses, mapped to the frozen spec.

This is not a gate and is separate from the kill rate above. A cross family LLM adversary injects realistic bugs mutmut's syntactic operators cannot express; each survivor is then confirmed by hand against specs/ (ADR-0007: the spec is the referee). A survivor is a candidate gap for a human, not a proven bug. The number is adversary dependent, so a strong score is never mistaken for the gated one.

run: anthropic:claude-sonnet-5 · killed 1 · survived 8 · of 9 · 2026-07-03 13:25 UTC

Frozen spec coverage (survivors confirmed against specs/)

6 full · 2 weak or unreached · 4 absent of 12 frozen rules: where a human pushes discovery next.

idfrozen requirementcoveragenote
INV-1captured_amount ≤ authorized_amountfullcaptured_le_authorized invariant
INV-2refunded_amount ≤ captured_amountfullrefunded_le_captured invariant
INV-3merchant, holds, platform_fees never negativefullclosed 2026-07-11: the non-negativity check now asserts merchant, holds and platform_fees are all >= 0 once per example (external_settlement, the funding source, may go negative)
INV-4global debits == credits (conservation)fullclosed 2026-07-11: a once-per-example conservation oracle asserts the net delta across every account this run touched is zero, measured against a per-instance system-account baseline (safe on a shared server since examples run sequentially)
INV-5transitions only along the state table; nothing from VOIDED or REFUNDEDpartialillegal-state rejection rules cover most transitions
INV-6preconditions enforced: illegal op returns 409, changes nothingfullauthorize/capture/refund/void illegal-state rules plus assert_unchanged
INV-7every AUTHORIZED+ intent has ≥ 1 ledger entry pairabsentno check that a ledger entry pair exists
amount ≥ 1amount < 1 returns 422absentzero or negative amount never probed (survivor: allow-zero)
capture > feecapture ≤ fee returns 422absentboundary where amount equals the fee never probed (survivor: fee-equal)
capture ≤ remainingcapture over remaining hold returns 422fullcapture_over_limit_rejected
refund ≤ refundablerefund over refundable returns 422reachabilityrefund_over_limit_rejected exists; sequence unreached even at 6x budget (survivor: refund-cap-total)
idempotencysame key, different endpoint returns 409absentthe replay relation replays one endpoint only; no discovered relation crosses endpoints under a shared key (survivor: idempotency-cross-endpoint)

The pipeline

Who proposes, what checks it, and whether it is green now.

The whole trust argument is one boundary: the LLM proposes (fallible), a deterministic engine disposes by compiling, executing, and scoring. The graph is coloured by that split (from agent/roles.py, drift gated); the gates below are the deterministic side, run live.

startingest_specinfer_endpoint_rulesinfer_invariantsinfer_relationscompile_specexecutetriagerefinereportend
the LLM proposes (fallible)the engine disposes (trustworthy)

Live gate status · all fast gates green

Layer 0 structural and drift plus the Layer 1 replay slice, run live when this page was generated. These are what block a merge.

layergateresultdetailelapsed
Layer 0import contractsPASS3 kept, 0 broken0.12s
Driftstate machine diagramPASS1 passed0.58s
Driftimportlinter snapshotPASS1 passed0.49s
Driftagent graph diagramPASS1 passed0.99s
Layer 1property sanity machinePASS1 passed3.12s
Layer 1idempotent replay (concurrency A)PASS1 passed2.19s