Authorization Attempt Quota
Context
The high-risk (NSFW) PSP enforces daily/weekly/monthly authorization attempt caps per user. Attempts exceeding these limits are automatically declined — the money won’t go through. Without pre-checkout enforcement:
- Users waste checkout attempts that the PSP will decline anyway
- Dunning retries burn quota invisibly — no coordination with CIT attempts
- Poor user experience: payment silently fails with no clear explanation
Decision
Split authorization-attempt enforcement into CIT (customer-initiated) and MIT (merchant-initiated) paths with a reservation buffer.
Limits:
| Window | Limit | CIT effective | MIT effective |
|---|---|---|---|
| Daily | 5 | 4 | 5 |
| Weekly | 20 | 19 | 20 |
| Monthly | 30 | 29 | 30 |
CIT path (ReservePaymentAttempt): Pre-checkout gRPC call from Brisket. Atomic check+insert using pg_advisory_xact_lock(hashtext(user_id)) to prevent TOCTOU races from concurrent browser tabs. Records authorization_attempt fraud event, enforces limit - 1 (reserves 1 slot for MIT renewal), returns allowed, remaining_attempts, reason. Fail-closed: any error returns allowed: false.
MIT path (dunning retry): Inline count-quota gate in ExecuteRenewalRetry. Uses full limits (no buffer) since MIT consumes the slot CIT reserved. Invoice-level distributed lock serializes retries for the same invoice. Sequential worker loop serializes within a poll cycle. Records authorization_attempt before PSP call. Quota-blocked attempts recorded as quota_blocked dunning attempts to prevent retry spam.
Scope: Enforcement is production-only (IsProd()), NSFW-tier only, and EMP-processor only (shouldEnforceHighRiskProcessorQuota). SFW renewals and NMI-routed NSFW users skip all quota checks.
Amount windows: Separate from count quotas, amount-based velocity windows enforce USD caps ($1800/$2000/$3000 daily/weekly/monthly) on settled payments. These use payment_successful events, not authorization_attempt.
Consequences
Positive:
- PSP quota enforced before money moves
- CIT and MIT coordinated via buffer reservation
- Quota-blocked dunning attempts recorded — no retry spam
- Frontend shows differentiated error messages (daily vs weekly/monthly)
Negative:
- CIT effective limits are 1 lower than PSP caps
- Production-only enforcement means staging/dev cannot test quota blocking without overrides
- MIT quota gate is not atomic (acceptable given serialization layers)
Observability:
authorization_attemptrows infraud_eventstablevelocity_triggeredfraud event on CIT block- PostHog
authorization_attempt_blockedevent - Slack alert on CIT block via
logReservePaymentAttemptBlock - Velocity block logs include
path: cit_checkoutorpath: mit_renewal
Alternatives Considered
Single shared atomic reservation for both CIT and MIT: Would close the theoretical MIT TOCTOU gap but adds per-user advisory locks to the dunning path. CIT needs this because users can spam checkout from multiple tabs. MIT is machine-driven — invoice lock + singleton scheduler + sequential processing make concurrent MIT for the same user effectively impossible. Adding per-user advisory locks introduces connection pinning overhead and deadlock risk disproportionate to a near-zero probability race.
No CIT buffer (MIT and CIT share the same limits): Without the buffer, a user who exhausts all 5 daily slots via checkout blocks their own renewal retry. The PSP would decline the MIT attempt, and the subscription would churn despite a valid payment method.
Fail-open on quota check errors: Allowing checkout through on failure would waste a PSP attempt that will be declined anyway. A temporary block with a clear user-facing message is preferable.