PaymentsOperational guardrails

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

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)