Limits & Rules
Centralised rules engine for deposits and withdrawals -- limits, KYC gates, responsible gambling, AML
Purpose
Business limits
Min/max per transaction, daily/weekly/monthly deposit caps -- all configurable per brand and per payment method.
KYC & compliance gates
Block or redirect users who have not completed required identity verification before a deposit or withdrawal threshold.
Responsible gambling
Self-exclusion checks, cooling-off enforcement, player self-set deposit limits. Regulatory requirement in most licensed jurisdictions.
AML monitoring
Flag large or suspicious transactions for manual review without blocking the payment. Does not expose flags to the player.
Withdrawal protections
Balance sufficiency, wagering locks from active bonuses, max pending withdrawals cap.
Audit trail
Every evaluation is logged (allow and block) for compliance reporting and dispute resolution.
Where it sits and how information flows
Position in the stack
Invocation flow
Input data and sources
user_id, brand_id, amount, currency, direction
from Request Router (JWT context)
KYC level, self-exclusion status, account status
users table (cached in Redis)
Min/max per method, KYC thresholds, brand caps
brand_limits table (cached)
Accumulated deposit/withdrawal sums (day/week/month)
transactions table aggregate (Redis cache)
Player self-set responsible gambling limits
player_limits table
Active bonus wager requirements, locked balance
Bonus Engine (async query)
Available balance (real money only, excl. bonus)
Wallet Engine (real-time)
Wallet Engine and Bonus Engine are called only when relevant: Wallet for withdrawal balance checks, Bonus Engine for wager lock checks. Do not call these services on deposit requests unnecessarily.
Rules and limits required at launch
Rule evaluation order
Per-transaction limits (min / max)
| Rule | Scope | Example value | Configurable? |
|---|---|---|---|
| Min deposit per transaction | brand + method + currency + country | e.g. $10 (configurable) | Yes |
| Max deposit per transaction | brand + method + currency + country | e.g. $5 000 (configurable) | Yes |
| Min withdrawal per transaction | brand + method + currency + country | e.g. $20 (configurable) | Yes |
| Max withdrawal per transaction | brand + method + currency + country | e.g. $10 000 (configurable) | Yes |
| Manual approval threshold (withdrawal) | brand + method | e.g. > $2 000 (configurable) | Yes |
Accumulation limits (caps)
| Rule | Scope | Value / note | Configurable? |
|---|---|---|---|
Daily deposit cap Required for gambling licenses in most jurisdictions | user + brand | Brand default; overrideable by player (RG) | Yes |
Weekly deposit cap | user + brand | Brand default; overrideable by player (RG) | Yes |
Monthly deposit cap | user + brand | Brand default; overrideable by player (RG) | Yes |
Max pending withdrawals Prevents abuse: player creates many pending requests, then reverses | user | 1–3 (configurable) | Yes |
AML reporting threshold Triggers manual review or CTR flag; values set by regulator | transaction | $10 000 (jurisdiction) | No (regulator) |
KYC gates
| Gate | Threshold | Action | Direction |
|---|---|---|---|
| No KYC (anonymous) | Cumulative deposits < $X | Allow deposit | Deposit |
| KYC Level 1 required | Cumulative deposits ≥ $X | Block deposit → redirect to KYC flow | Deposit |
| KYC required for all withdrawals | Any amount | Block withdrawal until KYC approved | Withdrawal |
| Enhanced Due Diligence (EDD) | Single transaction > $X or monthly > $Y | Flag for manual review + request source of funds | Both |
KYC threshold values ($X, $Y) are configured in brand_limits and depend on the brand's jurisdiction. They must be explicitly set at brand launch -- they are not inherited automatically.
Error codes and responses
| Code | HTTP | Description (server) | Player-facing message |
|---|---|---|---|
SELF_EXCLUDED | 403 | User is self-excluded or on cooling-off period | Player sees: "Your account is temporarily restricted" |
ACCOUNT_LOCKED | 403 | Account is suspended or locked by ops | Player sees: "Please contact support" |
KYC_REQUIRED | 422 | KYC verification required before this transaction | Player sees: "Verify your identity to continue" |
AMOUNT_BELOW_MINIMUM | 422 | Amount is below the configured minimum | Player sees min amount in the UI |
AMOUNT_ABOVE_MAXIMUM | 422 | Amount exceeds the configured maximum | Player sees max amount in the UI |
DAILY_LIMIT_REACHED | 422 | Player has hit their daily deposit cap | Player sees remaining cap + reset time |
WEEKLY_LIMIT_REACHED | 422 | Player has hit their weekly deposit cap | Player sees remaining cap + reset time |
MONTHLY_LIMIT_REACHED | 422 | Player has hit their monthly deposit cap | Player sees remaining cap + reset time |
INSUFFICIENT_BALANCE | 422 | Withdrawal amount exceeds available balance | Player sees current balance |
WAGER_NOT_COMPLETE | 422 | Active bonus has an unfulfilled wagering requirement | Player sees wager progress |
MAX_PENDING_WITHDRAWALS | 422 | Player already has max pending withdrawal requests | Player sees pending count |
{
"error": "DAILY_LIMIT_REACHED",
"message": "You have reached your daily deposit limit.",
"remaining_cap": 0,
"reset_at": "2026-06-05T00:00:00Z",
"limit_type": "daily_deposit",
"brand_id": "brand_abc"
}Responsible Gambling (RG)
RG tool catalog
Self-exclusion
Player blocks themselves from all deposits and gameplay for a defined period (1 day -- permanent). The platform must enforce this at the payment layer -- any deposit attempt returns SELF_EXCLUDED error. Exclusion cannot be reversed by the player during the active period -- only by operator support after a mandatory review process.
Cooling-off period
When a player wants to raise or remove a self-set deposit limit, the change does not take effect immediately -- it takes effect after a mandatory delay (typically 24--72 hours, jurisdiction-specific). This prevents impulsive reversal of protective limits. The player can cancel the increase request during the cooling-off window.
Player deposit limits
Player sets their own deposit caps -- these can only be decreased immediately; increases require cooling-off. Player limits always take precedence over brand limits -- if the brand daily cap is $500 and the player set $100, the effective cap is $100. Stored in player_limits table.
Loss limits
Player sets a maximum net loss (deposits minus withdrawals) over a period. More complex than deposit limits -- requires tracking game outcomes, not just payment amounts. Calculated as: sum(deposits) - sum(withdrawals) over the rolling window. When the limit is reached, deposits are blocked for the remainder of the period.
Affordability check
Required by UK UKGC from Sept 2024 (Enhanced Affordability): operators must verify player income when cumulative deposits exceed thresholds (£500/month at tier 1, £150/month at tier 2 for Enhanced). Platform checks accumulated deposits and flags when KYC upgrade is required. Source documents: bank statements, pay slips.
External exclusion list check
Some jurisdictions maintain national self-exclusion registries. Operators must query these at player registration and optionally on each deposit. Integration is per-jurisdiction: GamStop (UK), OASIS (Germany), Spelpaus (Sweden), Spelinspektionen. The platform provides an IExclusionProvider interface -- each jurisdiction has its own adapter.
Jurisdiction requirements matrix
| Jurisdiction / License | Self-excl. | Cooling-off | Dep. limits | Loss limits | Afford. check | Ext. registry |
|---|---|---|---|---|---|---|
| UK (UKGC) | GamStop | |||||
| Sweden (Spelinsp.) | Spelpaus | |||||
| Germany (GGL) | OASIS | |||||
| Malta (MGA) | -- | |||||
| Estonia (MTA) | -- | |||||
| Curacao / Anjouan | -- |
Critical implementation rules
Self-exclusion is checked first -- before any other rule
Already rule #1 in the evaluation order. Cached in Redis (TTL 60s). A stale exclusion that allows a deposit through is a regulatory violation -- prefer false positives over false negatives.
Player limits can only be decreased immediately -- increases require cooling-off
Store requested_limit and effective_at separately. A background job applies the new (higher) limit when effective_at is reached. Player can cancel the pending increase at any time during cooling-off.
Self-exclusion cannot be reversed by the player directly
No "undo self-exclusion" button in the player UI. Reversal requires operator support intervention after a mandatory review and waiting period (jurisdiction-defined, typically 24h--7 days). The UI must make this clear at the moment of setting exclusion.
Operator cannot set limits more permissive than regulatory minimums
Example: Germany mandates a hard €1,000/month deposit cap. Operator brand_limits cannot exceed this regardless of what they configure. Platform enforces regulatory floors as a non-configurable layer below brand limits.
All RG events are logged immutably in player_rg_events
Every self-exclusion, limit change, cooling-off trigger, and RG block must be logged with timestamp, player_id, operator_id, and who triggered it (player or ops). This log is the primary evidence in regulatory audits and player dispute resolution.
DB schema additions
player_rg_events (
id uuid PK,
player_id uuid FK,
operator_id uuid FK,
brand_id uuid FK,
event_type text, -- SELF_EXCLUSION_SET | SELF_EXCLUSION_LIFTED
-- LIMIT_DECREASED | LIMIT_INCREASE_REQUESTED
-- LIMIT_INCREASE_APPLIED | LIMIT_INCREASE_CANCELLED
-- COOLING_OFF_STARTED | DEPOSIT_BLOCKED_RG
-- AFFORDABILITY_FLAG_RAISED | EXT_EXCLUSION_MATCH
payload jsonb, -- event-specific detail (old value, new value, reason)
triggered_by text, -- 'player' | 'ops:{user_id}' | 'system'
created_at timestamptz
-- append-only: no updates, no deletes
)
-- Additions to player_limits:
ALTER TABLE player_limits ADD COLUMN
requested_daily_deposit_cap numeric, -- pending increase, null if none
requested_effective_at timestamptz, -- when pending increase applies
loss_limit_daily numeric, -- null if not set
loss_limit_weekly numeric,
loss_limit_monthly numeric,
self_excluded_until timestamptz, -- null if not excluded
self_excluded_permanent boolean DEFAULT false,
affordability_threshold_reached boolean DEFAULT false,
ext_exclusion_checked_at timestamptz -- last external registry checkBest practices
Configuration
- All limits stored in DB (brand_limits table), not hardcoded -- ops team must be able to change them without a deploy
- Player self-set limits (responsible gambling) stored separately and can only be decreased, never increased without a cooling-off period
- Default brand limits applied on brand creation -- never null in production
Performance
- Cache user KYC level and self-exclusion status in Redis (TTL ~60s) -- these are hot-path reads
- Pre-compute and cache accumulated totals (daily/weekly/monthly sums) -- recompute on each transaction, not on each check
- Short-circuit on first BLOCK -- do not run all 8 checks if rule 1 already blocks
- Total overhead target: < 5ms (rules evaluation only, excluding network)
Compliance & Audit
- Write an audit log entry for every rule evaluation -- include: user_id, brand_id, direction, amount, rule triggered, result, timestamp
- Audit logs must be immutable -- append-only, no updates. Required for regulatory audits
- Self-exclusion changes: 24h propagation delay by law in some jurisdictions -- design accordingly
- AML flags must not surface to the player -- send to internal monitoring queue silently
Frontend contract
- Always return remaining_cap in the error payload for limit-related blocks -- frontend shows "You have $X left today"
- Return reset_at timestamp for cap limits -- frontend shows countdown to reset
- KYC_REQUIRED must include kyc_redirect_url so frontend can route without hardcoding paths
- Pre-validate amounts client-side too (fetch limits on checkout load) -- but always enforce server-side; client is UX only
-- Per-brand configurable limits brand_limits ( brand_id, method, min_deposit, max_deposit, min_withdrawal, max_withdrawal, daily_deposit_cap, weekly_deposit_cap, monthly_deposit_cap, kyc_threshold_deposit, kyc_threshold_withdrawal, max_pending_withdrawals ) -- Player self-set responsible gambling limits (only decreaseable) player_limits ( user_id, brand_id, daily_deposit_cap, weekly_deposit_cap, monthly_deposit_cap, set_at, effective_at -- effective_at = set_at + cooling_off_period ) -- Append-only audit log limits_audit_log ( id, user_id, brand_id, direction, amount, currency, rule_triggered, result, -- 'ALLOW' | 'BLOCK' error_code, -- null if ALLOW created_at )