Skip to content

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:

  1. Users waste checkout attempts that the PSP will decline anyway
  2. Dunning retries burn quota invisibly — no coordination with CIT attempts
  3. 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:

WindowLimitCIT effectiveMIT effective
Daily545
Weekly201920
Monthly302930

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_attempt rows in fraud_events table
  • velocity_triggered fraud event on CIT block
  • PostHog authorization_attempt_blocked event
  • Slack alert on CIT block via logReservePaymentAttemptBlock
  • Velocity block logs include path: cit_checkout or path: 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.