On this page
ADR-0032: Payout gates are identical for simulated and live runs
Status: Accepted (WP-14). Implemented in crates/commonlayer-funding/src/domain/payout.rs and src/service/payouts.rs.
Context
Paying people is the highest-risk operation of the platform. A simulated mode exists so the payout path can be exercised before any real money moves; if simulated runs skipped gates, the gates themselves would never be exercised before going live.
Decision
- Execution of a
simulatedorlivepayout run requires every gate: the run's mode equalspayments.payouts.mode; the provider is configured and reports the payouts capability; the legal entity name, jurisdiction and registration are configured;payments.tax.handling_confirmed = true; the latest reconciliation of each kind is clean and less than 24 h old; a second platform admin approved the run. Simulated runs additionally require a non-production environment. - Failed gates are reported by name (
payouts.not_enabled). - Approving a run requires an interactive session (it is never available to API tokens), and the database refuses an approver equal to the initiator.
- A dry run is always available, posts nothing, and lists every skipped obligation with its reason (no verified destination, tax status missing, below minimum, held).
Consequences
- Configuration refuses
payments.payouts.mode = "live"without the legal entity fields and a real provider, and refuses simulated payments in production. - Live payouts through Stripe Connect are implemented but have only been exercised against a mock server, never against a live Stripe account.