Operational guardrails
What this is: the governance and testing discipline specific to money paths — the rules that sit around the code, not in it.
Who it’s for: anyone (human or agent) touching a payment or ledger path; anyone reviewing such a change.
What to read next: Routing & failover · Status & roadmap · Entity model.
Money paths are not normal code. ADR-0093’s governance section is binding: payment / ledger changes require human sign-off — no autonomous-agent merge — plus injected-failure + idempotency tests and provider sandbox cert gates.
1. Human sign-off — no autonomous merge on money paths (LOO-2215)
The platform supports many agents working in parallel. Money paths are carved out. A change to a payment or ledger path:
- requires explicit human sign-off; an agent may not autonomously merge it;
- must ship with injected-failure + idempotency tests (below);
- must pass provider sandbox certification gates before a provider goes live.
This is why the foundation is deliberately inert: the orchestrator and vault are merged but not wired to a live charge, so review and sign-off happen before money can move, not after.
2. Failure-injection test discipline
The safety invariants are only credible because failures are injected and asserted, not assumed. The foundation’s StubProvider exists precisely to drive failures on demand: a charge can be made to succeed, decline, timeout, or error, per MID, with a side-effect that fires even on timeout to model “the gateway may have captured before the wire response.”
This is what lets the no-double-charge proof actually prove something:
- inject a decline on the primary → assert failover to standby, captured exactly once;
- inject a timeout on the primary (with the capture side-effect firing) → assert the cascade HALTs and the standby is never charged;
- re-run the same cascade → assert identical per-attempt idempotency keys.
Every money-path change should extend this discipline: if you add a code path that moves money, add a test that injects the ambiguous failure (timeout / transport error) and asserts no double charge. A green happy-path test is not sufficient evidence for a money path.
All of this runs fully offline against StubVault + StubProvider — no Basis Theory key, no PSP key, no network — so the proofs run in CI on every change. The same flow against a real Basis Theory TEST tenant lives in a gated spike runner (scripts/spike-basis-theory.ts, gated on a key_test_ key).
3. Phase-0 gating — banking & compliance before code
The biggest guardrail is sequencing: Phase 0 (banking & compliance) gates the software. ADR-0093’s v2 red-team inverted the original risk framing — the existential risks are business risks (acquiring access, laundering/MATCH, un-reconcilable ledger), not commodity vault/gateway code.
Phase 0 deliverables (all non-code, must land first):
- MID / acquirer / TOS audit per LLC × product, in writing;
- confirm Stripe’s RUO posture;
- stand up ≥2 warm high-risk acquirers per RUO entity (4–12 weeks of underwriting each);
- counsel sign-off on the entity ↔ product ↔ MID matrix + MoR / tax;
- secure a Loop-owned TRID.
Acquiring access — not code — is the bottleneck. A routing engine has no value without ≥2 warm high-risk MIDs per RUO entity. This is why the seeded MIDs are Phase-0 placeholders and why the routing path is inert until counsel + banking confirm the real matrix.
4. Reconciliation, reserves, MoR, and tax are named workstreams
The red-team confirmed the legacy ledger could not reconcile — transactions / ledger_entries had no entity, fee, reserve, payout, or settlement dimension. So the foundation adds recon-ready dimensions to the accounting ledger from the start (additive, nullable, append-only-safe):
From services/accounting/migrations/0006_recon_dimensions.sql:
ALTER TABLE accounting.transactions
ADD COLUMN IF NOT EXISTS entity_id TEXT,
ADD COLUMN IF NOT EXISTS mid_id TEXT,
ADD COLUMN IF NOT EXISTS currency TEXT NOT NULL DEFAULT 'USD',
ADD COLUMN IF NOT EXISTS fx_rate_to_usd NUMERIC(18, 8),
ADD COLUMN IF NOT EXISTS settlement_batch_id TEXT;These exist so 3-way reconciliation per MID per entity (Phase C, LOO-2210) is possible across N providers × M entities. The ledger stays sacred: entity_id / mid_id / currency / fx_rate_to_usd are set once at post and immutable; settlement_batch_id is the one dimension that may be set later (when a payout batch settles) but is then immutable too. entity_id is a logical ref — no cross-schema FK, because accounting must not couple to another service’s schema.
Reconciliation, reserve/settlement modeling, chargeback survival (ECM / VAMP thresholds, Ethoca / RDR, representment), Merchant-of-Record, and sales-tax / VAT / nexus are Phase C — planned named workstreams. The recon-ready columns are built; the recon engine is not.
5. Idempotency is fixed, not assumed
LOO-2203 (the shipped bug where accounting-client.ts built its idempotency key with randomUUID(), so a retry double-booked the ledger) is fixed by the deterministic logical-attempt-id discipline. The caller supplies a stable id (e.g. from the order), so the accounting key and the per-PSP keys are stable across retries. See Routing & failover.
See also
- Routing & failover — the invariants these guardrails protect
- Status & roadmap — what is gated on Phase-0
- Entity model — the placeholders Phase-0 replaces
- Audit and PHI — every money-path handler writes an audit row
Source
services/accounting/migrations/0006_recon_dimensions.sql·services/payments/src/lib/stub-provider.ts- ADR-0093 (Governance; Phase 0; rule 8/9 — LOO-2215, LOO-2209)